@leapfrog/interactive-canvas 2.0.25-beta-8 → 2.0.25-beta-10

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
 
@@ -262,13 +268,21 @@ class AdvancedLayoutManager extends AbstractLayoutManager {
262
268
  this.repositionCutoffs = new RepositionCutoffsLayoutFunction();
263
269
  this.repositionBackgrounds = new RepositionBackgroundsLayoutFunction();
264
270
  this.resizeCanvas = new ResizeCanvasLayoutFunction();
271
+ this.dimensionsLayoutScheduled = false;
265
272
  }
266
273
  onCanvasDimensionsChange() {
267
- this.pinToBottom.execute(this.canvas);
268
- this.repositionCutoffs.execute(this.canvas);
269
- this.repositionBorders.execute(this.canvas);
270
- this.repositionBackgrounds.execute(this.canvas);
271
- this.redrawAll.execute(this.canvas);
274
+ if (this.dimensionsLayoutScheduled) {
275
+ return;
276
+ }
277
+ this.dimensionsLayoutScheduled = true;
278
+ Promise.resolve().then(() => {
279
+ this.pinToBottom.execute(this.canvas);
280
+ this.repositionCutoffs.execute(this.canvas);
281
+ this.repositionBorders.execute(this.canvas);
282
+ this.repositionBackgrounds.execute(this.canvas);
283
+ this.redrawAll.execute(this.canvas);
284
+ this.dimensionsLayoutScheduled = false;
285
+ });
272
286
  }
273
287
  onTextChange() {
274
288
  this.resizeCanvas.execute(this.canvas);
@@ -296,7 +310,6 @@ class StackObjectsLayoutFunction {
296
310
  const footerObjects = objects[0];
297
311
  const bodyObjects = objects[1];
298
312
  const bodyWidth = canvasDimensions.widthPx - canvasMargin.right - canvasMargin.left;
299
- canvas.redraw();
300
313
  //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.
301
314
  this.applyWidths(allObjects, bodyWidth);
302
315
  const bodyRows = this.getRows(bodyObjects);
@@ -383,7 +396,11 @@ class StackObjectsLayoutFunction {
383
396
  object.setPosition({
384
397
  width: actualWidth
385
398
  });
386
- object.updateDimensions();
399
+ });
400
+ _(objects).forEach((object) => {
401
+ if (object.getType() !== ObjectType.VERTICAL_RULE) {
402
+ object.updateDimensions();
403
+ }
387
404
  });
388
405
  }
389
406
  adjustCanvasSize(canvas, requiredHeight) {
@@ -456,6 +473,7 @@ class StandardLayoutManager extends AbstractLayoutManager {
456
473
  this.repositionBackgrounds = new RepositionBackgroundsLayoutFunction();
457
474
  this.stackObjects = new StackObjectsLayoutFunction();
458
475
  this.updateObjectDimensions = new UpdateObjectDimensionsLayoutFunction();
476
+ this.layoutBatchScheduled = false;
459
477
  }
460
478
  /**
461
479
  * @override
@@ -497,12 +515,19 @@ class StandardLayoutManager extends AbstractLayoutManager {
497
515
  this.stackAndRedraw();
498
516
  }
499
517
  stackAndRedraw() {
500
- this.stackObjects.execute(this.canvas);
501
- this.repositionCutoffs.execute(this.canvas);
502
- this.repositionBorders.execute(this.canvas);
503
- this.repositionBackgrounds.execute(this.canvas);
504
- this.updateObjectDimensions.execute(this.canvas);
505
- this.redrawAll.execute(this.canvas);
518
+ if (this.layoutBatchScheduled) {
519
+ return;
520
+ }
521
+ this.layoutBatchScheduled = true;
522
+ Promise.resolve().then(() => {
523
+ this.stackObjects.execute(this.canvas);
524
+ this.repositionCutoffs.execute(this.canvas);
525
+ this.repositionBorders.execute(this.canvas);
526
+ this.repositionBackgrounds.execute(this.canvas);
527
+ this.updateObjectDimensions.execute(this.canvas);
528
+ this.redrawAll.execute(this.canvas);
529
+ this.layoutBatchScheduled = false;
530
+ });
506
531
  }
507
532
  }
508
533
 
@@ -1042,7 +1067,8 @@ class AbstractCanvasObject {
1042
1067
  mt: options.allowScale,
1043
1068
  mr: options.allowScale,
1044
1069
  mb: options.allowScale,
1045
- mtr: options.allowRotate
1070
+ mtr: options.allowRotate,
1071
+ // dragControl: options.allowMove // Show drag control when movement is allowed
1046
1072
  });
1047
1073
  }
1048
1074
  }
@@ -1780,15 +1806,42 @@ var ObjectType;
1780
1806
  class Text extends AbstractCanvasObject {
1781
1807
  constructor(canvas, fabricObject, persistedField) {
1782
1808
  super(ObjectType.TEXT, canvas, fabricObject, persistedField);
1809
+ this.notifyCanvasTimeout = null;
1810
+ this.NOTIFY_DELAY_MS = 150; // Debounce canvas notifications
1783
1811
  this.canvas = canvas;
1784
1812
  this.fontLibrary = canvas.getFontLibrary();
1785
1813
  this.baseComponentHeight = this.fabricObject.height;
1786
1814
  this.text$ = new BehaviorSubject(this.fabricObject.text);
1787
1815
  this.data.userText = this.fabricObject.text;
1788
1816
  this.textSelection$ = new BehaviorSubject(new TextSelection(this.fabricObject, this.fontLibrary));
1789
- this.fabricObject.set({ strokeWidth: 0 });
1817
+ this.fabricObject.set({
1818
+ strokeWidth: 0,
1819
+ perPixelTargetFind: false // Disable per-pixel detection for this object
1820
+ });
1790
1821
  this.fabricObject.on("changed", () => this.onTextChange());
1791
1822
  this.fabricObject.on("selection:changed", () => this.onSelectionChange());
1823
+ this.fabricObject.containsPoint.bind(this.fabricObject);
1824
+ this.fabricObject.containsPoint = function (point) {
1825
+ const coords = this.getCoords();
1826
+ if (coords.length !== 4)
1827
+ return false;
1828
+ let inside = false;
1829
+ for (let i = 0, j = coords.length - 1; i < coords.length; j = i++) {
1830
+ const xi = coords[i].x, yi = coords[i].y;
1831
+ const xj = coords[j].x, yj = coords[j].y;
1832
+ const intersect = ((yi > point.y) !== (yj > point.y))
1833
+ && (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi);
1834
+ if (intersect)
1835
+ inside = !inside;
1836
+ }
1837
+ return inside;
1838
+ };
1839
+ this.fabricObject.on("mousedown", (e) => {
1840
+ var _a;
1841
+ if (!this.fabricObject.isEditing) {
1842
+ (_a = e.e) === null || _a === void 0 ? void 0 : _a.preventDefault();
1843
+ }
1844
+ });
1792
1845
  this.applyBulletListMonkeyPatches();
1793
1846
  }
1794
1847
  /**
@@ -2398,6 +2451,7 @@ class Text extends AbstractCanvasObject {
2398
2451
  lockMovementY: true,
2399
2452
  hasControls: false,
2400
2453
  selectable: false,
2454
+ // evented: false, // Don't capture mouse events - allows clicks to pass through to textbox
2401
2455
  originX: "left",
2402
2456
  originY: "top"
2403
2457
  });
@@ -2504,13 +2558,24 @@ class Text extends AbstractCanvasObject {
2504
2558
  * @inheritDoc
2505
2559
  */
2506
2560
  updateFabricObjectBehavior() {
2561
+ // DON'T call super.updateFabricObjectBehavior() for textboxes
2562
+ // because it locks movement which interferes with text editing in Fabric v7
2507
2563
  super.updateFabricObjectBehavior();
2508
2564
  const profile = this.canvas.getProfile();
2509
2565
  const allowManualPositioning = !this.canvas.getLayoutManager().isEnforcedPositioning;
2566
+ // For textboxes, we keep them moveable but hide all controls except drag control
2567
+ // This allows text editing to work properly while still supporting drag via the icon
2510
2568
  this.fabricObject.set({
2569
+ lockMovementX: false, // Allow dragging
2570
+ lockMovementY: false, // Allow dragging
2511
2571
  lockScalingY: true,
2512
2572
  lockScalingX: this.isPinToBottom(),
2513
- lockScalingFlip: true
2573
+ lockScalingFlip: true,
2574
+ lockRotation: true,
2575
+ selectable: true,
2576
+ editable: false, // Only enable on double-click
2577
+ hoverCursor: "move", // Show move cursor by default
2578
+ moveCursor: "move",
2514
2579
  });
2515
2580
  this.fabricObject.setControlsVisibility({
2516
2581
  tl: false,
@@ -2520,35 +2585,50 @@ class Text extends AbstractCanvasObject {
2520
2585
  ml: allowManualPositioning && profile.allowScale && !this.isPinToBottom(),
2521
2586
  mt: false,
2522
2587
  mr: allowManualPositioning && profile.allowScale && !this.isPinToBottom(),
2523
- mb: false
2588
+ mb: false,
2589
+ mtr: false,
2524
2590
  });
2525
2591
  this.redraw();
2526
2592
  }
2527
2593
  onTextChange() {
2528
2594
  this.data.userText = this.fabricObject.text;
2529
2595
  this.text$.next(this.fabricObject.text);
2530
- if (this.getBoundingBox().width > this.canvas.getDimensions().widthPx) {
2531
- this.fabricObject.set({ left: 0, width: this.canvas.getDimensions().widthPx });
2532
- this.redraw();
2596
+ // Only check width bounds if text is close to canvas edge (performance optimization)
2597
+ const currentLeft = this.fabricObject.left || 0;
2598
+ const canvasWidth = this.canvas.getDimensions().widthPx;
2599
+ // Skip expensive getBoundingBox if object is clearly within bounds
2600
+ if (currentLeft + (this.fabricObject.width || 0) > canvasWidth * 0.9) {
2601
+ if (this.getBoundingBox().width > canvasWidth) {
2602
+ this.fabricObject.set({ left: 0, width: canvasWidth });
2603
+ this.redraw();
2604
+ }
2533
2605
  }
2534
2606
  }
2535
2607
  onDelete() {
2536
2608
  this.removeBullets();
2537
2609
  }
2538
2610
  onSelectionChange() {
2539
- this.updateTextSelection();
2611
+ this.updateTextSelection(false);
2540
2612
  }
2541
2613
  hasTextSelection() {
2542
2614
  return this.canvas.getSelection().isText && this.textSelection$.value.length > 0;
2543
2615
  }
2544
- updateTextSelection() {
2616
+ updateTextSelection(notifyCanvas = true) {
2545
2617
  if (this.canvas.headless)
2546
2618
  return;
2547
2619
  this.textSelection$.next(new TextSelection(this.fabricObject, this.fontLibrary));
2548
- this.notifyCanvasOfTextChanges();
2620
+ if (notifyCanvas) {
2621
+ this.notifyCanvasOfTextChanges();
2622
+ }
2549
2623
  }
2550
2624
  notifyCanvasOfTextChanges() {
2551
- this.canvas.notifyTextChanged();
2625
+ if (this.notifyCanvasTimeout) {
2626
+ clearTimeout(this.notifyCanvasTimeout);
2627
+ }
2628
+ this.notifyCanvasTimeout = setTimeout(() => {
2629
+ this.canvas.notifyTextChanged();
2630
+ this.notifyCanvasTimeout = null;
2631
+ }, this.NOTIFY_DELAY_MS);
2552
2632
  }
2553
2633
  }
2554
2634
  Text.NEW_LINE = "\n";
@@ -2714,6 +2794,7 @@ class AbstractInteractiveCanvas {
2714
2794
  this.devicePixelRatio = devicePixelRatio;
2715
2795
  this.fontLibrary = new FontLibrary();
2716
2796
  this.dimensionChangeRejections$ = new Subject();
2797
+ this.redrawScheduled = false;
2717
2798
  this.IMAGE_CAPTURE_COEFFICIENT = this.calculateImageCaptureCoefficient();
2718
2799
  /**
2719
2800
  * Fabric defaults to only 2 decimal places when saving, which causes havoc on the scaling of large images.
@@ -2736,7 +2817,7 @@ class AbstractInteractiveCanvas {
2736
2817
  this.fabricCanvas.stateful = true;
2737
2818
  this.fabricCanvas.preserveObjectStacking = true;
2738
2819
  this.fabricCanvas.perPixelTargetFind = true;
2739
- this.fabricCanvas.targetFindTolerance = 4;
2820
+ this.fabricCanvas.targetFindTolerance = 4; // Reduced tolerance for more precise text selection
2740
2821
  this.setBackgroundColor("#FFFFFF");
2741
2822
  }
2742
2823
  /**
@@ -3040,7 +3121,6 @@ class AbstractInteractiveCanvas {
3040
3121
  */
3041
3122
  addImageObject(imageUrl) {
3042
3123
  return __awaiter(this, void 0, void 0, function* () {
3043
- // Don't let the image be more than half the canvas in size.
3044
3124
  const maxSizeScale = 2;
3045
3125
  const maxWidth = this.dimensions$.value.widthPx / maxSizeScale;
3046
3126
  const options = { crossOrigin: "anonymous" };
@@ -3052,7 +3132,6 @@ class AbstractInteractiveCanvas {
3052
3132
  if (curWidth > maxWidth) {
3053
3133
  newScale = maxWidth / curWidth;
3054
3134
  }
3055
- // Set image properties and add to the canvas
3056
3135
  const fabricImage = new this.fabric.Image(image, {
3057
3136
  id: id,
3058
3137
  scaleX: newScale,
@@ -3072,7 +3151,6 @@ class AbstractInteractiveCanvas {
3072
3151
  return object;
3073
3152
  }
3074
3153
  else {
3075
- // Browser-side logic
3076
3154
  return yield new Promise((resolve) => {
3077
3155
  this.fabric.Image.fromURL(imageUrl, options).then((fabricImage) => {
3078
3156
  const id = this.randomId();
@@ -3124,14 +3202,13 @@ class AbstractInteractiveCanvas {
3124
3202
  fontFamily: profile.defaultFont,
3125
3203
  textAlign: profile.defaultTextAlignment,
3126
3204
  hoverCursor: "move",
3127
- moveCursor: 'move',
3205
+ moveCursor: "move",
3206
+ editable: false,
3207
+ editingBorderColor: '#2196F3',
3128
3208
  multiLine: true,
3129
3209
  lineHeight: profile.defaultLineHeight,
3130
3210
  charSpacing: profile.defaultCharacterSpacing,
3131
- perPixelTargetFind: false,
3132
- targetFindTolerance: 4,
3133
3211
  splitByGrapheme: true,
3134
- editable: false, // Only enable on double-click
3135
3212
  padding: 0,
3136
3213
  cornerSize: 8,
3137
3214
  touchCornerSize: 12
@@ -3581,9 +3658,17 @@ class AbstractInteractiveCanvas {
3581
3658
  }
3582
3659
  /**
3583
3660
  * Redraw the canvas.
3661
+ * Uses requestAnimationFrame to batch multiple redraw calls into a single render.
3584
3662
  */
3585
3663
  redraw() {
3586
- this.fabricCanvas.requestRenderAll();
3664
+ if (this.redrawScheduled) {
3665
+ return;
3666
+ }
3667
+ this.redrawScheduled = true;
3668
+ requestAnimationFrame(() => {
3669
+ this.fabricCanvas.requestRenderAll();
3670
+ this.redrawScheduled = false;
3671
+ });
3587
3672
  }
3588
3673
  syncObjectList() {
3589
3674
  const newObjectList = _(this.fabricCanvas.getObjects())
@@ -3709,7 +3794,6 @@ class AbstractInteractiveCanvas {
3709
3794
  * Update the actual dimensions of the canvas by multiplying the requested dimensions by the current zoom factor.
3710
3795
  */
3711
3796
  updateCanvasDimensions(dims, zoom) {
3712
- const oldWidth = this.fabricCanvas.getWidth();
3713
3797
  const width = dims.widthPx * zoom;
3714
3798
  const height = dims.heightPx * zoom;
3715
3799
  this.fabricCanvas.setZoom(zoom);
@@ -3717,34 +3801,6 @@ class AbstractInteractiveCanvas {
3717
3801
  width,
3718
3802
  height
3719
3803
  });
3720
- const scaleFactor = width / oldWidth;
3721
- const objects = this.fabricCanvas.getObjects();
3722
- objects.forEach((obj) => {
3723
- obj.set({
3724
- left: obj.left * scaleFactor,
3725
- });
3726
- if (obj.type === "textbox" || obj.type === "text") {
3727
- obj.set({
3728
- scaleX: 1,
3729
- scaleY: 1,
3730
- width: obj.width * scaleFactor,
3731
- });
3732
- }
3733
- else if (obj.type === "border" || obj.type === "rect") {
3734
- obj.set({
3735
- width: obj.width * scaleFactor,
3736
- scaleX: 1,
3737
- strokeUniform: true,
3738
- });
3739
- }
3740
- else {
3741
- obj.set({
3742
- scaleX: obj.scaleX * scaleFactor,
3743
- });
3744
- }
3745
- obj.setCoords();
3746
- });
3747
- this.fabricCanvas.requestRenderAll();
3748
3804
  }
3749
3805
  onProfileChanged(profile) {
3750
3806
  this.setMultiSelect(profile.allowMultiSelect);
@@ -3899,7 +3955,6 @@ class InteractiveCanvas extends AbstractInteractiveCanvas {
3899
3955
  this.fabricCanvas.on("selection:created", (event) => this.onFabricSelectionChanged(event));
3900
3956
  this.fabricCanvas.on("selection:cleared", (event) => this.onFabricSelectionChanged(event));
3901
3957
  this.fabricCanvas.on("text:editing:entered", () => this.onTextEditing());
3902
- //this.fabricCanvas.on("text:editing:exited", () => this.onTextEditing());
3903
3958
  this.fabricCanvas.on("text:editing:exited", (event) => {
3904
3959
  this.onTextEditing();
3905
3960
  const target = event.target;
@@ -3908,8 +3963,6 @@ class InteractiveCanvas extends AbstractInteractiveCanvas {
3908
3963
  }
3909
3964
  });
3910
3965
  this.fabricCanvas.on("text:changed", () => this.onTextChange());
3911
- // Handle double-click on textbox to enter editing mode
3912
- // @ts-ignore
3913
3966
  this.fabricCanvas.on("mouse:dblclick", (event) => this.onDoubleClick(event));
3914
3967
  }
3915
3968
  /**
@@ -3944,29 +3997,20 @@ class InteractiveCanvas extends AbstractInteractiveCanvas {
3944
3997
  selection.setEvent(event);
3945
3998
  }
3946
3999
  this.selection$.next(selection);
3947
- this.enterTextSelectionModeIfRequired();
3948
- }
3949
- enterTextSelectionModeIfRequired() {
3950
- var _a, _b;
3951
- if (!this.profile$.value.enterTextEditImmediatelyOnSelect && !this.layoutManager$.value.enterTextEditImmediatelyOnSelect)
3952
- return;
3953
- 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)
3954
- return;
3955
- const text = this.selection$.value.objects[0];
3956
- text.enterEditing();
3957
4000
  }
3958
4001
  onTextEditing() {
3959
4002
  const selected = this.getFieldsByCanvasObject(this.fabricCanvas.getActiveObjects());
3960
4003
  this.selection$.next(new SelectionData(selected));
3961
4004
  }
3962
4005
  onDoubleClick(event) {
4006
+ var _a, _b;
3963
4007
  const target = event.target;
3964
4008
  if (target && (target.type === 'textbox' || target.type === 'i-text')) {
3965
- if (!target.isEditing) {
4009
+ const targetIText = target;
4010
+ if (!targetIText.isEditing) {
3966
4011
  target.set({ editable: true, hoverCursor: 'text' });
3967
- // target.enterEditing();
3968
- // target.hiddenTextarea?.focus();
3969
- this.enterTextSelectionModeIfRequired();
4012
+ (_a = targetIText.enterEditing) === null || _a === void 0 ? void 0 : _a.call(targetIText);
4013
+ (_b = targetIText.hiddenTextarea) === null || _b === void 0 ? void 0 : _b.focus();
3970
4014
  }
3971
4015
  }
3972
4016
  }
@@ -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
 
@@ -283,13 +289,21 @@ class AdvancedLayoutManager extends AbstractLayoutManager {
283
289
  this.repositionCutoffs = new RepositionCutoffsLayoutFunction();
284
290
  this.repositionBackgrounds = new RepositionBackgroundsLayoutFunction();
285
291
  this.resizeCanvas = new ResizeCanvasLayoutFunction();
292
+ this.dimensionsLayoutScheduled = false;
286
293
  }
287
294
  onCanvasDimensionsChange() {
288
- this.pinToBottom.execute(this.canvas);
289
- this.repositionCutoffs.execute(this.canvas);
290
- this.repositionBorders.execute(this.canvas);
291
- this.repositionBackgrounds.execute(this.canvas);
292
- this.redrawAll.execute(this.canvas);
295
+ if (this.dimensionsLayoutScheduled) {
296
+ return;
297
+ }
298
+ this.dimensionsLayoutScheduled = true;
299
+ Promise.resolve().then(() => {
300
+ this.pinToBottom.execute(this.canvas);
301
+ this.repositionCutoffs.execute(this.canvas);
302
+ this.repositionBorders.execute(this.canvas);
303
+ this.repositionBackgrounds.execute(this.canvas);
304
+ this.redrawAll.execute(this.canvas);
305
+ this.dimensionsLayoutScheduled = false;
306
+ });
293
307
  }
294
308
  onTextChange() {
295
309
  this.resizeCanvas.execute(this.canvas);
@@ -317,7 +331,6 @@ class StackObjectsLayoutFunction {
317
331
  const footerObjects = objects[0];
318
332
  const bodyObjects = objects[1];
319
333
  const bodyWidth = canvasDimensions.widthPx - canvasMargin.right - canvasMargin.left;
320
- canvas.redraw();
321
334
  //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.
322
335
  this.applyWidths(allObjects, bodyWidth);
323
336
  const bodyRows = this.getRows(bodyObjects);
@@ -404,7 +417,11 @@ class StackObjectsLayoutFunction {
404
417
  object.setPosition({
405
418
  width: actualWidth
406
419
  });
407
- object.updateDimensions();
420
+ });
421
+ _(objects).forEach((object) => {
422
+ if (object.getType() !== exports.ObjectType.VERTICAL_RULE) {
423
+ object.updateDimensions();
424
+ }
408
425
  });
409
426
  }
410
427
  adjustCanvasSize(canvas, requiredHeight) {
@@ -477,6 +494,7 @@ class StandardLayoutManager extends AbstractLayoutManager {
477
494
  this.repositionBackgrounds = new RepositionBackgroundsLayoutFunction();
478
495
  this.stackObjects = new StackObjectsLayoutFunction();
479
496
  this.updateObjectDimensions = new UpdateObjectDimensionsLayoutFunction();
497
+ this.layoutBatchScheduled = false;
480
498
  }
481
499
  /**
482
500
  * @override
@@ -518,12 +536,19 @@ class StandardLayoutManager extends AbstractLayoutManager {
518
536
  this.stackAndRedraw();
519
537
  }
520
538
  stackAndRedraw() {
521
- this.stackObjects.execute(this.canvas);
522
- this.repositionCutoffs.execute(this.canvas);
523
- this.repositionBorders.execute(this.canvas);
524
- this.repositionBackgrounds.execute(this.canvas);
525
- this.updateObjectDimensions.execute(this.canvas);
526
- this.redrawAll.execute(this.canvas);
539
+ if (this.layoutBatchScheduled) {
540
+ return;
541
+ }
542
+ this.layoutBatchScheduled = true;
543
+ Promise.resolve().then(() => {
544
+ this.stackObjects.execute(this.canvas);
545
+ this.repositionCutoffs.execute(this.canvas);
546
+ this.repositionBorders.execute(this.canvas);
547
+ this.repositionBackgrounds.execute(this.canvas);
548
+ this.updateObjectDimensions.execute(this.canvas);
549
+ this.redrawAll.execute(this.canvas);
550
+ this.layoutBatchScheduled = false;
551
+ });
527
552
  }
528
553
  }
529
554
 
@@ -1063,7 +1088,8 @@ class AbstractCanvasObject {
1063
1088
  mt: options.allowScale,
1064
1089
  mr: options.allowScale,
1065
1090
  mb: options.allowScale,
1066
- mtr: options.allowRotate
1091
+ mtr: options.allowRotate,
1092
+ // dragControl: options.allowMove // Show drag control when movement is allowed
1067
1093
  });
1068
1094
  }
1069
1095
  }
@@ -1801,15 +1827,42 @@ exports.ObjectType = void 0;
1801
1827
  class Text extends AbstractCanvasObject {
1802
1828
  constructor(canvas, fabricObject, persistedField) {
1803
1829
  super(exports.ObjectType.TEXT, canvas, fabricObject, persistedField);
1830
+ this.notifyCanvasTimeout = null;
1831
+ this.NOTIFY_DELAY_MS = 150; // Debounce canvas notifications
1804
1832
  this.canvas = canvas;
1805
1833
  this.fontLibrary = canvas.getFontLibrary();
1806
1834
  this.baseComponentHeight = this.fabricObject.height;
1807
1835
  this.text$ = new rxjs.BehaviorSubject(this.fabricObject.text);
1808
1836
  this.data.userText = this.fabricObject.text;
1809
1837
  this.textSelection$ = new rxjs.BehaviorSubject(new TextSelection(this.fabricObject, this.fontLibrary));
1810
- this.fabricObject.set({ strokeWidth: 0 });
1838
+ this.fabricObject.set({
1839
+ strokeWidth: 0,
1840
+ perPixelTargetFind: false // Disable per-pixel detection for this object
1841
+ });
1811
1842
  this.fabricObject.on("changed", () => this.onTextChange());
1812
1843
  this.fabricObject.on("selection:changed", () => this.onSelectionChange());
1844
+ this.fabricObject.containsPoint.bind(this.fabricObject);
1845
+ this.fabricObject.containsPoint = function (point) {
1846
+ const coords = this.getCoords();
1847
+ if (coords.length !== 4)
1848
+ return false;
1849
+ let inside = false;
1850
+ for (let i = 0, j = coords.length - 1; i < coords.length; j = i++) {
1851
+ const xi = coords[i].x, yi = coords[i].y;
1852
+ const xj = coords[j].x, yj = coords[j].y;
1853
+ const intersect = ((yi > point.y) !== (yj > point.y))
1854
+ && (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi);
1855
+ if (intersect)
1856
+ inside = !inside;
1857
+ }
1858
+ return inside;
1859
+ };
1860
+ this.fabricObject.on("mousedown", (e) => {
1861
+ var _a;
1862
+ if (!this.fabricObject.isEditing) {
1863
+ (_a = e.e) === null || _a === void 0 ? void 0 : _a.preventDefault();
1864
+ }
1865
+ });
1813
1866
  this.applyBulletListMonkeyPatches();
1814
1867
  }
1815
1868
  /**
@@ -2419,6 +2472,7 @@ class Text extends AbstractCanvasObject {
2419
2472
  lockMovementY: true,
2420
2473
  hasControls: false,
2421
2474
  selectable: false,
2475
+ // evented: false, // Don't capture mouse events - allows clicks to pass through to textbox
2422
2476
  originX: "left",
2423
2477
  originY: "top"
2424
2478
  });
@@ -2525,13 +2579,24 @@ class Text extends AbstractCanvasObject {
2525
2579
  * @inheritDoc
2526
2580
  */
2527
2581
  updateFabricObjectBehavior() {
2582
+ // DON'T call super.updateFabricObjectBehavior() for textboxes
2583
+ // because it locks movement which interferes with text editing in Fabric v7
2528
2584
  super.updateFabricObjectBehavior();
2529
2585
  const profile = this.canvas.getProfile();
2530
2586
  const allowManualPositioning = !this.canvas.getLayoutManager().isEnforcedPositioning;
2587
+ // For textboxes, we keep them moveable but hide all controls except drag control
2588
+ // This allows text editing to work properly while still supporting drag via the icon
2531
2589
  this.fabricObject.set({
2590
+ lockMovementX: false, // Allow dragging
2591
+ lockMovementY: false, // Allow dragging
2532
2592
  lockScalingY: true,
2533
2593
  lockScalingX: this.isPinToBottom(),
2534
- lockScalingFlip: true
2594
+ lockScalingFlip: true,
2595
+ lockRotation: true,
2596
+ selectable: true,
2597
+ editable: false, // Only enable on double-click
2598
+ hoverCursor: "move", // Show move cursor by default
2599
+ moveCursor: "move",
2535
2600
  });
2536
2601
  this.fabricObject.setControlsVisibility({
2537
2602
  tl: false,
@@ -2541,35 +2606,50 @@ class Text extends AbstractCanvasObject {
2541
2606
  ml: allowManualPositioning && profile.allowScale && !this.isPinToBottom(),
2542
2607
  mt: false,
2543
2608
  mr: allowManualPositioning && profile.allowScale && !this.isPinToBottom(),
2544
- mb: false
2609
+ mb: false,
2610
+ mtr: false,
2545
2611
  });
2546
2612
  this.redraw();
2547
2613
  }
2548
2614
  onTextChange() {
2549
2615
  this.data.userText = this.fabricObject.text;
2550
2616
  this.text$.next(this.fabricObject.text);
2551
- if (this.getBoundingBox().width > this.canvas.getDimensions().widthPx) {
2552
- this.fabricObject.set({ left: 0, width: this.canvas.getDimensions().widthPx });
2553
- this.redraw();
2617
+ // Only check width bounds if text is close to canvas edge (performance optimization)
2618
+ const currentLeft = this.fabricObject.left || 0;
2619
+ const canvasWidth = this.canvas.getDimensions().widthPx;
2620
+ // Skip expensive getBoundingBox if object is clearly within bounds
2621
+ if (currentLeft + (this.fabricObject.width || 0) > canvasWidth * 0.9) {
2622
+ if (this.getBoundingBox().width > canvasWidth) {
2623
+ this.fabricObject.set({ left: 0, width: canvasWidth });
2624
+ this.redraw();
2625
+ }
2554
2626
  }
2555
2627
  }
2556
2628
  onDelete() {
2557
2629
  this.removeBullets();
2558
2630
  }
2559
2631
  onSelectionChange() {
2560
- this.updateTextSelection();
2632
+ this.updateTextSelection(false);
2561
2633
  }
2562
2634
  hasTextSelection() {
2563
2635
  return this.canvas.getSelection().isText && this.textSelection$.value.length > 0;
2564
2636
  }
2565
- updateTextSelection() {
2637
+ updateTextSelection(notifyCanvas = true) {
2566
2638
  if (this.canvas.headless)
2567
2639
  return;
2568
2640
  this.textSelection$.next(new TextSelection(this.fabricObject, this.fontLibrary));
2569
- this.notifyCanvasOfTextChanges();
2641
+ if (notifyCanvas) {
2642
+ this.notifyCanvasOfTextChanges();
2643
+ }
2570
2644
  }
2571
2645
  notifyCanvasOfTextChanges() {
2572
- this.canvas.notifyTextChanged();
2646
+ if (this.notifyCanvasTimeout) {
2647
+ clearTimeout(this.notifyCanvasTimeout);
2648
+ }
2649
+ this.notifyCanvasTimeout = setTimeout(() => {
2650
+ this.canvas.notifyTextChanged();
2651
+ this.notifyCanvasTimeout = null;
2652
+ }, this.NOTIFY_DELAY_MS);
2573
2653
  }
2574
2654
  }
2575
2655
  Text.NEW_LINE = "\n";
@@ -2735,6 +2815,7 @@ class AbstractInteractiveCanvas {
2735
2815
  this.devicePixelRatio = devicePixelRatio;
2736
2816
  this.fontLibrary = new FontLibrary();
2737
2817
  this.dimensionChangeRejections$ = new rxjs.Subject();
2818
+ this.redrawScheduled = false;
2738
2819
  this.IMAGE_CAPTURE_COEFFICIENT = this.calculateImageCaptureCoefficient();
2739
2820
  /**
2740
2821
  * Fabric defaults to only 2 decimal places when saving, which causes havoc on the scaling of large images.
@@ -2757,7 +2838,7 @@ class AbstractInteractiveCanvas {
2757
2838
  this.fabricCanvas.stateful = true;
2758
2839
  this.fabricCanvas.preserveObjectStacking = true;
2759
2840
  this.fabricCanvas.perPixelTargetFind = true;
2760
- this.fabricCanvas.targetFindTolerance = 4;
2841
+ this.fabricCanvas.targetFindTolerance = 4; // Reduced tolerance for more precise text selection
2761
2842
  this.setBackgroundColor("#FFFFFF");
2762
2843
  }
2763
2844
  /**
@@ -3061,7 +3142,6 @@ class AbstractInteractiveCanvas {
3061
3142
  */
3062
3143
  addImageObject(imageUrl) {
3063
3144
  return __awaiter(this, void 0, void 0, function* () {
3064
- // Don't let the image be more than half the canvas in size.
3065
3145
  const maxSizeScale = 2;
3066
3146
  const maxWidth = this.dimensions$.value.widthPx / maxSizeScale;
3067
3147
  const options = { crossOrigin: "anonymous" };
@@ -3073,7 +3153,6 @@ class AbstractInteractiveCanvas {
3073
3153
  if (curWidth > maxWidth) {
3074
3154
  newScale = maxWidth / curWidth;
3075
3155
  }
3076
- // Set image properties and add to the canvas
3077
3156
  const fabricImage = new this.fabric.Image(image, {
3078
3157
  id: id,
3079
3158
  scaleX: newScale,
@@ -3093,7 +3172,6 @@ class AbstractInteractiveCanvas {
3093
3172
  return object;
3094
3173
  }
3095
3174
  else {
3096
- // Browser-side logic
3097
3175
  return yield new Promise((resolve) => {
3098
3176
  this.fabric.Image.fromURL(imageUrl, options).then((fabricImage) => {
3099
3177
  const id = this.randomId();
@@ -3145,14 +3223,13 @@ class AbstractInteractiveCanvas {
3145
3223
  fontFamily: profile.defaultFont,
3146
3224
  textAlign: profile.defaultTextAlignment,
3147
3225
  hoverCursor: "move",
3148
- moveCursor: 'move',
3226
+ moveCursor: "move",
3227
+ editable: false,
3228
+ editingBorderColor: '#2196F3',
3149
3229
  multiLine: true,
3150
3230
  lineHeight: profile.defaultLineHeight,
3151
3231
  charSpacing: profile.defaultCharacterSpacing,
3152
- perPixelTargetFind: false,
3153
- targetFindTolerance: 4,
3154
3232
  splitByGrapheme: true,
3155
- editable: false, // Only enable on double-click
3156
3233
  padding: 0,
3157
3234
  cornerSize: 8,
3158
3235
  touchCornerSize: 12
@@ -3602,9 +3679,17 @@ class AbstractInteractiveCanvas {
3602
3679
  }
3603
3680
  /**
3604
3681
  * Redraw the canvas.
3682
+ * Uses requestAnimationFrame to batch multiple redraw calls into a single render.
3605
3683
  */
3606
3684
  redraw() {
3607
- this.fabricCanvas.requestRenderAll();
3685
+ if (this.redrawScheduled) {
3686
+ return;
3687
+ }
3688
+ this.redrawScheduled = true;
3689
+ requestAnimationFrame(() => {
3690
+ this.fabricCanvas.requestRenderAll();
3691
+ this.redrawScheduled = false;
3692
+ });
3608
3693
  }
3609
3694
  syncObjectList() {
3610
3695
  const newObjectList = _(this.fabricCanvas.getObjects())
@@ -3730,7 +3815,6 @@ class AbstractInteractiveCanvas {
3730
3815
  * Update the actual dimensions of the canvas by multiplying the requested dimensions by the current zoom factor.
3731
3816
  */
3732
3817
  updateCanvasDimensions(dims, zoom) {
3733
- const oldWidth = this.fabricCanvas.getWidth();
3734
3818
  const width = dims.widthPx * zoom;
3735
3819
  const height = dims.heightPx * zoom;
3736
3820
  this.fabricCanvas.setZoom(zoom);
@@ -3738,34 +3822,6 @@ class AbstractInteractiveCanvas {
3738
3822
  width,
3739
3823
  height
3740
3824
  });
3741
- const scaleFactor = width / oldWidth;
3742
- const objects = this.fabricCanvas.getObjects();
3743
- objects.forEach((obj) => {
3744
- obj.set({
3745
- left: obj.left * scaleFactor,
3746
- });
3747
- if (obj.type === "textbox" || obj.type === "text") {
3748
- obj.set({
3749
- scaleX: 1,
3750
- scaleY: 1,
3751
- width: obj.width * scaleFactor,
3752
- });
3753
- }
3754
- else if (obj.type === "border" || obj.type === "rect") {
3755
- obj.set({
3756
- width: obj.width * scaleFactor,
3757
- scaleX: 1,
3758
- strokeUniform: true,
3759
- });
3760
- }
3761
- else {
3762
- obj.set({
3763
- scaleX: obj.scaleX * scaleFactor,
3764
- });
3765
- }
3766
- obj.setCoords();
3767
- });
3768
- this.fabricCanvas.requestRenderAll();
3769
3825
  }
3770
3826
  onProfileChanged(profile) {
3771
3827
  this.setMultiSelect(profile.allowMultiSelect);
@@ -3920,7 +3976,6 @@ class InteractiveCanvas extends AbstractInteractiveCanvas {
3920
3976
  this.fabricCanvas.on("selection:created", (event) => this.onFabricSelectionChanged(event));
3921
3977
  this.fabricCanvas.on("selection:cleared", (event) => this.onFabricSelectionChanged(event));
3922
3978
  this.fabricCanvas.on("text:editing:entered", () => this.onTextEditing());
3923
- //this.fabricCanvas.on("text:editing:exited", () => this.onTextEditing());
3924
3979
  this.fabricCanvas.on("text:editing:exited", (event) => {
3925
3980
  this.onTextEditing();
3926
3981
  const target = event.target;
@@ -3929,8 +3984,6 @@ class InteractiveCanvas extends AbstractInteractiveCanvas {
3929
3984
  }
3930
3985
  });
3931
3986
  this.fabricCanvas.on("text:changed", () => this.onTextChange());
3932
- // Handle double-click on textbox to enter editing mode
3933
- // @ts-ignore
3934
3987
  this.fabricCanvas.on("mouse:dblclick", (event) => this.onDoubleClick(event));
3935
3988
  }
3936
3989
  /**
@@ -3965,29 +4018,20 @@ class InteractiveCanvas extends AbstractInteractiveCanvas {
3965
4018
  selection.setEvent(event);
3966
4019
  }
3967
4020
  this.selection$.next(selection);
3968
- this.enterTextSelectionModeIfRequired();
3969
- }
3970
- enterTextSelectionModeIfRequired() {
3971
- var _a, _b;
3972
- if (!this.profile$.value.enterTextEditImmediatelyOnSelect && !this.layoutManager$.value.enterTextEditImmediatelyOnSelect)
3973
- return;
3974
- 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)
3975
- return;
3976
- const text = this.selection$.value.objects[0];
3977
- text.enterEditing();
3978
4021
  }
3979
4022
  onTextEditing() {
3980
4023
  const selected = this.getFieldsByCanvasObject(this.fabricCanvas.getActiveObjects());
3981
4024
  this.selection$.next(new SelectionData(selected));
3982
4025
  }
3983
4026
  onDoubleClick(event) {
4027
+ var _a, _b;
3984
4028
  const target = event.target;
3985
4029
  if (target && (target.type === 'textbox' || target.type === 'i-text')) {
3986
- if (!target.isEditing) {
4030
+ const targetIText = target;
4031
+ if (!targetIText.isEditing) {
3987
4032
  target.set({ editable: true, hoverCursor: 'text' });
3988
- // target.enterEditing();
3989
- // target.hiddenTextarea?.focus();
3990
- this.enterTextSelectionModeIfRequired();
4033
+ (_a = targetIText.enterEditing) === null || _a === void 0 ? void 0 : _a.call(targetIText);
4034
+ (_b = targetIText.hiddenTextarea) === null || _b === void 0 ? void 0 : _b.focus();
3991
4035
  }
3992
4036
  }
3993
4037
  }
@@ -9,6 +9,7 @@ export declare class AdvancedLayoutManager extends AbstractLayoutManager {
9
9
  private repositionCutoffs;
10
10
  private repositionBackgrounds;
11
11
  private resizeCanvas;
12
+ private dimensionsLayoutScheduled;
12
13
  constructor(canvas: IInteractiveCanvas);
13
14
  onCanvasDimensionsChange(): void;
14
15
  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
  }
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leapfrog/interactive-canvas",
3
- "version": "2.0.25-beta-8",
3
+ "version": "2.0.25-beta-10",
4
4
  "description": "Leapfrog interactive canvas",
5
5
  "files": [
6
6
  "dist"