@leapfrog/interactive-canvas 2.0.25-beta-14 → 2.0.25-beta-15

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.
@@ -340,8 +340,10 @@ export declare abstract class AbstractInteractiveCanvas implements IInteractiveC
340
340
  * @inheritDoc
341
341
  */
342
342
  cloneObject(sourceObject: FabricObject, objectData: CanvasObjectData): Promise<void>;
343
+ private redrawScheduled;
343
344
  /**
344
345
  * Redraw the canvas.
346
+ * Uses requestAnimationFrame to batch multiple redraw calls into a single render.
345
347
  */
346
348
  redraw(): void;
347
349
  syncObjectList(): void;
@@ -20,7 +20,6 @@ export declare class InteractiveCanvas extends AbstractInteractiveCanvas {
20
20
  */
21
21
  redo(): Promise<void>;
22
22
  private onFabricSelectionChanged;
23
- private enterTextSelectionModeIfRequired;
24
23
  private onTextEditing;
25
24
  private onDoubleClick;
26
25
  }
@@ -232,9 +232,15 @@ class ResizeCanvasLayoutFunction {
232
232
  const targetHeight = increase.withSize(increase.width, baseDimensions.height + increase.height);
233
233
  if (currentDimensions.height !== targetHeight.height) {
234
234
  canvas.setDimensions(targetHeight);
235
+ canvas.getObjectsInternal().forEach((object) => {
236
+ var _a;
237
+ const canvasObj = canvas.getObjectsByTypeInternal(object.type).find(o => o.getId() === object.getId());
238
+ if (canvasObj && (((_a = canvasObj.isPinToBottom) === null || _a === void 0 ? void 0 : _a.call(canvasObj)) || object.top > currentDimensions.heightPx * 0.5)) {
239
+ object.setCoords();
240
+ }
241
+ });
242
+ canvas.redraw();
235
243
  }
236
- canvas.getObjectsInternal().forEach((object) => object.setCoords());
237
- canvas.redraw();
238
244
  }
239
245
  }
240
246
 
@@ -264,7 +270,6 @@ class StackObjectsLayoutFunction {
264
270
  const footerObjects = objects[0];
265
271
  const bodyObjects = objects[1];
266
272
  const bodyWidth = canvasDimensions.widthPx - canvasMargin.right - canvasMargin.left;
267
- canvas.redraw();
268
273
  //Make one pass and set the widths of all elements. This will force objects with dynamic heights (like text) to take on their necessary height before trying to lay them out.
269
274
  this.applyWidths(allObjects, bodyWidth);
270
275
  const bodyRows = this.getRows(bodyObjects);
@@ -351,7 +356,11 @@ class StackObjectsLayoutFunction {
351
356
  object.setPosition({
352
357
  width: actualWidth
353
358
  });
354
- object.updateDimensions();
359
+ });
360
+ _(objects).forEach((object) => {
361
+ if (object.getType() !== ObjectType.VERTICAL_RULE) {
362
+ object.updateDimensions();
363
+ }
355
364
  });
356
365
  }
357
366
  adjustCanvasSize(canvas, requiredHeight) {
@@ -437,15 +446,22 @@ class AdvancedLayoutManager extends AbstractLayoutManager {
437
446
  this.repositionBackgrounds = new RepositionBackgroundsLayoutFunction();
438
447
  this.resizeCanvas = new ResizeCanvasLayoutFunction();
439
448
  this.stackObjects = new StackObjectsLayoutFunction();
449
+ this.dimensionsLayoutScheduled = false;
440
450
  }
441
451
  onCanvasDimensionsChange() {
442
- this.pinToBottom.execute(this.canvas);
443
- // this.stackObjects.execute(this.canvas);
444
- this.dimensionChange();
445
- this.repositionCutoffs.execute(this.canvas);
446
- this.repositionBorders.execute(this.canvas);
447
- this.repositionBackgrounds.execute(this.canvas);
448
- this.redrawAll.execute(this.canvas);
452
+ if (this.dimensionsLayoutScheduled) {
453
+ return;
454
+ }
455
+ this.dimensionsLayoutScheduled = true;
456
+ Promise.resolve().then(() => {
457
+ this.pinToBottom.execute(this.canvas);
458
+ this.dimensionChange();
459
+ this.repositionCutoffs.execute(this.canvas);
460
+ this.repositionBorders.execute(this.canvas);
461
+ this.repositionBackgrounds.execute(this.canvas);
462
+ this.redrawAll.execute(this.canvas);
463
+ this.dimensionsLayoutScheduled = false;
464
+ });
449
465
  }
450
466
  onTextChange() {
451
467
  this.resizeCanvas.execute(this.canvas);
@@ -484,6 +500,7 @@ class StandardLayoutManager extends AbstractLayoutManager {
484
500
  this.repositionBackgrounds = new RepositionBackgroundsLayoutFunction();
485
501
  this.stackObjects = new StackObjectsLayoutFunction();
486
502
  this.updateObjectDimensions = new UpdateObjectDimensionsLayoutFunction();
503
+ this.layoutBatchScheduled = false;
487
504
  }
488
505
  /**
489
506
  * @override
@@ -525,12 +542,19 @@ class StandardLayoutManager extends AbstractLayoutManager {
525
542
  this.stackAndRedraw();
526
543
  }
527
544
  stackAndRedraw() {
528
- this.stackObjects.execute(this.canvas);
529
- this.repositionCutoffs.execute(this.canvas);
530
- this.repositionBorders.execute(this.canvas);
531
- this.repositionBackgrounds.execute(this.canvas);
532
- this.updateObjectDimensions.execute(this.canvas);
533
- this.redrawAll.execute(this.canvas);
545
+ if (this.layoutBatchScheduled) {
546
+ return;
547
+ }
548
+ this.layoutBatchScheduled = true;
549
+ Promise.resolve().then(() => {
550
+ this.stackObjects.execute(this.canvas);
551
+ this.repositionCutoffs.execute(this.canvas);
552
+ this.repositionBorders.execute(this.canvas);
553
+ this.repositionBackgrounds.execute(this.canvas);
554
+ this.updateObjectDimensions.execute(this.canvas);
555
+ this.redrawAll.execute(this.canvas);
556
+ this.layoutBatchScheduled = false;
557
+ });
534
558
  }
535
559
  }
536
560
 
@@ -997,7 +1021,7 @@ class AbstractCanvasObject {
997
1021
  //no-op
998
1022
  }
999
1023
  preventLeavingCanvas() {
1000
- const overflow = this.getCanvasOverflow();
1024
+ const overflow = this.getCanvasOverflowForBoundaryPrevention();
1001
1025
  if (overflow.hasOverflow) {
1002
1026
  const adjustX = overflow.left || -overflow.right;
1003
1027
  const adjustY = overflow.top || -overflow.bottom;
@@ -1023,6 +1047,29 @@ class AbstractCanvasObject {
1023
1047
  hasOverflow: top > 0 || right > 0 || bottom > 0 || left > 0
1024
1048
  };
1025
1049
  }
1050
+ getCanvasOverflowForBoundaryPrevention() {
1051
+ // Use a simpler bounding box calculation for boundary prevention
1052
+ // to avoid issues with decorative elements (like ordered list markers)
1053
+ // extending beyond the main object bounds
1054
+ const bound = this.getSimpleBoundingBox();
1055
+ const canvasDimensions = this.canvas.getDimensions();
1056
+ const top = Math.max(0, 0 - bound.top);
1057
+ const right = Math.max(0, bound.left + bound.width - canvasDimensions.widthPx);
1058
+ const bottom = Math.max(0, bound.top + bound.height - canvasDimensions.heightPx);
1059
+ const left = Math.max(0, 0 - bound.left);
1060
+ return {
1061
+ top: top,
1062
+ right: right,
1063
+ bottom: bottom,
1064
+ left: left,
1065
+ hasOverflow: top > 0 || right > 0 || bottom > 0 || left > 0
1066
+ };
1067
+ }
1068
+ getSimpleBoundingBox() {
1069
+ // Default implementation: use normal bounding box
1070
+ // Subclasses can override this to provide simpler bounds for drag boundary checks
1071
+ return this.getBoundingBox();
1072
+ }
1026
1073
  centerHorizontallyInternal() {
1027
1074
  const canvasWidth = this.canvas.getDimensions().widthPx;
1028
1075
  const objectWidth = this.getDimensions().width.px;
@@ -1070,7 +1117,8 @@ class AbstractCanvasObject {
1070
1117
  mt: options.allowScale,
1071
1118
  mr: options.allowScale,
1072
1119
  mb: options.allowScale,
1073
- mtr: options.allowRotate
1120
+ mtr: options.allowRotate,
1121
+ // dragControl: options.allowMove // Show drag control when movement is allowed
1074
1122
  });
1075
1123
  }
1076
1124
  }
@@ -1794,15 +1842,29 @@ class RichText extends Image {
1794
1842
  class Text extends AbstractCanvasObject {
1795
1843
  constructor(canvas, fabricObject, persistedField) {
1796
1844
  super(ObjectType.TEXT, canvas, fabricObject, persistedField);
1845
+ this.notifyCanvasTimeout = null;
1846
+ this.NOTIFY_DELAY_MS = 150; // Debounce canvas notifications
1797
1847
  this.canvas = canvas;
1798
1848
  this.fontLibrary = canvas.getFontLibrary();
1799
1849
  this.baseComponentHeight = this.fabricObject.height;
1800
1850
  this.text$ = new BehaviorSubject(this.fabricObject.text);
1801
1851
  this.data.userText = this.fabricObject.text;
1802
1852
  this.textSelection$ = new BehaviorSubject(new TextSelection(this.fabricObject, this.fontLibrary));
1803
- this.fabricObject.set({ strokeWidth: 0 });
1853
+ this.fabricObject.set({
1854
+ strokeWidth: 0,
1855
+ perPixelTargetFind: false, // Disable per-pixel detection for this object
1856
+ padding: 0 // Add padding for larger hover area (Fabric v7 compatibility)
1857
+ });
1804
1858
  this.fabricObject.on("changed", () => this.onTextChange());
1805
1859
  this.fabricObject.on("selection:changed", () => this.onSelectionChange());
1860
+ // Note: Removed containsPoint override to allow Fabric's default bounding box
1861
+ // detection with padding to work properly for hover detection
1862
+ this.fabricObject.on("mousedown", (e) => {
1863
+ var _a;
1864
+ if (!this.fabricObject.isEditing) {
1865
+ (_a = e.e) === null || _a === void 0 ? void 0 : _a.preventDefault();
1866
+ }
1867
+ });
1806
1868
  this.applyBulletListMonkeyPatches();
1807
1869
  }
1808
1870
  /**
@@ -2154,11 +2216,25 @@ class Text extends AbstractCanvasObject {
2154
2216
  getBulletMargin() {
2155
2217
  if (!this.isBulleted())
2156
2218
  return 0;
2157
- let fontSize = 0;
2158
- each(this.fabricObject._textLines, (textLine, lineIndex) => {
2159
- fontSize = Math.max(fontSize, this.fabricObject.getValueOfPropertyAt(lineIndex, 0, "fontSize"));
2160
- });
2161
- return fontSize * 0.6;
2219
+ try {
2220
+ let fontSize = 0;
2221
+ each(this.fabricObject._textLines, (textLine, lineIndex) => {
2222
+ const size = this.fabricObject.getValueOfPropertyAt(lineIndex, 0, "fontSize");
2223
+ // Sanity check fontSize
2224
+ if (isFinite(size) && size > 0 && size < 1000) {
2225
+ fontSize = Math.max(fontSize, size);
2226
+ }
2227
+ });
2228
+ // Additional safety check
2229
+ if (!isFinite(fontSize) || fontSize < 0 || fontSize > 1000) {
2230
+ return 0;
2231
+ }
2232
+ return fontSize * 0.6;
2233
+ }
2234
+ catch (e) {
2235
+ console.warn('Error calculating bullet margin:', e);
2236
+ return 0;
2237
+ }
2162
2238
  }
2163
2239
  /**
2164
2240
  * @override
@@ -2168,10 +2244,35 @@ class Text extends AbstractCanvasObject {
2168
2244
  if (!this.isOrderedList())
2169
2245
  return 0;
2170
2246
  const orderedListMarkerEntities = this.fabricObject._orderedListMarkerEntities;
2171
- let maxValue = max((orderedListMarkerEntities !== null && orderedListMarkerEntities !== void 0 ? orderedListMarkerEntities : [])
2172
- .filter((x) => !!x)
2173
- .map((x) => x.getBoundingRect().width));
2174
- return maxValue ? (maxValue * 1.2) : 0;
2247
+ try {
2248
+ const widths = (orderedListMarkerEntities !== null && orderedListMarkerEntities !== void 0 ? orderedListMarkerEntities : [])
2249
+ .filter((x) => !!x)
2250
+ .map((x) => {
2251
+ var _a;
2252
+ try {
2253
+ const rect = x.getBoundingRect();
2254
+ const width = (_a = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _a !== void 0 ? _a : 0;
2255
+ // Sanity check: if width is invalid, return 0
2256
+ if (!isFinite(width) || width < 0 || width > 1000) {
2257
+ return 0;
2258
+ }
2259
+ return width;
2260
+ }
2261
+ catch (e) {
2262
+ return 0;
2263
+ }
2264
+ });
2265
+ const maxValue = max(widths);
2266
+ // Additional safety: ensure maxValue is a valid number
2267
+ if (!maxValue || !isFinite(maxValue) || maxValue < 0 || maxValue > 1000) {
2268
+ return 0;
2269
+ }
2270
+ return maxValue * 1.2;
2271
+ }
2272
+ catch (e) {
2273
+ console.warn('Error calculating ordered list margin:', e);
2274
+ return 0;
2275
+ }
2175
2276
  }
2176
2277
  /**
2177
2278
  * @override
@@ -2412,6 +2513,7 @@ class Text extends AbstractCanvasObject {
2412
2513
  lockMovementY: true,
2413
2514
  hasControls: false,
2414
2515
  selectable: false,
2516
+ // evented: false, // Don't capture mouse events - allows clicks to pass through to textbox
2415
2517
  originX: "left",
2416
2518
  originY: "top"
2417
2519
  });
@@ -2506,7 +2608,7 @@ class Text extends AbstractCanvasObject {
2506
2608
  text: `${lineNumber}.`
2507
2609
  });
2508
2610
  }
2509
- if (method === "fillText" && this.fabricObject.left <= 3) {
2611
+ if (method === "fillText" && this.isOrderedList() && this.fabricObject.left <= 3) {
2510
2612
  left = left + this.getOrderedListMargin();
2511
2613
  }
2512
2614
  this.fabricObject.dirty = true;
@@ -2518,13 +2620,24 @@ class Text extends AbstractCanvasObject {
2518
2620
  * @inheritDoc
2519
2621
  */
2520
2622
  updateFabricObjectBehavior() {
2623
+ // DON'T call super.updateFabricObjectBehavior() for textboxes
2624
+ // because it locks movement which interferes with text editing in Fabric v7
2521
2625
  super.updateFabricObjectBehavior();
2522
2626
  const profile = this.canvas.getProfile();
2523
2627
  const allowManualPositioning = !this.canvas.getLayoutManager().isEnforcedPositioning;
2628
+ // For textboxes, we keep them moveable but hide all controls except drag control
2629
+ // This allows text editing to work properly while still supporting drag via the icon
2524
2630
  this.fabricObject.set({
2631
+ lockMovementX: false, // Allow dragging
2632
+ lockMovementY: false, // Allow dragging
2525
2633
  lockScalingY: true,
2526
2634
  lockScalingX: this.isPinToBottom(),
2527
- lockScalingFlip: true
2635
+ lockScalingFlip: true,
2636
+ lockRotation: true,
2637
+ selectable: true,
2638
+ editable: false, // Only enable on double-click
2639
+ hoverCursor: "move", // Show move cursor by default
2640
+ moveCursor: "move",
2528
2641
  });
2529
2642
  this.fabricObject.setControlsVisibility({
2530
2643
  tl: false,
@@ -2534,35 +2647,64 @@ class Text extends AbstractCanvasObject {
2534
2647
  ml: allowManualPositioning && profile.allowScale && !this.isPinToBottom(),
2535
2648
  mt: false,
2536
2649
  mr: allowManualPositioning && profile.allowScale && !this.isPinToBottom(),
2537
- mb: false
2650
+ mb: false,
2651
+ mtr: false,
2538
2652
  });
2539
2653
  this.redraw();
2540
2654
  }
2541
2655
  onTextChange() {
2542
2656
  this.data.userText = this.fabricObject.text;
2543
2657
  this.text$.next(this.fabricObject.text);
2544
- if (this.getBoundingBox().width > this.canvas.getDimensions().widthPx) {
2545
- this.fabricObject.set({ left: 0, width: this.canvas.getDimensions().widthPx });
2546
- this.redraw();
2658
+ // Only check width bounds if text is close to canvas edge (performance optimization)
2659
+ const currentLeft = this.fabricObject.left || 0;
2660
+ const canvasWidth = this.canvas.getDimensions().widthPx;
2661
+ // Skip expensive getBoundingBox if object is clearly within bounds
2662
+ if (currentLeft + (this.fabricObject.width || 0) > canvasWidth * 0.9) {
2663
+ if (this.getBoundingBox().width > canvasWidth) {
2664
+ this.fabricObject.set({ left: 0, width: canvasWidth });
2665
+ this.redraw();
2666
+ }
2547
2667
  }
2548
2668
  }
2549
2669
  onDelete() {
2550
2670
  this.removeBullets();
2551
2671
  }
2552
2672
  onSelectionChange() {
2553
- this.updateTextSelection();
2673
+ this.updateTextSelection(false);
2554
2674
  }
2555
2675
  hasTextSelection() {
2556
2676
  return this.canvas.getSelection().isText && this.textSelection$.value.length > 0;
2557
2677
  }
2558
- updateTextSelection() {
2678
+ updateTextSelection(notifyCanvas = true) {
2559
2679
  if (this.canvas.headless)
2560
2680
  return;
2561
2681
  this.textSelection$.next(new TextSelection(this.fabricObject, this.fontLibrary));
2562
- this.notifyCanvasOfTextChanges();
2682
+ if (notifyCanvas) {
2683
+ this.notifyCanvasOfTextChanges();
2684
+ }
2563
2685
  }
2564
2686
  notifyCanvasOfTextChanges() {
2565
- this.canvas.notifyTextChanged();
2687
+ if (this.notifyCanvasTimeout) {
2688
+ clearTimeout(this.notifyCanvasTimeout);
2689
+ }
2690
+ this.notifyCanvasTimeout = setTimeout(() => {
2691
+ this.canvas.notifyTextChanged();
2692
+ this.notifyCanvasTimeout = null;
2693
+ }, this.NOTIFY_DELAY_MS);
2694
+ }
2695
+ /**
2696
+ * @override
2697
+ * Provide a simpler bounding box for drag boundary checks that uses the textbox
2698
+ * dimensions directly instead of getBoundingRect(), which includes ordered list
2699
+ * markers that may extend far beyond the text and cause drag shaking issues.
2700
+ */
2701
+ getSimpleBoundingBox() {
2702
+ return {
2703
+ top: this.fabricObject.top || 0,
2704
+ left: this.fabricObject.left || 0,
2705
+ width: (this.fabricObject.width || 0) * (this.fabricObject.scaleX || 1),
2706
+ height: (this.fabricObject.height || 0) * (this.fabricObject.scaleY || 1)
2707
+ };
2566
2708
  }
2567
2709
  }
2568
2710
  Text.NEW_LINE = "\n";
@@ -2728,6 +2870,7 @@ class AbstractInteractiveCanvas {
2728
2870
  this.devicePixelRatio = devicePixelRatio;
2729
2871
  this.fontLibrary = new FontLibrary();
2730
2872
  this.dimensionChangeRejections$ = new Subject();
2873
+ this.redrawScheduled = false;
2731
2874
  this.IMAGE_CAPTURE_COEFFICIENT = this.calculateImageCaptureCoefficient();
2732
2875
  /**
2733
2876
  * Fabric defaults to only 2 decimal places when saving, which causes havoc on the scaling of large images.
@@ -2749,8 +2892,8 @@ class AbstractInteractiveCanvas {
2749
2892
  //Configure canvas behaviour
2750
2893
  this.fabricCanvas.stateful = true;
2751
2894
  this.fabricCanvas.preserveObjectStacking = true;
2752
- this.fabricCanvas.perPixelTargetFind = true;
2753
- this.fabricCanvas.targetFindTolerance = 4;
2895
+ this.fabricCanvas.perPixelTargetFind = false; // Disabled for better hover tolerance in Fabric v7
2896
+ this.fabricCanvas.targetFindTolerance = 0; // Increased for better hover detection in Fabric v7 (was 4)
2754
2897
  this.setBackgroundColor("#FFFFFF");
2755
2898
  }
2756
2899
  /**
@@ -3054,7 +3197,6 @@ class AbstractInteractiveCanvas {
3054
3197
  */
3055
3198
  addImageObject(imageUrl) {
3056
3199
  return __awaiter(this, void 0, void 0, function* () {
3057
- // Don't let the image be more than half the canvas in size.
3058
3200
  const maxSizeScale = 2;
3059
3201
  const maxWidth = this.dimensions$.value.widthPx / maxSizeScale;
3060
3202
  const options = { crossOrigin: "anonymous" };
@@ -3066,7 +3208,6 @@ class AbstractInteractiveCanvas {
3066
3208
  if (curWidth > maxWidth) {
3067
3209
  newScale = maxWidth / curWidth;
3068
3210
  }
3069
- // Set image properties and add to the canvas
3070
3211
  const fabricImage = new this.fabric.Image(image, {
3071
3212
  id: id,
3072
3213
  scaleX: newScale,
@@ -3086,7 +3227,6 @@ class AbstractInteractiveCanvas {
3086
3227
  return object;
3087
3228
  }
3088
3229
  else {
3089
- // Browser-side logic
3090
3230
  return yield new Promise((resolve) => {
3091
3231
  this.fabric.Image.fromURL(imageUrl, options).then((fabricImage) => {
3092
3232
  const id = this.randomId();
@@ -3138,15 +3278,14 @@ class AbstractInteractiveCanvas {
3138
3278
  fontFamily: profile.defaultFont,
3139
3279
  textAlign: profile.defaultTextAlignment,
3140
3280
  hoverCursor: "move",
3141
- moveCursor: 'move',
3281
+ moveCursor: "move",
3282
+ editable: false,
3283
+ editingBorderColor: '#2196F3',
3142
3284
  multiLine: true,
3143
3285
  lineHeight: profile.defaultLineHeight,
3144
3286
  charSpacing: profile.defaultCharacterSpacing,
3145
- perPixelTargetFind: false,
3146
- targetFindTolerance: 4,
3147
3287
  splitByGrapheme: true,
3148
- editable: false, // Only enable on double-click
3149
- padding: 0,
3288
+ padding: 0, // Increased from 0 for better hover detection in Fabric v7
3150
3289
  cornerSize: 8,
3151
3290
  touchCornerSize: 12
3152
3291
  };
@@ -3595,9 +3734,17 @@ class AbstractInteractiveCanvas {
3595
3734
  }
3596
3735
  /**
3597
3736
  * Redraw the canvas.
3737
+ * Uses requestAnimationFrame to batch multiple redraw calls into a single render.
3598
3738
  */
3599
3739
  redraw() {
3600
- this.fabricCanvas.requestRenderAll();
3740
+ if (this.redrawScheduled) {
3741
+ return;
3742
+ }
3743
+ this.redrawScheduled = true;
3744
+ requestAnimationFrame(() => {
3745
+ this.fabricCanvas.requestRenderAll();
3746
+ this.redrawScheduled = false;
3747
+ });
3601
3748
  }
3602
3749
  syncObjectList() {
3603
3750
  const newObjectList = _(this.fabricCanvas.getObjects())
@@ -3723,7 +3870,6 @@ class AbstractInteractiveCanvas {
3723
3870
  * Update the actual dimensions of the canvas by multiplying the requested dimensions by the current zoom factor.
3724
3871
  */
3725
3872
  updateCanvasDimensions(dims, zoom) {
3726
- //const oldWidth = this.fabricCanvas.getWidth();
3727
3873
  const width = dims.widthPx * zoom;
3728
3874
  const height = dims.heightPx * zoom;
3729
3875
  this.fabricCanvas.setZoom(zoom);
@@ -3731,39 +3877,6 @@ class AbstractInteractiveCanvas {
3731
3877
  width,
3732
3878
  height
3733
3879
  });
3734
- /*if (this.layoutManager$.value instanceof AdvancedLayoutManager) {
3735
- const scaleFactor = width / oldWidth;
3736
- //this.fabricCanvas.calcOffset();
3737
- const objects = this.fabricCanvas.getObjects();
3738
- objects.forEach((obj: any) => {
3739
- obj.set({ ₹
3740
- left: obj.left * scaleFactor,
3741
- });
3742
- if (obj.type === "textbox" || obj.type === "text") {
3743
- obj.set({
3744
- scaleX: 1,
3745
- scaleY: 1,
3746
- // scaleX: obj.scaleX * scaleFactor,
3747
- // scaleY: obj.scaleY * scaleFactor,
3748
- width: obj.width * scaleFactor,
3749
- });
3750
- // obj.initDimensions();
3751
- } else if (obj.type === "border" || obj.type === "rect") {
3752
- obj.set({
3753
- width: obj.width * scaleFactor,
3754
- scaleX: 1,
3755
- strokeUniform: true,
3756
- // strokeAlign: "inner",
3757
- });
3758
- } else {
3759
- obj.set({
3760
- scaleX: obj.scaleX * scaleFactor,
3761
- });
3762
- }
3763
- obj.setCoords();
3764
- });
3765
- this.fabricCanvas.requestRenderAll();
3766
- }*/
3767
3880
  }
3768
3881
  onProfileChanged(profile) {
3769
3882
  this.setMultiSelect(profile.allowMultiSelect);
@@ -3918,7 +4031,6 @@ class InteractiveCanvas extends AbstractInteractiveCanvas {
3918
4031
  this.fabricCanvas.on("selection:created", (event) => this.onFabricSelectionChanged(event));
3919
4032
  this.fabricCanvas.on("selection:cleared", (event) => this.onFabricSelectionChanged(event));
3920
4033
  this.fabricCanvas.on("text:editing:entered", () => this.onTextEditing());
3921
- this.fabricCanvas.on("text:editing:exited", () => this.onTextEditing());
3922
4034
  this.fabricCanvas.on("text:editing:exited", (event) => {
3923
4035
  this.onTextEditing();
3924
4036
  const target = event.target;
@@ -3927,8 +4039,6 @@ class InteractiveCanvas extends AbstractInteractiveCanvas {
3927
4039
  }
3928
4040
  });
3929
4041
  this.fabricCanvas.on("text:changed", () => this.onTextChange());
3930
- // Handle double-click on textbox to enter editing mode
3931
- // @ts-ignore
3932
4042
  this.fabricCanvas.on("mouse:dblclick", (event) => this.onDoubleClick(event));
3933
4043
  }
3934
4044
  /**
@@ -3963,29 +4073,20 @@ class InteractiveCanvas extends AbstractInteractiveCanvas {
3963
4073
  selection.setEvent(event);
3964
4074
  }
3965
4075
  this.selection$.next(selection);
3966
- this.enterTextSelectionModeIfRequired();
3967
- }
3968
- enterTextSelectionModeIfRequired() {
3969
- var _a, _b;
3970
- if (!this.profile$.value.enterTextEditImmediatelyOnSelect && !this.layoutManager$.value.enterTextEditImmediatelyOnSelect)
3971
- return;
3972
- if (((_b = (_a = this.selection$.value) === null || _a === void 0 ? void 0 : _a.objects[0]) === null || _b === void 0 ? void 0 : _b.getType()) !== ObjectType.TEXT)
3973
- return;
3974
- const text = this.selection$.value.objects[0];
3975
- text.enterEditing();
3976
4076
  }
3977
4077
  onTextEditing() {
3978
4078
  const selected = this.getFieldsByCanvasObject(this.fabricCanvas.getActiveObjects());
3979
4079
  this.selection$.next(new SelectionData(selected));
3980
4080
  }
3981
4081
  onDoubleClick(event) {
4082
+ var _a, _b;
3982
4083
  const target = event.target;
3983
4084
  if (target && (target.type === 'textbox' || target.type === 'i-text')) {
3984
- if (!target.isEditing) {
4085
+ const targetIText = target;
4086
+ if (!targetIText.isEditing) {
3985
4087
  target.set({ editable: true, hoverCursor: 'text' });
3986
- // target.enterEditing();
3987
- // target.hiddenTextarea?.focus();
3988
- this.enterTextSelectionModeIfRequired();
4088
+ (_a = targetIText.enterEditing) === null || _a === void 0 ? void 0 : _a.call(targetIText);
4089
+ (_b = targetIText.hiddenTextarea) === null || _b === void 0 ? void 0 : _b.focus();
3989
4090
  }
3990
4091
  }
3991
4092
  }
@@ -253,9 +253,15 @@ class ResizeCanvasLayoutFunction {
253
253
  const targetHeight = increase.withSize(increase.width, baseDimensions.height + increase.height);
254
254
  if (currentDimensions.height !== targetHeight.height) {
255
255
  canvas.setDimensions(targetHeight);
256
+ canvas.getObjectsInternal().forEach((object) => {
257
+ var _a;
258
+ const canvasObj = canvas.getObjectsByTypeInternal(object.type).find(o => o.getId() === object.getId());
259
+ if (canvasObj && (((_a = canvasObj.isPinToBottom) === null || _a === void 0 ? void 0 : _a.call(canvasObj)) || object.top > currentDimensions.heightPx * 0.5)) {
260
+ object.setCoords();
261
+ }
262
+ });
263
+ canvas.redraw();
256
264
  }
257
- canvas.getObjectsInternal().forEach((object) => object.setCoords());
258
- canvas.redraw();
259
265
  }
260
266
  }
261
267
 
@@ -285,7 +291,6 @@ class StackObjectsLayoutFunction {
285
291
  const footerObjects = objects[0];
286
292
  const bodyObjects = objects[1];
287
293
  const bodyWidth = canvasDimensions.widthPx - canvasMargin.right - canvasMargin.left;
288
- canvas.redraw();
289
294
  //Make one pass and set the widths of all elements. This will force objects with dynamic heights (like text) to take on their necessary height before trying to lay them out.
290
295
  this.applyWidths(allObjects, bodyWidth);
291
296
  const bodyRows = this.getRows(bodyObjects);
@@ -372,7 +377,11 @@ class StackObjectsLayoutFunction {
372
377
  object.setPosition({
373
378
  width: actualWidth
374
379
  });
375
- object.updateDimensions();
380
+ });
381
+ _(objects).forEach((object) => {
382
+ if (object.getType() !== exports.ObjectType.VERTICAL_RULE) {
383
+ object.updateDimensions();
384
+ }
376
385
  });
377
386
  }
378
387
  adjustCanvasSize(canvas, requiredHeight) {
@@ -458,15 +467,22 @@ class AdvancedLayoutManager extends AbstractLayoutManager {
458
467
  this.repositionBackgrounds = new RepositionBackgroundsLayoutFunction();
459
468
  this.resizeCanvas = new ResizeCanvasLayoutFunction();
460
469
  this.stackObjects = new StackObjectsLayoutFunction();
470
+ this.dimensionsLayoutScheduled = false;
461
471
  }
462
472
  onCanvasDimensionsChange() {
463
- this.pinToBottom.execute(this.canvas);
464
- // this.stackObjects.execute(this.canvas);
465
- this.dimensionChange();
466
- this.repositionCutoffs.execute(this.canvas);
467
- this.repositionBorders.execute(this.canvas);
468
- this.repositionBackgrounds.execute(this.canvas);
469
- this.redrawAll.execute(this.canvas);
473
+ if (this.dimensionsLayoutScheduled) {
474
+ return;
475
+ }
476
+ this.dimensionsLayoutScheduled = true;
477
+ Promise.resolve().then(() => {
478
+ this.pinToBottom.execute(this.canvas);
479
+ this.dimensionChange();
480
+ this.repositionCutoffs.execute(this.canvas);
481
+ this.repositionBorders.execute(this.canvas);
482
+ this.repositionBackgrounds.execute(this.canvas);
483
+ this.redrawAll.execute(this.canvas);
484
+ this.dimensionsLayoutScheduled = false;
485
+ });
470
486
  }
471
487
  onTextChange() {
472
488
  this.resizeCanvas.execute(this.canvas);
@@ -505,6 +521,7 @@ class StandardLayoutManager extends AbstractLayoutManager {
505
521
  this.repositionBackgrounds = new RepositionBackgroundsLayoutFunction();
506
522
  this.stackObjects = new StackObjectsLayoutFunction();
507
523
  this.updateObjectDimensions = new UpdateObjectDimensionsLayoutFunction();
524
+ this.layoutBatchScheduled = false;
508
525
  }
509
526
  /**
510
527
  * @override
@@ -546,12 +563,19 @@ class StandardLayoutManager extends AbstractLayoutManager {
546
563
  this.stackAndRedraw();
547
564
  }
548
565
  stackAndRedraw() {
549
- this.stackObjects.execute(this.canvas);
550
- this.repositionCutoffs.execute(this.canvas);
551
- this.repositionBorders.execute(this.canvas);
552
- this.repositionBackgrounds.execute(this.canvas);
553
- this.updateObjectDimensions.execute(this.canvas);
554
- this.redrawAll.execute(this.canvas);
566
+ if (this.layoutBatchScheduled) {
567
+ return;
568
+ }
569
+ this.layoutBatchScheduled = true;
570
+ Promise.resolve().then(() => {
571
+ this.stackObjects.execute(this.canvas);
572
+ this.repositionCutoffs.execute(this.canvas);
573
+ this.repositionBorders.execute(this.canvas);
574
+ this.repositionBackgrounds.execute(this.canvas);
575
+ this.updateObjectDimensions.execute(this.canvas);
576
+ this.redrawAll.execute(this.canvas);
577
+ this.layoutBatchScheduled = false;
578
+ });
555
579
  }
556
580
  }
557
581
 
@@ -1018,7 +1042,7 @@ class AbstractCanvasObject {
1018
1042
  //no-op
1019
1043
  }
1020
1044
  preventLeavingCanvas() {
1021
- const overflow = this.getCanvasOverflow();
1045
+ const overflow = this.getCanvasOverflowForBoundaryPrevention();
1022
1046
  if (overflow.hasOverflow) {
1023
1047
  const adjustX = overflow.left || -overflow.right;
1024
1048
  const adjustY = overflow.top || -overflow.bottom;
@@ -1044,6 +1068,29 @@ class AbstractCanvasObject {
1044
1068
  hasOverflow: top > 0 || right > 0 || bottom > 0 || left > 0
1045
1069
  };
1046
1070
  }
1071
+ getCanvasOverflowForBoundaryPrevention() {
1072
+ // Use a simpler bounding box calculation for boundary prevention
1073
+ // to avoid issues with decorative elements (like ordered list markers)
1074
+ // extending beyond the main object bounds
1075
+ const bound = this.getSimpleBoundingBox();
1076
+ const canvasDimensions = this.canvas.getDimensions();
1077
+ const top = Math.max(0, 0 - bound.top);
1078
+ const right = Math.max(0, bound.left + bound.width - canvasDimensions.widthPx);
1079
+ const bottom = Math.max(0, bound.top + bound.height - canvasDimensions.heightPx);
1080
+ const left = Math.max(0, 0 - bound.left);
1081
+ return {
1082
+ top: top,
1083
+ right: right,
1084
+ bottom: bottom,
1085
+ left: left,
1086
+ hasOverflow: top > 0 || right > 0 || bottom > 0 || left > 0
1087
+ };
1088
+ }
1089
+ getSimpleBoundingBox() {
1090
+ // Default implementation: use normal bounding box
1091
+ // Subclasses can override this to provide simpler bounds for drag boundary checks
1092
+ return this.getBoundingBox();
1093
+ }
1047
1094
  centerHorizontallyInternal() {
1048
1095
  const canvasWidth = this.canvas.getDimensions().widthPx;
1049
1096
  const objectWidth = this.getDimensions().width.px;
@@ -1091,7 +1138,8 @@ class AbstractCanvasObject {
1091
1138
  mt: options.allowScale,
1092
1139
  mr: options.allowScale,
1093
1140
  mb: options.allowScale,
1094
- mtr: options.allowRotate
1141
+ mtr: options.allowRotate,
1142
+ // dragControl: options.allowMove // Show drag control when movement is allowed
1095
1143
  });
1096
1144
  }
1097
1145
  }
@@ -1815,15 +1863,29 @@ class RichText extends Image {
1815
1863
  class Text extends AbstractCanvasObject {
1816
1864
  constructor(canvas, fabricObject, persistedField) {
1817
1865
  super(exports.ObjectType.TEXT, canvas, fabricObject, persistedField);
1866
+ this.notifyCanvasTimeout = null;
1867
+ this.NOTIFY_DELAY_MS = 150; // Debounce canvas notifications
1818
1868
  this.canvas = canvas;
1819
1869
  this.fontLibrary = canvas.getFontLibrary();
1820
1870
  this.baseComponentHeight = this.fabricObject.height;
1821
1871
  this.text$ = new rxjs.BehaviorSubject(this.fabricObject.text);
1822
1872
  this.data.userText = this.fabricObject.text;
1823
1873
  this.textSelection$ = new rxjs.BehaviorSubject(new TextSelection(this.fabricObject, this.fontLibrary));
1824
- this.fabricObject.set({ strokeWidth: 0 });
1874
+ this.fabricObject.set({
1875
+ strokeWidth: 0,
1876
+ perPixelTargetFind: false, // Disable per-pixel detection for this object
1877
+ padding: 0 // Add padding for larger hover area (Fabric v7 compatibility)
1878
+ });
1825
1879
  this.fabricObject.on("changed", () => this.onTextChange());
1826
1880
  this.fabricObject.on("selection:changed", () => this.onSelectionChange());
1881
+ // Note: Removed containsPoint override to allow Fabric's default bounding box
1882
+ // detection with padding to work properly for hover detection
1883
+ this.fabricObject.on("mousedown", (e) => {
1884
+ var _a;
1885
+ if (!this.fabricObject.isEditing) {
1886
+ (_a = e.e) === null || _a === void 0 ? void 0 : _a.preventDefault();
1887
+ }
1888
+ });
1827
1889
  this.applyBulletListMonkeyPatches();
1828
1890
  }
1829
1891
  /**
@@ -2175,11 +2237,25 @@ class Text extends AbstractCanvasObject {
2175
2237
  getBulletMargin() {
2176
2238
  if (!this.isBulleted())
2177
2239
  return 0;
2178
- let fontSize = 0;
2179
- _.each(this.fabricObject._textLines, (textLine, lineIndex) => {
2180
- fontSize = Math.max(fontSize, this.fabricObject.getValueOfPropertyAt(lineIndex, 0, "fontSize"));
2181
- });
2182
- return fontSize * 0.6;
2240
+ try {
2241
+ let fontSize = 0;
2242
+ _.each(this.fabricObject._textLines, (textLine, lineIndex) => {
2243
+ const size = this.fabricObject.getValueOfPropertyAt(lineIndex, 0, "fontSize");
2244
+ // Sanity check fontSize
2245
+ if (isFinite(size) && size > 0 && size < 1000) {
2246
+ fontSize = Math.max(fontSize, size);
2247
+ }
2248
+ });
2249
+ // Additional safety check
2250
+ if (!isFinite(fontSize) || fontSize < 0 || fontSize > 1000) {
2251
+ return 0;
2252
+ }
2253
+ return fontSize * 0.6;
2254
+ }
2255
+ catch (e) {
2256
+ console.warn('Error calculating bullet margin:', e);
2257
+ return 0;
2258
+ }
2183
2259
  }
2184
2260
  /**
2185
2261
  * @override
@@ -2189,10 +2265,35 @@ class Text extends AbstractCanvasObject {
2189
2265
  if (!this.isOrderedList())
2190
2266
  return 0;
2191
2267
  const orderedListMarkerEntities = this.fabricObject._orderedListMarkerEntities;
2192
- let maxValue = _.max((orderedListMarkerEntities !== null && orderedListMarkerEntities !== void 0 ? orderedListMarkerEntities : [])
2193
- .filter((x) => !!x)
2194
- .map((x) => x.getBoundingRect().width));
2195
- return maxValue ? (maxValue * 1.2) : 0;
2268
+ try {
2269
+ const widths = (orderedListMarkerEntities !== null && orderedListMarkerEntities !== void 0 ? orderedListMarkerEntities : [])
2270
+ .filter((x) => !!x)
2271
+ .map((x) => {
2272
+ var _a;
2273
+ try {
2274
+ const rect = x.getBoundingRect();
2275
+ const width = (_a = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _a !== void 0 ? _a : 0;
2276
+ // Sanity check: if width is invalid, return 0
2277
+ if (!isFinite(width) || width < 0 || width > 1000) {
2278
+ return 0;
2279
+ }
2280
+ return width;
2281
+ }
2282
+ catch (e) {
2283
+ return 0;
2284
+ }
2285
+ });
2286
+ const maxValue = _.max(widths);
2287
+ // Additional safety: ensure maxValue is a valid number
2288
+ if (!maxValue || !isFinite(maxValue) || maxValue < 0 || maxValue > 1000) {
2289
+ return 0;
2290
+ }
2291
+ return maxValue * 1.2;
2292
+ }
2293
+ catch (e) {
2294
+ console.warn('Error calculating ordered list margin:', e);
2295
+ return 0;
2296
+ }
2196
2297
  }
2197
2298
  /**
2198
2299
  * @override
@@ -2433,6 +2534,7 @@ class Text extends AbstractCanvasObject {
2433
2534
  lockMovementY: true,
2434
2535
  hasControls: false,
2435
2536
  selectable: false,
2537
+ // evented: false, // Don't capture mouse events - allows clicks to pass through to textbox
2436
2538
  originX: "left",
2437
2539
  originY: "top"
2438
2540
  });
@@ -2527,7 +2629,7 @@ class Text extends AbstractCanvasObject {
2527
2629
  text: `${lineNumber}.`
2528
2630
  });
2529
2631
  }
2530
- if (method === "fillText" && this.fabricObject.left <= 3) {
2632
+ if (method === "fillText" && this.isOrderedList() && this.fabricObject.left <= 3) {
2531
2633
  left = left + this.getOrderedListMargin();
2532
2634
  }
2533
2635
  this.fabricObject.dirty = true;
@@ -2539,13 +2641,24 @@ class Text extends AbstractCanvasObject {
2539
2641
  * @inheritDoc
2540
2642
  */
2541
2643
  updateFabricObjectBehavior() {
2644
+ // DON'T call super.updateFabricObjectBehavior() for textboxes
2645
+ // because it locks movement which interferes with text editing in Fabric v7
2542
2646
  super.updateFabricObjectBehavior();
2543
2647
  const profile = this.canvas.getProfile();
2544
2648
  const allowManualPositioning = !this.canvas.getLayoutManager().isEnforcedPositioning;
2649
+ // For textboxes, we keep them moveable but hide all controls except drag control
2650
+ // This allows text editing to work properly while still supporting drag via the icon
2545
2651
  this.fabricObject.set({
2652
+ lockMovementX: false, // Allow dragging
2653
+ lockMovementY: false, // Allow dragging
2546
2654
  lockScalingY: true,
2547
2655
  lockScalingX: this.isPinToBottom(),
2548
- lockScalingFlip: true
2656
+ lockScalingFlip: true,
2657
+ lockRotation: true,
2658
+ selectable: true,
2659
+ editable: false, // Only enable on double-click
2660
+ hoverCursor: "move", // Show move cursor by default
2661
+ moveCursor: "move",
2549
2662
  });
2550
2663
  this.fabricObject.setControlsVisibility({
2551
2664
  tl: false,
@@ -2555,35 +2668,64 @@ class Text extends AbstractCanvasObject {
2555
2668
  ml: allowManualPositioning && profile.allowScale && !this.isPinToBottom(),
2556
2669
  mt: false,
2557
2670
  mr: allowManualPositioning && profile.allowScale && !this.isPinToBottom(),
2558
- mb: false
2671
+ mb: false,
2672
+ mtr: false,
2559
2673
  });
2560
2674
  this.redraw();
2561
2675
  }
2562
2676
  onTextChange() {
2563
2677
  this.data.userText = this.fabricObject.text;
2564
2678
  this.text$.next(this.fabricObject.text);
2565
- if (this.getBoundingBox().width > this.canvas.getDimensions().widthPx) {
2566
- this.fabricObject.set({ left: 0, width: this.canvas.getDimensions().widthPx });
2567
- this.redraw();
2679
+ // Only check width bounds if text is close to canvas edge (performance optimization)
2680
+ const currentLeft = this.fabricObject.left || 0;
2681
+ const canvasWidth = this.canvas.getDimensions().widthPx;
2682
+ // Skip expensive getBoundingBox if object is clearly within bounds
2683
+ if (currentLeft + (this.fabricObject.width || 0) > canvasWidth * 0.9) {
2684
+ if (this.getBoundingBox().width > canvasWidth) {
2685
+ this.fabricObject.set({ left: 0, width: canvasWidth });
2686
+ this.redraw();
2687
+ }
2568
2688
  }
2569
2689
  }
2570
2690
  onDelete() {
2571
2691
  this.removeBullets();
2572
2692
  }
2573
2693
  onSelectionChange() {
2574
- this.updateTextSelection();
2694
+ this.updateTextSelection(false);
2575
2695
  }
2576
2696
  hasTextSelection() {
2577
2697
  return this.canvas.getSelection().isText && this.textSelection$.value.length > 0;
2578
2698
  }
2579
- updateTextSelection() {
2699
+ updateTextSelection(notifyCanvas = true) {
2580
2700
  if (this.canvas.headless)
2581
2701
  return;
2582
2702
  this.textSelection$.next(new TextSelection(this.fabricObject, this.fontLibrary));
2583
- this.notifyCanvasOfTextChanges();
2703
+ if (notifyCanvas) {
2704
+ this.notifyCanvasOfTextChanges();
2705
+ }
2584
2706
  }
2585
2707
  notifyCanvasOfTextChanges() {
2586
- this.canvas.notifyTextChanged();
2708
+ if (this.notifyCanvasTimeout) {
2709
+ clearTimeout(this.notifyCanvasTimeout);
2710
+ }
2711
+ this.notifyCanvasTimeout = setTimeout(() => {
2712
+ this.canvas.notifyTextChanged();
2713
+ this.notifyCanvasTimeout = null;
2714
+ }, this.NOTIFY_DELAY_MS);
2715
+ }
2716
+ /**
2717
+ * @override
2718
+ * Provide a simpler bounding box for drag boundary checks that uses the textbox
2719
+ * dimensions directly instead of getBoundingRect(), which includes ordered list
2720
+ * markers that may extend far beyond the text and cause drag shaking issues.
2721
+ */
2722
+ getSimpleBoundingBox() {
2723
+ return {
2724
+ top: this.fabricObject.top || 0,
2725
+ left: this.fabricObject.left || 0,
2726
+ width: (this.fabricObject.width || 0) * (this.fabricObject.scaleX || 1),
2727
+ height: (this.fabricObject.height || 0) * (this.fabricObject.scaleY || 1)
2728
+ };
2587
2729
  }
2588
2730
  }
2589
2731
  Text.NEW_LINE = "\n";
@@ -2749,6 +2891,7 @@ class AbstractInteractiveCanvas {
2749
2891
  this.devicePixelRatio = devicePixelRatio;
2750
2892
  this.fontLibrary = new FontLibrary();
2751
2893
  this.dimensionChangeRejections$ = new rxjs.Subject();
2894
+ this.redrawScheduled = false;
2752
2895
  this.IMAGE_CAPTURE_COEFFICIENT = this.calculateImageCaptureCoefficient();
2753
2896
  /**
2754
2897
  * Fabric defaults to only 2 decimal places when saving, which causes havoc on the scaling of large images.
@@ -2770,8 +2913,8 @@ class AbstractInteractiveCanvas {
2770
2913
  //Configure canvas behaviour
2771
2914
  this.fabricCanvas.stateful = true;
2772
2915
  this.fabricCanvas.preserveObjectStacking = true;
2773
- this.fabricCanvas.perPixelTargetFind = true;
2774
- this.fabricCanvas.targetFindTolerance = 4;
2916
+ this.fabricCanvas.perPixelTargetFind = false; // Disabled for better hover tolerance in Fabric v7
2917
+ this.fabricCanvas.targetFindTolerance = 0; // Increased for better hover detection in Fabric v7 (was 4)
2775
2918
  this.setBackgroundColor("#FFFFFF");
2776
2919
  }
2777
2920
  /**
@@ -3075,7 +3218,6 @@ class AbstractInteractiveCanvas {
3075
3218
  */
3076
3219
  addImageObject(imageUrl) {
3077
3220
  return __awaiter(this, void 0, void 0, function* () {
3078
- // Don't let the image be more than half the canvas in size.
3079
3221
  const maxSizeScale = 2;
3080
3222
  const maxWidth = this.dimensions$.value.widthPx / maxSizeScale;
3081
3223
  const options = { crossOrigin: "anonymous" };
@@ -3087,7 +3229,6 @@ class AbstractInteractiveCanvas {
3087
3229
  if (curWidth > maxWidth) {
3088
3230
  newScale = maxWidth / curWidth;
3089
3231
  }
3090
- // Set image properties and add to the canvas
3091
3232
  const fabricImage = new this.fabric.Image(image, {
3092
3233
  id: id,
3093
3234
  scaleX: newScale,
@@ -3107,7 +3248,6 @@ class AbstractInteractiveCanvas {
3107
3248
  return object;
3108
3249
  }
3109
3250
  else {
3110
- // Browser-side logic
3111
3251
  return yield new Promise((resolve) => {
3112
3252
  this.fabric.Image.fromURL(imageUrl, options).then((fabricImage) => {
3113
3253
  const id = this.randomId();
@@ -3159,15 +3299,14 @@ class AbstractInteractiveCanvas {
3159
3299
  fontFamily: profile.defaultFont,
3160
3300
  textAlign: profile.defaultTextAlignment,
3161
3301
  hoverCursor: "move",
3162
- moveCursor: 'move',
3302
+ moveCursor: "move",
3303
+ editable: false,
3304
+ editingBorderColor: '#2196F3',
3163
3305
  multiLine: true,
3164
3306
  lineHeight: profile.defaultLineHeight,
3165
3307
  charSpacing: profile.defaultCharacterSpacing,
3166
- perPixelTargetFind: false,
3167
- targetFindTolerance: 4,
3168
3308
  splitByGrapheme: true,
3169
- editable: false, // Only enable on double-click
3170
- padding: 0,
3309
+ padding: 0, // Increased from 0 for better hover detection in Fabric v7
3171
3310
  cornerSize: 8,
3172
3311
  touchCornerSize: 12
3173
3312
  };
@@ -3616,9 +3755,17 @@ class AbstractInteractiveCanvas {
3616
3755
  }
3617
3756
  /**
3618
3757
  * Redraw the canvas.
3758
+ * Uses requestAnimationFrame to batch multiple redraw calls into a single render.
3619
3759
  */
3620
3760
  redraw() {
3621
- this.fabricCanvas.requestRenderAll();
3761
+ if (this.redrawScheduled) {
3762
+ return;
3763
+ }
3764
+ this.redrawScheduled = true;
3765
+ requestAnimationFrame(() => {
3766
+ this.fabricCanvas.requestRenderAll();
3767
+ this.redrawScheduled = false;
3768
+ });
3622
3769
  }
3623
3770
  syncObjectList() {
3624
3771
  const newObjectList = _(this.fabricCanvas.getObjects())
@@ -3744,7 +3891,6 @@ class AbstractInteractiveCanvas {
3744
3891
  * Update the actual dimensions of the canvas by multiplying the requested dimensions by the current zoom factor.
3745
3892
  */
3746
3893
  updateCanvasDimensions(dims, zoom) {
3747
- //const oldWidth = this.fabricCanvas.getWidth();
3748
3894
  const width = dims.widthPx * zoom;
3749
3895
  const height = dims.heightPx * zoom;
3750
3896
  this.fabricCanvas.setZoom(zoom);
@@ -3752,39 +3898,6 @@ class AbstractInteractiveCanvas {
3752
3898
  width,
3753
3899
  height
3754
3900
  });
3755
- /*if (this.layoutManager$.value instanceof AdvancedLayoutManager) {
3756
- const scaleFactor = width / oldWidth;
3757
- //this.fabricCanvas.calcOffset();
3758
- const objects = this.fabricCanvas.getObjects();
3759
- objects.forEach((obj: any) => {
3760
- obj.set({ ₹
3761
- left: obj.left * scaleFactor,
3762
- });
3763
- if (obj.type === "textbox" || obj.type === "text") {
3764
- obj.set({
3765
- scaleX: 1,
3766
- scaleY: 1,
3767
- // scaleX: obj.scaleX * scaleFactor,
3768
- // scaleY: obj.scaleY * scaleFactor,
3769
- width: obj.width * scaleFactor,
3770
- });
3771
- // obj.initDimensions();
3772
- } else if (obj.type === "border" || obj.type === "rect") {
3773
- obj.set({
3774
- width: obj.width * scaleFactor,
3775
- scaleX: 1,
3776
- strokeUniform: true,
3777
- // strokeAlign: "inner",
3778
- });
3779
- } else {
3780
- obj.set({
3781
- scaleX: obj.scaleX * scaleFactor,
3782
- });
3783
- }
3784
- obj.setCoords();
3785
- });
3786
- this.fabricCanvas.requestRenderAll();
3787
- }*/
3788
3901
  }
3789
3902
  onProfileChanged(profile) {
3790
3903
  this.setMultiSelect(profile.allowMultiSelect);
@@ -3939,7 +4052,6 @@ class InteractiveCanvas extends AbstractInteractiveCanvas {
3939
4052
  this.fabricCanvas.on("selection:created", (event) => this.onFabricSelectionChanged(event));
3940
4053
  this.fabricCanvas.on("selection:cleared", (event) => this.onFabricSelectionChanged(event));
3941
4054
  this.fabricCanvas.on("text:editing:entered", () => this.onTextEditing());
3942
- this.fabricCanvas.on("text:editing:exited", () => this.onTextEditing());
3943
4055
  this.fabricCanvas.on("text:editing:exited", (event) => {
3944
4056
  this.onTextEditing();
3945
4057
  const target = event.target;
@@ -3948,8 +4060,6 @@ class InteractiveCanvas extends AbstractInteractiveCanvas {
3948
4060
  }
3949
4061
  });
3950
4062
  this.fabricCanvas.on("text:changed", () => this.onTextChange());
3951
- // Handle double-click on textbox to enter editing mode
3952
- // @ts-ignore
3953
4063
  this.fabricCanvas.on("mouse:dblclick", (event) => this.onDoubleClick(event));
3954
4064
  }
3955
4065
  /**
@@ -3984,29 +4094,20 @@ class InteractiveCanvas extends AbstractInteractiveCanvas {
3984
4094
  selection.setEvent(event);
3985
4095
  }
3986
4096
  this.selection$.next(selection);
3987
- this.enterTextSelectionModeIfRequired();
3988
- }
3989
- enterTextSelectionModeIfRequired() {
3990
- var _a, _b;
3991
- if (!this.profile$.value.enterTextEditImmediatelyOnSelect && !this.layoutManager$.value.enterTextEditImmediatelyOnSelect)
3992
- return;
3993
- if (((_b = (_a = this.selection$.value) === null || _a === void 0 ? void 0 : _a.objects[0]) === null || _b === void 0 ? void 0 : _b.getType()) !== exports.ObjectType.TEXT)
3994
- return;
3995
- const text = this.selection$.value.objects[0];
3996
- text.enterEditing();
3997
4097
  }
3998
4098
  onTextEditing() {
3999
4099
  const selected = this.getFieldsByCanvasObject(this.fabricCanvas.getActiveObjects());
4000
4100
  this.selection$.next(new SelectionData(selected));
4001
4101
  }
4002
4102
  onDoubleClick(event) {
4103
+ var _a, _b;
4003
4104
  const target = event.target;
4004
4105
  if (target && (target.type === 'textbox' || target.type === 'i-text')) {
4005
- if (!target.isEditing) {
4106
+ const targetIText = target;
4107
+ if (!targetIText.isEditing) {
4006
4108
  target.set({ editable: true, hoverCursor: 'text' });
4007
- // target.enterEditing();
4008
- // target.hiddenTextarea?.focus();
4009
- this.enterTextSelectionModeIfRequired();
4109
+ (_a = targetIText.enterEditing) === null || _a === void 0 ? void 0 : _a.call(targetIText);
4110
+ (_b = targetIText.hiddenTextarea) === null || _b === void 0 ? void 0 : _b.focus();
4010
4111
  }
4011
4112
  }
4012
4113
  }
@@ -10,6 +10,7 @@ export declare class AdvancedLayoutManager extends AbstractLayoutManager {
10
10
  private repositionBackgrounds;
11
11
  private resizeCanvas;
12
12
  private stackObjects;
13
+ private dimensionsLayoutScheduled;
13
14
  constructor(canvas: IInteractiveCanvas);
14
15
  onCanvasDimensionsChange(): void;
15
16
  onTextChange(): void;
@@ -36,5 +36,6 @@ export declare class StandardLayoutManager extends AbstractLayoutManager {
36
36
  * @override
37
37
  */
38
38
  onMarginChange(): void;
39
+ private layoutBatchScheduled;
39
40
  private stackAndRedraw;
40
41
  }
@@ -205,6 +205,13 @@ export declare abstract class AbstractCanvasObject<T extends FabricObject> imple
205
205
  protected onDelete(): void;
206
206
  protected preventLeavingCanvas(): void;
207
207
  protected getCanvasOverflow(): IOverflow;
208
+ protected getCanvasOverflowForBoundaryPrevention(): IOverflow;
209
+ protected getSimpleBoundingBox(): {
210
+ top: number;
211
+ left: number;
212
+ width: number;
213
+ height: number;
214
+ };
208
215
  protected centerHorizontallyInternal(): void;
209
216
  protected centerVerticallyInternal(): void;
210
217
  protected nudgeInternal(x?: number, y?: number): void;
@@ -63,6 +63,8 @@ export declare class Text extends AbstractCanvasObject<ExtendedFabricTextbox> im
63
63
  private readonly text$;
64
64
  private readonly textSelection$;
65
65
  static readonly NEW_LINE: string;
66
+ private notifyCanvasTimeout;
67
+ private readonly NOTIFY_DELAY_MS;
66
68
  constructor(canvas: IInteractiveCanvas, fabricObject: fabric.Textbox, persistedField?: CanvasObjectData);
67
69
  /**
68
70
  * @override
@@ -321,4 +323,16 @@ export declare class Text extends AbstractCanvasObject<ExtendedFabricTextbox> im
321
323
  private hasTextSelection;
322
324
  private updateTextSelection;
323
325
  private notifyCanvasOfTextChanges;
326
+ /**
327
+ * @override
328
+ * Provide a simpler bounding box for drag boundary checks that uses the textbox
329
+ * dimensions directly instead of getBoundingRect(), which includes ordered list
330
+ * markers that may extend far beyond the text and cause drag shaking issues.
331
+ */
332
+ protected getSimpleBoundingBox(): {
333
+ top: number;
334
+ left: number;
335
+ width: number;
336
+ height: number;
337
+ };
324
338
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leapfrog/interactive-canvas",
3
- "version": "2.0.25-beta-14",
3
+ "version": "2.0.25-beta-15",
4
4
  "description": "Leapfrog interactive canvas",
5
5
  "files": [
6
6
  "dist"