@bensitu/image-editor 2.1.0 → 2.2.0

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.
Files changed (113) hide show
  1. package/README.md +235 -81
  2. package/dist/cjs/index.cjs +2969 -747
  3. package/dist/cjs/index.cjs.map +1 -1
  4. package/dist/esm/annotation/annotation-lock.js +7 -0
  5. package/dist/esm/annotation/annotation-lock.js.map +1 -0
  6. package/dist/esm/annotation/annotation-manager.js +217 -0
  7. package/dist/esm/annotation/annotation-manager.js.map +1 -0
  8. package/dist/esm/annotation/annotation-style.js +50 -0
  9. package/dist/esm/annotation/annotation-style.js.map +1 -0
  10. package/dist/esm/annotation/draw-controller.js +114 -0
  11. package/dist/esm/annotation/draw-controller.js.map +1 -0
  12. package/dist/esm/annotation/text-controller.js +234 -0
  13. package/dist/esm/annotation/text-controller.js.map +1 -0
  14. package/dist/esm/core/default-options.js +232 -3
  15. package/dist/esm/core/default-options.js.map +1 -1
  16. package/dist/esm/core/editor-object-kind.js +37 -0
  17. package/dist/esm/core/editor-object-kind.js.map +1 -0
  18. package/dist/esm/core/errors.js +19 -0
  19. package/dist/esm/core/errors.js.map +1 -1
  20. package/dist/esm/core/layer-order.js +100 -0
  21. package/dist/esm/core/layer-order.js.map +1 -0
  22. package/dist/esm/core/public-types.js +34 -1
  23. package/dist/esm/core/public-types.js.map +1 -1
  24. package/dist/esm/core/state-serializer.js +104 -24
  25. package/dist/esm/core/state-serializer.js.map +1 -1
  26. package/dist/esm/crop/crop-controller.js +2 -0
  27. package/dist/esm/crop/crop-controller.js.map +1 -1
  28. package/dist/esm/export/export-format.js.map +1 -1
  29. package/dist/esm/export/export-service.js +123 -135
  30. package/dist/esm/export/export-service.js.map +1 -1
  31. package/dist/esm/export/overlay-merge-service.js +75 -0
  32. package/dist/esm/export/overlay-merge-service.js.map +1 -0
  33. package/dist/esm/history/history-manager.js +2 -2
  34. package/dist/esm/history/history-manager.js.map +1 -1
  35. package/dist/esm/image/image-loader.js +18 -49
  36. package/dist/esm/image/image-loader.js.map +1 -1
  37. package/dist/esm/image/transform-controller.js.map +1 -1
  38. package/dist/esm/image-editor.js +1063 -60
  39. package/dist/esm/image-editor.js.map +1 -1
  40. package/dist/esm/index.js +1 -1
  41. package/dist/esm/index.js.map +1 -1
  42. package/dist/esm/mask/mask-factory.js +39 -14
  43. package/dist/esm/mask/mask-factory.js.map +1 -1
  44. package/dist/esm/mask/mask-label-manager.js +2 -0
  45. package/dist/esm/mask/mask-label-manager.js.map +1 -1
  46. package/dist/esm/mask/mask-list.js.map +1 -1
  47. package/dist/esm/mask/mask-style.js.map +1 -1
  48. package/dist/esm/mosaic/mosaic-controller.js +24 -28
  49. package/dist/esm/mosaic/mosaic-controller.js.map +1 -1
  50. package/dist/esm/utils/image-element-loader.js +55 -0
  51. package/dist/esm/utils/image-element-loader.js.map +1 -0
  52. package/dist/esm/utils/pointer.js +28 -0
  53. package/dist/esm/utils/pointer.js.map +1 -0
  54. package/dist/types/annotation/annotation-lock.d.ts +12 -0
  55. package/dist/types/annotation/annotation-lock.d.ts.map +1 -0
  56. package/dist/types/annotation/annotation-manager.d.ts +33 -0
  57. package/dist/types/annotation/annotation-manager.d.ts.map +1 -0
  58. package/dist/types/annotation/annotation-style.d.ts +13 -0
  59. package/dist/types/annotation/annotation-style.d.ts.map +1 -0
  60. package/dist/types/annotation/draw-controller.d.ts +43 -0
  61. package/dist/types/annotation/draw-controller.d.ts.map +1 -0
  62. package/dist/types/annotation/text-controller.d.ts +47 -0
  63. package/dist/types/annotation/text-controller.d.ts.map +1 -0
  64. package/dist/types/core/default-options.d.ts +14 -2
  65. package/dist/types/core/default-options.d.ts.map +1 -1
  66. package/dist/types/core/editor-object-kind.d.ts +29 -0
  67. package/dist/types/core/editor-object-kind.d.ts.map +1 -0
  68. package/dist/types/core/errors.d.ts +11 -1
  69. package/dist/types/core/errors.d.ts.map +1 -1
  70. package/dist/types/core/layer-order.d.ts +21 -0
  71. package/dist/types/core/layer-order.d.ts.map +1 -0
  72. package/dist/types/core/public-types.d.ts +222 -24
  73. package/dist/types/core/public-types.d.ts.map +1 -1
  74. package/dist/types/core/state-serializer.d.ts +30 -5
  75. package/dist/types/core/state-serializer.d.ts.map +1 -1
  76. package/dist/types/crop/crop-controller.d.ts +6 -7
  77. package/dist/types/crop/crop-controller.d.ts.map +1 -1
  78. package/dist/types/export/export-format.d.ts +5 -33
  79. package/dist/types/export/export-format.d.ts.map +1 -1
  80. package/dist/types/export/export-service.d.ts +24 -15
  81. package/dist/types/export/export-service.d.ts.map +1 -1
  82. package/dist/types/export/overlay-merge-service.d.ts +38 -0
  83. package/dist/types/export/overlay-merge-service.d.ts.map +1 -0
  84. package/dist/types/history/history-manager.d.ts +11 -14
  85. package/dist/types/history/history-manager.d.ts.map +1 -1
  86. package/dist/types/image/image-loader.d.ts +22 -17
  87. package/dist/types/image/image-loader.d.ts.map +1 -1
  88. package/dist/types/image/image-resampler.d.ts +1 -1
  89. package/dist/types/image/transform-controller.d.ts +5 -7
  90. package/dist/types/image/transform-controller.d.ts.map +1 -1
  91. package/dist/types/image-editor.d.ts +75 -7
  92. package/dist/types/image-editor.d.ts.map +1 -1
  93. package/dist/types/index.d.cts +3 -3
  94. package/dist/types/index.d.cts.map +1 -1
  95. package/dist/types/index.d.ts +3 -3
  96. package/dist/types/index.d.ts.map +1 -1
  97. package/dist/types/mask/mask-factory.d.ts.map +1 -1
  98. package/dist/types/mask/mask-label-manager.d.ts +10 -9
  99. package/dist/types/mask/mask-label-manager.d.ts.map +1 -1
  100. package/dist/types/mask/mask-list.d.ts +11 -12
  101. package/dist/types/mask/mask-list.d.ts.map +1 -1
  102. package/dist/types/mask/mask-style.d.ts +19 -20
  103. package/dist/types/mask/mask-style.d.ts.map +1 -1
  104. package/dist/types/mosaic/mosaic-controller.d.ts +3 -3
  105. package/dist/types/mosaic/mosaic-controller.d.ts.map +1 -1
  106. package/dist/types/ui/visibility-state.d.ts +2 -2
  107. package/dist/types/utils/image-element-loader.d.ts +19 -0
  108. package/dist/types/utils/image-element-loader.d.ts.map +1 -0
  109. package/dist/types/utils/pointer.d.ts +16 -0
  110. package/dist/types/utils/pointer.d.ts.map +1 -0
  111. package/dist/umd/image-editor.umd.js +1 -1
  112. package/dist/umd/image-editor.umd.js.map +1 -1
  113. package/package.json +1 -1
@@ -1,14 +1,20 @@
1
1
  import { AnimationQueue } from './animation/animation-queue.js';
2
2
  import { reportError, reportWarning } from './core/callback-reporter.js';
3
- import { areResolvedMosaicConfigsEqual, cloneResolvedMosaicConfig, getInvalidMosaicConfigFields, isLayoutMode, mergeMosaicConfigPatch, resolveOptions, } from './core/default-options.js';
3
+ import { areResolvedMosaicConfigsEqual, areResolvedDrawConfigsEqual, areResolvedTextAnnotationConfigsEqual, cloneResolvedMosaicConfig, cloneResolvedDrawConfig, cloneResolvedTextAnnotationConfig, getInvalidDrawConfigFields, getInvalidMosaicConfigFields, getInvalidTextAnnotationConfigFields, isLayoutMode, mergeDrawConfigPatch, mergeMosaicConfigPatch, mergeTextAnnotationConfigPatch, resolveOptions, } from './core/default-options.js';
4
4
  import { OperationGuard } from './core/operation-guard.js';
5
5
  import { loadFromState as loadFromStateImpl, saveState as saveStateImpl, } from './core/state-serializer.js';
6
6
  import { Command, HistoryManager } from './history/history-manager.js';
7
7
  import { detectFabric } from './fabric/fabric-adapter.js';
8
- import { isMaskObject } from './core/public-types.js';
8
+ import { isAnnotationObject, isDrawAnnotationObject, isEditableOverlayObject, isMaskObject, isTextAnnotationObject, } from './core/public-types.js';
9
+ import { getAnnotations as getAnnotationsImpl, removeAllAnnotations as removeAllAnnotationsImpl, removeAnnotationObjects, removeSelectedAnnotation as removeSelectedAnnotationImpl, renderAnnotationList, updateAnnotation as updateAnnotationImpl, updateAnnotationListSelection, updateSelectedAnnotation as updateSelectedAnnotationImpl, } from './annotation/annotation-manager.js';
10
+ import { attachTextEditingHandlersToAnnotations, createTextAnnotation as createTextAnnotationImpl, enterTextMode as enterTextModeImpl, exitTextMode as exitTextModeImpl, finalizeActiveTextEditing, } from './annotation/text-controller.js';
11
+ import { enterDrawMode as enterDrawModeImpl, exitDrawMode as exitDrawModeImpl, updateDrawBrush, } from './annotation/draw-controller.js';
12
+ import { isAnnotationLocked, isAnnotationUnlocked } from './annotation/annotation-lock.js';
13
+ import { syncAnnotationRuntimeStates } from './annotation/annotation-style.js';
14
+ import { normalizeLayerOrder, getEditableOverlayRange } from './core/layer-order.js';
9
15
  import { applyCrop as applyCropImpl, cancelCrop as cancelCropImpl, enterCropMode as enterCropModeImpl, } from './crop/crop-controller.js';
10
16
  import { enterMosaicMode as enterMosaicModeImpl, exitMosaicMode as exitMosaicModeImpl, updateMosaicPreview, } from './mosaic/mosaic-controller.js';
11
- import { downloadImage as downloadImageImpl, exportImageBase64 as exportImageBase64Impl, exportImageFile as exportImageFileImpl, mergeMasks as mergeMasksImpl, } from './export/export-service.js';
17
+ import { downloadImage as downloadImageImpl, exportImageBase64 as exportImageBase64Impl, exportImageFile as exportImageFileImpl, mergeAnnotations as mergeAnnotationsImpl, mergeMasks as mergeMasksImpl, } from './export/export-service.js';
12
18
  import { loadImage as loadImageImpl } from './image/image-loader.js';
13
19
  import { ViewportCache, applyCanvasDimensions, computeScrollableCanvasSize, measureScrollbarSize, } from './image/layout-manager.js';
14
20
  import { TransformController } from './image/transform-controller.js';
@@ -33,6 +39,22 @@ const CROP_MODE_CONTROL_KEYS = [
33
39
  'removeSelectedMaskButton',
34
40
  'removeAllMasksButton',
35
41
  'mergeMasksButton',
42
+ 'mergeAnnotationsButton',
43
+ 'enterTextModeButton',
44
+ 'exitTextModeButton',
45
+ 'textColorInput',
46
+ 'textFontSizeInput',
47
+ 'enterDrawModeButton',
48
+ 'exitDrawModeButton',
49
+ 'drawColorInput',
50
+ 'drawBrushSizeInput',
51
+ 'removeSelectedAnnotationButton',
52
+ 'removeAllAnnotationsButton',
53
+ 'deleteSelectedObjectButton',
54
+ 'bringSelectedObjectForwardButton',
55
+ 'sendSelectedObjectBackwardButton',
56
+ 'bringSelectedObjectToFrontButton',
57
+ 'sendSelectedObjectToBackButton',
36
58
  'downloadImageButton',
37
59
  'zoomInButton',
38
60
  'zoomOutButton',
@@ -50,6 +72,16 @@ const CROP_MODE_CONTROL_KEYS = [
50
72
  ];
51
73
  const CROP_MODE_ENABLED_KEYS = ['applyCropButton', 'cancelCropButton'];
52
74
  const CROP_SESSION_ALLOWED_OPERATIONS = new Set(['applyCrop', 'cancelCrop']);
75
+ const TEXT_MODE_ENABLED_KEYS = [
76
+ 'exitTextModeButton',
77
+ 'textColorInput',
78
+ 'textFontSizeInput',
79
+ ];
80
+ const DRAW_MODE_ENABLED_KEYS = [
81
+ 'exitDrawModeButton',
82
+ 'drawColorInput',
83
+ 'drawBrushSizeInput',
84
+ ];
53
85
  const MOSAIC_MODE_CONTROL_KEYS = [
54
86
  'scalePercentageInput',
55
87
  'rotateLeftDegreesInput',
@@ -60,6 +92,22 @@ const MOSAIC_MODE_CONTROL_KEYS = [
60
92
  'removeSelectedMaskButton',
61
93
  'removeAllMasksButton',
62
94
  'mergeMasksButton',
95
+ 'mergeAnnotationsButton',
96
+ 'enterTextModeButton',
97
+ 'exitTextModeButton',
98
+ 'textColorInput',
99
+ 'textFontSizeInput',
100
+ 'enterDrawModeButton',
101
+ 'exitDrawModeButton',
102
+ 'drawColorInput',
103
+ 'drawBrushSizeInput',
104
+ 'removeSelectedAnnotationButton',
105
+ 'removeAllAnnotationsButton',
106
+ 'deleteSelectedObjectButton',
107
+ 'bringSelectedObjectForwardButton',
108
+ 'sendSelectedObjectBackwardButton',
109
+ 'bringSelectedObjectToFrontButton',
110
+ 'sendSelectedObjectToBackButton',
63
111
  'downloadImageButton',
64
112
  'zoomInButton',
65
113
  'zoomOutButton',
@@ -102,6 +150,29 @@ const IMAGE_EDITOR_OPERATIONS = new Set([
102
150
  'removeSelectedMask',
103
151
  'removeAllMasks',
104
152
  'mergeMasks',
153
+ 'createTextAnnotation',
154
+ 'enterTextMode',
155
+ 'exitTextMode',
156
+ 'setTextConfig',
157
+ 'resetTextConfig',
158
+ 'setTextColor',
159
+ 'setTextFontSize',
160
+ 'enterDrawMode',
161
+ 'exitDrawMode',
162
+ 'setDrawConfig',
163
+ 'resetDrawConfig',
164
+ 'setDrawColor',
165
+ 'setDrawBrushSize',
166
+ 'updateSelectedAnnotation',
167
+ 'updateAnnotation',
168
+ 'removeSelectedAnnotation',
169
+ 'removeAllAnnotations',
170
+ 'deleteSelectedObject',
171
+ 'mergeAnnotations',
172
+ 'bringSelectedObjectForward',
173
+ 'sendSelectedObjectBackward',
174
+ 'bringSelectedObjectToFront',
175
+ 'sendSelectedObjectToBack',
105
176
  'enterCropMode',
106
177
  'applyCrop',
107
178
  'cancelCrop',
@@ -119,6 +190,27 @@ const IMAGE_EDITOR_OPERATIONS = new Set([
119
190
  'downloadImage',
120
191
  'dispose',
121
192
  ]);
193
+ const TOOL_MODE_ALLOWED_OPERATIONS = {
194
+ crop: CROP_SESSION_ALLOWED_OPERATIONS,
195
+ mosaic: MOSAIC_SESSION_ALLOWED_OPERATIONS,
196
+ text: new Set([
197
+ 'exitTextMode',
198
+ 'createTextAnnotation',
199
+ 'setTextConfig',
200
+ 'resetTextConfig',
201
+ 'setTextColor',
202
+ 'setTextFontSize',
203
+ 'saveState',
204
+ ]),
205
+ draw: new Set([
206
+ 'exitDrawMode',
207
+ 'setDrawConfig',
208
+ 'resetDrawConfig',
209
+ 'setDrawColor',
210
+ 'setDrawBrushSize',
211
+ 'saveState',
212
+ ]),
213
+ };
122
214
  function isImageEditorOperation(value) {
123
215
  return value !== null && IMAGE_EDITOR_OPERATIONS.has(value);
124
216
  }
@@ -161,6 +253,30 @@ export class ImageEditor {
161
253
  writable: true,
162
254
  value: void 0
163
255
  });
256
+ Object.defineProperty(this, "defaultTextConfig", {
257
+ enumerable: true,
258
+ configurable: true,
259
+ writable: true,
260
+ value: void 0
261
+ });
262
+ Object.defineProperty(this, "currentTextConfig", {
263
+ enumerable: true,
264
+ configurable: true,
265
+ writable: true,
266
+ value: void 0
267
+ });
268
+ Object.defineProperty(this, "defaultDrawConfig", {
269
+ enumerable: true,
270
+ configurable: true,
271
+ writable: true,
272
+ value: void 0
273
+ });
274
+ Object.defineProperty(this, "currentDrawConfig", {
275
+ enumerable: true,
276
+ configurable: true,
277
+ writable: true,
278
+ value: void 0
279
+ });
164
280
  Object.defineProperty(this, "canvas", {
165
281
  enumerable: true,
166
282
  configurable: true,
@@ -257,6 +373,12 @@ export class ImageEditor {
257
373
  writable: true,
258
374
  value: null
259
375
  });
376
+ Object.defineProperty(this, "annotationCounter", {
377
+ enumerable: true,
378
+ configurable: true,
379
+ writable: true,
380
+ value: 0
381
+ });
260
382
  Object.defineProperty(this, "lastSnapshot", {
261
383
  enumerable: true,
262
384
  configurable: true,
@@ -305,12 +427,36 @@ export class ImageEditor {
305
427
  writable: true,
306
428
  value: null
307
429
  });
430
+ Object.defineProperty(this, "textSession", {
431
+ enumerable: true,
432
+ configurable: true,
433
+ writable: true,
434
+ value: null
435
+ });
436
+ Object.defineProperty(this, "drawSession", {
437
+ enumerable: true,
438
+ configurable: true,
439
+ writable: true,
440
+ value: null
441
+ });
308
442
  Object.defineProperty(this, "domBindings", {
309
443
  enumerable: true,
310
444
  configurable: true,
311
445
  writable: true,
312
446
  value: null
313
447
  });
448
+ Object.defineProperty(this, "keyboardDocument", {
449
+ enumerable: true,
450
+ configurable: true,
451
+ writable: true,
452
+ value: null
453
+ });
454
+ Object.defineProperty(this, "keyboardHandler", {
455
+ enumerable: true,
456
+ configurable: true,
457
+ writable: true,
458
+ value: null
459
+ });
314
460
  Object.defineProperty(this, "isDisposed", {
315
461
  enumerable: true,
316
462
  configurable: true,
@@ -348,6 +494,10 @@ export class ImageEditor {
348
494
  this.currentLayoutMode = this.options.layoutMode;
349
495
  this.defaultMosaicConfig = this.options.defaultMosaicConfig;
350
496
  this.currentMosaicConfig = cloneResolvedMosaicConfig(this.defaultMosaicConfig);
497
+ this.defaultTextConfig = this.options.defaultTextConfig;
498
+ this.currentTextConfig = cloneResolvedTextAnnotationConfig(this.defaultTextConfig);
499
+ this.defaultDrawConfig = this.options.defaultDrawConfig;
500
+ this.currentDrawConfig = cloneResolvedDrawConfig(this.defaultDrawConfig);
351
501
  const rawDefaultLayoutMode = detected.options
352
502
  .defaultLayoutMode;
353
503
  if (rawDefaultLayoutMode !== undefined && !isLayoutMode(rawDefaultLayoutMode)) {
@@ -384,6 +534,23 @@ export class ImageEditor {
384
534
  removeSelectedMaskButton: 'removeSelectedMaskButton',
385
535
  removeAllMasksButton: 'removeAllMasksButton',
386
536
  mergeMasksButton: 'mergeMasksButton',
537
+ annotationList: 'annotationList',
538
+ enterTextModeButton: 'enterTextModeButton',
539
+ exitTextModeButton: 'exitTextModeButton',
540
+ textColorInput: 'textColorInput',
541
+ textFontSizeInput: 'textFontSizeInput',
542
+ enterDrawModeButton: 'enterDrawModeButton',
543
+ exitDrawModeButton: 'exitDrawModeButton',
544
+ drawColorInput: 'drawColorInput',
545
+ drawBrushSizeInput: 'drawBrushSizeInput',
546
+ removeSelectedAnnotationButton: 'removeSelectedAnnotationButton',
547
+ removeAllAnnotationsButton: 'removeAllAnnotationsButton',
548
+ deleteSelectedObjectButton: 'deleteSelectedObjectButton',
549
+ mergeAnnotationsButton: 'mergeAnnotationsButton',
550
+ bringSelectedObjectForwardButton: 'bringSelectedObjectForwardButton',
551
+ sendSelectedObjectBackwardButton: 'sendSelectedObjectBackwardButton',
552
+ bringSelectedObjectToFrontButton: 'bringSelectedObjectToFrontButton',
553
+ sendSelectedObjectToBackButton: 'sendSelectedObjectToBackButton',
387
554
  downloadImageButton: 'downloadImageButton',
388
555
  maskList: 'maskList',
389
556
  zoomInButton: 'zoomInButton',
@@ -402,12 +569,13 @@ export class ImageEditor {
402
569
  uploadArea: 'uploadArea',
403
570
  };
404
571
  this.elements = { ...defaults, ...idMap };
405
- this.domBindings = new DomBindings((key) => this.elements[key], () => this.isDisposed, () => { var _a, _b; return (_b = (_a = this.canvasElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : document; });
406
572
  this.initCanvas();
573
+ this.domBindings = new DomBindings((key) => this.elements[key], () => this.isDisposed, () => { var _a, _b; return (_b = (_a = this.canvasElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : document; });
407
574
  this.transformController = new TransformController(this.buildTransformContext());
408
575
  this.bindDomEvents();
409
576
  this.updateInputs();
410
577
  this.updateMaskList();
578
+ this.updateAnnotationList();
411
579
  this.updateUi();
412
580
  if (this.options.initialImageBase64) {
413
581
  void this.loadImage(this.options.initialImageBase64).catch(() => {
@@ -459,20 +627,24 @@ export class ImageEditor {
459
627
  });
460
628
  this.canvas.on('selection:cleared', () => this.handleSelectionChanged([]));
461
629
  const onObjectEvent = (e) => {
462
- if (e.target && isMaskObject(e.target))
463
- this.syncMaskLabel(e.target);
630
+ if (e.target)
631
+ this.handleObjectMovingScalingRotating(e.target);
464
632
  };
465
633
  const onObjectModified = (e) => {
466
- if (!e.target || !isMaskObject(e.target))
467
- return;
468
- this.syncMaskLabel(e.target);
469
- this.saveState();
634
+ if (e.target)
635
+ this.handleObjectModified(e.target);
470
636
  };
471
637
  this.canvas.on('object:moving', onObjectEvent);
472
638
  this.canvas.on('object:scaling', onObjectEvent);
473
639
  this.canvas.on('object:rotating', onObjectEvent);
474
640
  this.canvas.on('object:modified', onObjectModified);
475
641
  }
642
+ getLiveCanvasOrThrow(operationName) {
643
+ if (this.isDisposed || !this.canvas) {
644
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" after dispose.`);
645
+ }
646
+ return this.canvas;
647
+ }
476
648
  bindDomEvents() {
477
649
  this.bindElementIfExists('uploadArea', 'click', () => {
478
650
  var _a;
@@ -507,6 +679,42 @@ export class ImageEditor {
507
679
  this.bindElementIfExists('mergeMasksButton', 'click', () => {
508
680
  void this.mergeMasks();
509
681
  });
682
+ this.bindElementIfExists('mergeAnnotationsButton', 'click', () => {
683
+ void this.mergeAnnotations();
684
+ });
685
+ this.bindElementIfExists('enterTextModeButton', 'click', () => {
686
+ this.enterTextMode();
687
+ });
688
+ this.bindElementIfExists('exitTextModeButton', 'click', () => {
689
+ this.exitTextMode();
690
+ });
691
+ this.bindElementIfExists('enterDrawModeButton', 'click', () => {
692
+ this.enterDrawMode();
693
+ });
694
+ this.bindElementIfExists('exitDrawModeButton', 'click', () => {
695
+ this.exitDrawMode();
696
+ });
697
+ this.bindElementIfExists('removeSelectedAnnotationButton', 'click', () => {
698
+ this.removeSelectedAnnotation();
699
+ });
700
+ this.bindElementIfExists('removeAllAnnotationsButton', 'click', () => {
701
+ this.removeAllAnnotations();
702
+ });
703
+ this.bindElementIfExists('deleteSelectedObjectButton', 'click', () => {
704
+ this.deleteSelectedObject();
705
+ });
706
+ this.bindElementIfExists('bringSelectedObjectForwardButton', 'click', () => {
707
+ this.bringSelectedObjectForward();
708
+ });
709
+ this.bindElementIfExists('sendSelectedObjectBackwardButton', 'click', () => {
710
+ this.sendSelectedObjectBackward();
711
+ });
712
+ this.bindElementIfExists('bringSelectedObjectToFrontButton', 'click', () => {
713
+ this.bringSelectedObjectToFront();
714
+ });
715
+ this.bindElementIfExists('sendSelectedObjectToBackButton', 'click', () => {
716
+ this.sendSelectedObjectToBack();
717
+ });
510
718
  this.bindElementIfExists('downloadImageButton', 'click', () => {
511
719
  this.downloadImage();
512
720
  });
@@ -573,11 +781,92 @@ export class ImageEditor {
573
781
  bindMosaicSizeInput('mosaicBlockSizeInput', (value) => {
574
782
  this.setMosaicBlockSize(value);
575
783
  });
784
+ const bindStringInput = (key, applyValue) => {
785
+ const handler = (event) => {
786
+ applyValue(event.target.value);
787
+ };
788
+ this.bindElementIfExists(key, 'input', handler);
789
+ this.bindElementIfExists(key, 'change', handler);
790
+ };
791
+ const bindNumberInput = (key, applyValue) => {
792
+ const handler = (event) => {
793
+ applyValue(parseFloat(event.target.value));
794
+ };
795
+ this.bindElementIfExists(key, 'input', handler);
796
+ this.bindElementIfExists(key, 'change', handler);
797
+ };
798
+ bindStringInput('textColorInput', (value) => this.applyTextColorInput(value));
799
+ bindNumberInput('textFontSizeInput', (value) => this.applyTextFontSizeInput(value));
800
+ bindStringInput('drawColorInput', (value) => this.applyDrawColorInput(value));
801
+ bindNumberInput('drawBrushSizeInput', (value) => this.applyDrawBrushSizeInput(value));
802
+ this.bindKeyboardEvents();
576
803
  }
577
804
  bindElementIfExists(key, event, handler) {
578
805
  var _a;
579
806
  (_a = this.domBindings) === null || _a === void 0 ? void 0 : _a.bindIfExists(key, event, handler);
580
807
  }
808
+ bindKeyboardEvents() {
809
+ var _a, _b;
810
+ const ownerDocument = (_b = (_a = this.canvasElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : document;
811
+ if (this.keyboardHandler && this.keyboardDocument) {
812
+ this.keyboardDocument.removeEventListener('keydown', this.keyboardHandler);
813
+ }
814
+ this.keyboardDocument = ownerDocument;
815
+ this.keyboardHandler = (event) => this.handleKeyboardEvent(event);
816
+ ownerDocument.addEventListener('keydown', this.keyboardHandler);
817
+ }
818
+ isNativeTextInputActive() {
819
+ var _a;
820
+ const activeElement = (_a = this.keyboardDocument) === null || _a === void 0 ? void 0 : _a.activeElement;
821
+ if (!activeElement)
822
+ return false;
823
+ const tagName = activeElement.tagName.toLowerCase();
824
+ return (tagName === 'input' ||
825
+ tagName === 'textarea' ||
826
+ tagName === 'select' ||
827
+ activeElement.isContentEditable === true);
828
+ }
829
+ isFabricTextEditingActive() {
830
+ var _a;
831
+ const activeObject = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
832
+ return !!(activeObject &&
833
+ isTextAnnotationObject(activeObject) &&
834
+ activeObject.isEditing === true);
835
+ }
836
+ handleKeyboardEvent(event) {
837
+ if (this.isDisposed)
838
+ return;
839
+ if (event.key === 'Delete' || event.key === 'Backspace') {
840
+ if (this.isNativeTextInputActive() || this.isFabricTextEditingActive())
841
+ return;
842
+ this.deleteSelectedObject();
843
+ return;
844
+ }
845
+ if (event.key !== 'Escape')
846
+ return;
847
+ if (this.isFabricTextEditingActive() && this.canvas) {
848
+ finalizeActiveTextEditing(this.buildTextControllerContext(), { commit: false });
849
+ event.preventDefault();
850
+ return;
851
+ }
852
+ if (this.textSession) {
853
+ this.exitTextMode();
854
+ }
855
+ else if (this.drawSession) {
856
+ this.exitDrawMode();
857
+ }
858
+ else if (this.mosaicSession) {
859
+ this.exitMosaicMode();
860
+ }
861
+ else if (this.cropSession) {
862
+ this.cancelCrop();
863
+ }
864
+ }
865
+ finalizeActiveTextEditingIfNeeded() {
866
+ if (!this.canvas || !this.isFabricTextEditingActive())
867
+ return;
868
+ finalizeActiveTextEditing(this.buildTextControllerContext(), { commit: true });
869
+ }
581
870
  async loadImageFile(file) {
582
871
  const inputId = this.elements.imageInput;
583
872
  const inputEl = inputId
@@ -619,9 +908,11 @@ export class ImageEditor {
619
908
  return;
620
909
  if (!this.canRunIdleOperation('loadImage', options))
621
910
  return;
911
+ this.finalizeActiveTextEditingIfNeeded();
622
912
  const callbackContext = this.getOperationContext('loadImage', options);
623
913
  const previousImage = this.originalImage;
624
914
  const hadMasks = this.getMasks().length > 0;
915
+ const hadAnnotations = this.getAnnotations().length > 0;
625
916
  this.emitOptionCallback('onImageLoadStart', [callbackContext]);
626
917
  this.operationGuard.beginLoading();
627
918
  this.emitBusyChangeIfChanged(callbackContext);
@@ -650,6 +941,10 @@ export class ImageEditor {
650
941
  setMaskCounter: (v) => {
651
942
  this.maskCounter = v;
652
943
  },
944
+ getAnnotationCounter: () => this.annotationCounter,
945
+ setAnnotationCounter: (v) => {
946
+ this.annotationCounter = v;
947
+ },
653
948
  getCurrentScale: () => this.currentScale,
654
949
  setCurrentScale: (v) => {
655
950
  this.currentScale = v;
@@ -682,6 +977,7 @@ export class ImageEditor {
682
977
  this.lastMask = null;
683
978
  this.updateInputs();
684
979
  this.updateMaskList();
980
+ this.updateAnnotationList();
685
981
  this.updateUi();
686
982
  if (previousImage && previousImage !== this.originalImage) {
687
983
  this.emitOptionCallback('onImageCleared', [previousImage, callbackContext]);
@@ -693,6 +989,9 @@ export class ImageEditor {
693
989
  if (hadMasks) {
694
990
  this.emitMasksChanged(callbackContext);
695
991
  }
992
+ if (hadAnnotations) {
993
+ this.emitAnnotationsChanged(callbackContext);
994
+ }
696
995
  this.emitImageChanged(callbackContext);
697
996
  }
698
997
  getInternalOperationToken(options) {
@@ -717,15 +1016,11 @@ export class ImageEditor {
717
1016
  assertIdleForOperation(operationName, options) {
718
1017
  const token = this.getInternalOperationToken(options);
719
1018
  this.operationGuard.assertIdleForOperation(operationName, token);
720
- if (this.cropSession &&
1019
+ const activeToolMode = this.getActiveToolMode();
1020
+ if (activeToolMode &&
721
1021
  !this.operationGuard.isOwnOperation(token) &&
722
- !CROP_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
723
- throw new Error(`[ImageEditor] Cannot run "${operationName}" while crop mode is active.`);
724
- }
725
- if (this.mosaicSession &&
726
- !this.operationGuard.isOwnOperation(token) &&
727
- !MOSAIC_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
728
- throw new Error(`[ImageEditor] Cannot run "${operationName}" while mosaic mode is active.`);
1022
+ !TOOL_MODE_ALLOWED_OPERATIONS[activeToolMode].has(operationName)) {
1023
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" while ${activeToolMode} mode is active.`);
729
1024
  }
730
1025
  if (this.animQueue.isBusy() && !this.canRunDuringAnimationQueue(options)) {
731
1026
  throw new Error(`[ImageEditor] Cannot run "${operationName}" while an animation is queued.`);
@@ -758,10 +1053,7 @@ export class ImageEditor {
758
1053
  ((_b = this.originalImage.height) !== null && _b !== void 0 ? _b : 0) > 0);
759
1054
  }
760
1055
  isBusy() {
761
- return (this.operationGuard.isBusy() ||
762
- this.animQueue.isBusy() ||
763
- this.cropSession !== null ||
764
- this.mosaicSession !== null);
1056
+ return this.operationGuard.isBusy() || this.animQueue.isBusy() || this.isToolModeActive();
765
1057
  }
766
1058
  setLayoutMode(mode) {
767
1059
  if (!isLayoutMode(mode)) {
@@ -836,11 +1128,35 @@ export class ImageEditor {
836
1128
  return [];
837
1129
  return this.canvas.getObjects().filter(isMaskObject).slice();
838
1130
  }
1131
+ getAnnotations() {
1132
+ if (!this.canvas)
1133
+ return [];
1134
+ return getAnnotationsImpl(this.canvas);
1135
+ }
839
1136
  getMaskCollectionSignature() {
840
1137
  return this.getMasks()
841
1138
  .map((mask) => `${mask.maskId}:${mask.maskName}`)
842
1139
  .join('|');
843
1140
  }
1141
+ getAnnotationCollectionSignature() {
1142
+ return this.getAnnotations()
1143
+ .map((annotation) => `${annotation.annotationId}:${annotation.annotationName}`)
1144
+ .join('|');
1145
+ }
1146
+ getActiveToolMode() {
1147
+ if (this.cropSession)
1148
+ return 'crop';
1149
+ if (this.mosaicSession)
1150
+ return 'mosaic';
1151
+ if (this.textSession)
1152
+ return 'text';
1153
+ if (this.drawSession)
1154
+ return 'draw';
1155
+ return null;
1156
+ }
1157
+ isToolModeActive() {
1158
+ return this.getActiveToolMode() !== null;
1159
+ }
844
1160
  getEditorState() {
845
1161
  const canvasWidth = this.canvas ? this.canvas.getWidth() : 0;
846
1162
  const canvasHeight = this.canvas ? this.canvas.getHeight() : 0;
@@ -849,11 +1165,15 @@ export class ImageEditor {
849
1165
  hasImage: image !== null,
850
1166
  image,
851
1167
  maskCount: this.getMasks().length,
1168
+ annotationCount: this.getAnnotations().length,
852
1169
  currentScale: this.currentScale,
853
1170
  currentRotation: this.currentRotation,
854
1171
  isBusy: this.isBusy(),
1172
+ activeToolMode: this.getActiveToolMode(),
855
1173
  isCropMode: this.cropSession !== null,
856
1174
  isMosaicMode: this.mosaicSession !== null,
1175
+ isTextMode: this.textSession !== null,
1176
+ isDrawMode: this.drawSession !== null,
857
1177
  canUndo: this.historyManager.canUndo(),
858
1178
  canRedo: this.historyManager.canRedo(),
859
1179
  canvasWidth,
@@ -866,6 +1186,9 @@ export class ImageEditor {
866
1186
  emitMasksChanged(context) {
867
1187
  this.emitOptionCallback('onMasksChanged', [this.getMasks(), context]);
868
1188
  }
1189
+ emitAnnotationsChanged(context) {
1190
+ this.emitOptionCallback('onAnnotationsChanged', [this.getAnnotations(), context]);
1191
+ }
869
1192
  emitBusyChangeIfChanged(context) {
870
1193
  const isBusy = this.isBusy();
871
1194
  if (this.lastEmittedIsBusy === isBusy)
@@ -874,11 +1197,20 @@ export class ImageEditor {
874
1197
  this.emitOptionCallback('onBusyChange', [isBusy, context]);
875
1198
  }
876
1199
  buildSelection(selected) {
877
- var _a;
1200
+ var _a, _b;
878
1201
  const selectedMasks = selected.filter(isMaskObject);
1202
+ const selectedAnnotations = selected.filter(isAnnotationObject);
1203
+ const selectedObjectKind = selectedMasks.length === 1 && selectedAnnotations.length === 0
1204
+ ? 'mask'
1205
+ : selectedAnnotations.length === 1 && selectedMasks.length === 0
1206
+ ? 'annotation'
1207
+ : null;
879
1208
  return {
880
1209
  selectedMask: (_a = selectedMasks[0]) !== null && _a !== void 0 ? _a : null,
881
1210
  selectedMasks,
1211
+ selectedAnnotation: (_b = selectedAnnotations[0]) !== null && _b !== void 0 ? _b : null,
1212
+ selectedAnnotations,
1213
+ selectedObjectKind,
882
1214
  };
883
1215
  }
884
1216
  withSelectionChangeContext(context, callback) {
@@ -917,7 +1249,7 @@ export class ImageEditor {
917
1249
  applyCanvasDimensions(this.canvas, widthPx, heightPx, this.containerElement);
918
1250
  }
919
1251
  alignObjectBoundingBoxToCanvasTopLeft(object) {
920
- var _a, _b;
1252
+ var _a, _b, _c;
921
1253
  object.setCoords();
922
1254
  const boundingRect = object.getBoundingRect();
923
1255
  object.set({
@@ -925,7 +1257,7 @@ export class ImageEditor {
925
1257
  top: ((_b = object.top) !== null && _b !== void 0 ? _b : 0) - boundingRect.top,
926
1258
  });
927
1259
  object.setCoords();
928
- this.canvas.renderAll();
1260
+ (_c = this.canvas) === null || _c === void 0 ? void 0 : _c.renderAll();
929
1261
  }
930
1262
  measureLayoutViewport(scrollbarSize) {
931
1263
  return this.viewportCache.measure(this.containerElement, {
@@ -933,7 +1265,13 @@ export class ImageEditor {
933
1265
  height: this.options.canvasHeight,
934
1266
  }, scrollbarSize);
935
1267
  }
936
- updateCanvasSizeToImageBounds() {
1268
+ getScrollbarStableViewportCanvasSize(viewport) {
1269
+ return {
1270
+ width: Math.max(1, viewport.width - 1),
1271
+ height: Math.max(1, viewport.height - 1),
1272
+ };
1273
+ }
1274
+ updateCanvasSizeToImageBounds(options = {}) {
937
1275
  var _a, _b;
938
1276
  if (!this.originalImage)
939
1277
  return;
@@ -941,13 +1279,26 @@ export class ImageEditor {
941
1279
  const boundingRect = this.originalImage.getBoundingRect();
942
1280
  const scrollbarSize = measureScrollbarSize((_b = (_a = this.containerElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : null);
943
1281
  const viewport = this.measureLayoutViewport(scrollbarSize);
1282
+ const shouldStabilizeContainedViewport = options.stabilizeContainedViewport !== false;
1283
+ const imageFitsViewport = boundingRect.width <= viewport.width + LAYOUT_EPSILON &&
1284
+ boundingRect.height <= viewport.height + LAYOUT_EPSILON;
944
1285
  if (this.currentLayoutMode === 'fit' || this.currentLayoutMode === 'cover') {
1286
+ if (imageFitsViewport) {
1287
+ const canvasSize = shouldStabilizeContainedViewport
1288
+ ? this.getScrollbarStableViewportCanvasSize(viewport)
1289
+ : viewport;
1290
+ this.setCanvasSizePx(canvasSize.width, canvasSize.height);
1291
+ return;
1292
+ }
945
1293
  const canvasSize = computeScrollableCanvasSize(boundingRect.width, boundingRect.height, viewport, scrollbarSize);
946
1294
  this.setCanvasSizePx(canvasSize.width, canvasSize.height);
947
1295
  return;
948
1296
  }
949
- if (boundingRect.width <= viewport.width && boundingRect.height <= viewport.height) {
950
- this.setCanvasSizePx(viewport.width, viewport.height);
1297
+ if (imageFitsViewport) {
1298
+ const canvasSize = shouldStabilizeContainedViewport
1299
+ ? this.getScrollbarStableViewportCanvasSize(viewport)
1300
+ : viewport;
1301
+ this.setCanvasSizePx(canvasSize.width, canvasSize.height);
951
1302
  return;
952
1303
  }
953
1304
  this.setCanvasSizePx(Math.max(viewport.width, Math.ceil(boundingRect.width)), Math.max(viewport.height, Math.ceil(boundingRect.height)));
@@ -1047,7 +1398,7 @@ export class ImageEditor {
1047
1398
  }
1048
1399
  buildTransformContext() {
1049
1400
  return {
1050
- canvas: this.canvas,
1401
+ canvas: this.getLiveCanvasOrThrow('buildTransformContext'),
1051
1402
  options: this.options,
1052
1403
  guard: this.operationGuard,
1053
1404
  getOriginalImage: () => this.originalImage,
@@ -1199,6 +1550,7 @@ export class ImageEditor {
1199
1550
  const context = this.buildCallbackContext(activeRestoreOperation !== null && activeRestoreOperation !== void 0 ? activeRestoreOperation : 'loadFromState', activeRestoreOperation === 'undo' || activeRestoreOperation === 'redo');
1200
1551
  const previousImage = this.originalImage;
1201
1552
  const previousMaskSignature = this.getMaskCollectionSignature();
1553
+ const previousAnnotationSignature = this.getAnnotationCollectionSignature();
1202
1554
  try {
1203
1555
  const restoredState = await loadFromStateImpl({
1204
1556
  canvas: this.canvas,
@@ -1221,6 +1573,7 @@ export class ImageEditor {
1221
1573
  this.canvas.sendObjectToBack(this.originalImage);
1222
1574
  }
1223
1575
  this.maskCounter = restoredState.maxMaskId;
1576
+ this.annotationCounter = restoredState.maxAnnotationId;
1224
1577
  const editorState = restoredState.editorState;
1225
1578
  if (editorState) {
1226
1579
  this.currentScale = editorState.currentScale;
@@ -1238,22 +1591,25 @@ export class ImageEditor {
1238
1591
  }
1239
1592
  this.isImageLoadedToCanvas = !!this.originalImage;
1240
1593
  if (this.originalImage && this.shouldNormalizeCanvasSizeAfterStateRestore()) {
1241
- this.updateCanvasSizeToImageBounds();
1594
+ this.updateCanvasSizeToImageBounds({ stabilizeContainedViewport: false });
1242
1595
  this.alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
1243
1596
  }
1244
1597
  if (this.originalImage) {
1245
1598
  this.settleFitCoverScrollbarsAfterStateRestore();
1246
1599
  }
1247
- const restoredMasks = restoredState.objects.filter(isMaskObject);
1600
+ const restoredMasks = restoredState.masks;
1248
1601
  this.lastMask = restoredMasks.reduce((lastMask, maskObject) => !lastMask || maskObject.maskId > lastMask.maskId ? maskObject : lastMask, null);
1249
1602
  restoredMasks.forEach((maskObject) => {
1250
1603
  applyMaskUnselectedStyle(maskObject);
1251
1604
  reattachMaskHoverHandlers(maskObject);
1252
1605
  });
1606
+ syncAnnotationRuntimeStates(restoredState.annotations);
1607
+ attachTextEditingHandlersToAnnotations(this.buildTextControllerContext(), restoredState.annotations);
1253
1608
  this.lastSnapshot = this.captureSnapshotInternal();
1254
1609
  this.canvas.renderAll();
1255
1610
  this.updateInputs();
1256
1611
  this.updateMaskList();
1612
+ this.updateAnnotationList();
1257
1613
  this.updateUi();
1258
1614
  if (previousImage && previousImage !== this.originalImage) {
1259
1615
  this.emitOptionCallback('onImageCleared', [previousImage, context]);
@@ -1261,17 +1617,32 @@ export class ImageEditor {
1261
1617
  if (previousMaskSignature !== this.getMaskCollectionSignature()) {
1262
1618
  this.emitMasksChanged(context);
1263
1619
  }
1620
+ if (previousAnnotationSignature !== this.getAnnotationCollectionSignature()) {
1621
+ this.emitAnnotationsChanged(context);
1622
+ }
1264
1623
  this.emitImageChanged(context);
1624
+ const canvas = this.getLiveCanvasOrThrow('loadFromState');
1265
1625
  const activeMaskId = editorState === null || editorState === void 0 ? void 0 : editorState.activeMaskId;
1266
- if (typeof activeMaskId === 'number') {
1626
+ const activeAnnotationId = editorState === null || editorState === void 0 ? void 0 : editorState.activeAnnotationId;
1627
+ if ((editorState === null || editorState === void 0 ? void 0 : editorState.activeObjectKind) === 'mask' && typeof activeMaskId === 'number') {
1267
1628
  const activeMask = restoredMasks.find((maskObject) => maskObject.maskId === activeMaskId);
1268
1629
  if (activeMask) {
1269
1630
  this.withSelectionChangeContext(context, () => {
1270
- this.canvas.setActiveObject(activeMask);
1631
+ canvas.setActiveObject(activeMask);
1271
1632
  this.handleSelectionChanged([activeMask]);
1272
1633
  });
1273
1634
  }
1274
1635
  }
1636
+ else if ((editorState === null || editorState === void 0 ? void 0 : editorState.activeObjectKind) === 'annotation' &&
1637
+ typeof activeAnnotationId === 'number') {
1638
+ const activeAnnotation = restoredState.annotations.find((annotation) => annotation.annotationId === activeAnnotationId);
1639
+ if (activeAnnotation) {
1640
+ this.withSelectionChangeContext(context, () => {
1641
+ canvas.setActiveObject(activeAnnotation);
1642
+ this.handleSelectionChanged([activeAnnotation]);
1643
+ });
1644
+ }
1645
+ }
1275
1646
  }
1276
1647
  catch (error) {
1277
1648
  reportError(this.options, error, 'Failed to restore canvas state.');
@@ -1282,24 +1653,26 @@ export class ImageEditor {
1282
1653
  this.saveStateInternal();
1283
1654
  }
1284
1655
  saveStateInternal(options) {
1285
- var _a, _b;
1656
+ var _a, _b, _c;
1286
1657
  if (!this.canvas || this.shouldSuppressSaveState)
1287
1658
  return;
1288
1659
  if (!this.canRunIdleOperation('saveState', options))
1289
1660
  return;
1290
1661
  const activeObj = this.canvas.getActiveObject();
1291
1662
  const activeMask = this.getActiveMaskForSnapshot();
1663
+ const activeAnnotation = this.getActiveAnnotationForSnapshot();
1292
1664
  this.hideAllMaskLabels();
1293
1665
  try {
1294
1666
  const after = saveStateImpl({
1295
1667
  canvas: this.canvas,
1296
1668
  activeMaskId: (_a = activeMask === null || activeMask === void 0 ? void 0 : activeMask.maskId) !== null && _a !== void 0 ? _a : null,
1669
+ activeAnnotationId: (_b = activeAnnotation === null || activeAnnotation === void 0 ? void 0 : activeAnnotation.annotationId) !== null && _b !== void 0 ? _b : null,
1297
1670
  currentScale: this.currentScale,
1298
1671
  currentRotation: this.currentRotation,
1299
1672
  baseImageScale: this.baseImageScale,
1300
1673
  currentImageMimeType: this.currentImageMimeType,
1301
1674
  });
1302
- const before = (_b = this.lastSnapshot) !== null && _b !== void 0 ? _b : after;
1675
+ const before = (_c = this.lastSnapshot) !== null && _c !== void 0 ? _c : after;
1303
1676
  if (after === before) {
1304
1677
  return;
1305
1678
  }
@@ -1315,25 +1688,32 @@ export class ImageEditor {
1315
1688
  reportWarning(this.options, error, 'Failed to capture canvas snapshot.');
1316
1689
  }
1317
1690
  finally {
1318
- this.restoreActiveMaskAfterSnapshot(activeObj, activeMask);
1691
+ this.restoreActiveObjectAfterSnapshot(activeObj, activeMask, activeAnnotation);
1319
1692
  this.updateUi();
1320
1693
  }
1321
1694
  }
1322
- restoreActiveMaskAfterSnapshot(activeObj, activeMask) {
1695
+ restoreActiveObjectAfterSnapshot(activeObj, activeMask, activeAnnotation) {
1323
1696
  if (!this.canvas)
1324
1697
  return;
1325
1698
  const maskToRestore = activeObj && isMaskObject(activeObj) ? activeObj : activeMask;
1326
- if (!maskToRestore || !this.canvas.getObjects().includes(maskToRestore))
1699
+ const annotationToRestore = activeObj && isAnnotationObject(activeObj) ? activeObj : activeAnnotation;
1700
+ if (maskToRestore && this.canvas.getObjects().includes(maskToRestore)) {
1701
+ this.canvas.setActiveObject(maskToRestore);
1702
+ this.showLabelForMask(maskToRestore);
1703
+ this.updateMaskListSelection(maskToRestore);
1327
1704
  return;
1328
- this.canvas.setActiveObject(maskToRestore);
1329
- this.showLabelForMask(maskToRestore);
1330
- this.updateMaskListSelection(maskToRestore);
1705
+ }
1706
+ if (annotationToRestore && this.canvas.getObjects().includes(annotationToRestore)) {
1707
+ this.canvas.setActiveObject(annotationToRestore);
1708
+ this.updateAnnotationListSelection(annotationToRestore);
1709
+ }
1331
1710
  }
1332
1711
  undo() {
1333
1712
  if (this.isDisposed)
1334
1713
  return Promise.resolve();
1335
1714
  if (!this.canRunIdleOperation('undo'))
1336
1715
  return Promise.resolve();
1716
+ this.finalizeActiveTextEditingIfNeeded();
1337
1717
  const context = this.buildCallbackContext('undo', true);
1338
1718
  const job = this.animQueue.add(async () => {
1339
1719
  if (this.isDisposed)
@@ -1357,6 +1737,7 @@ export class ImageEditor {
1357
1737
  return Promise.resolve();
1358
1738
  if (!this.canRunIdleOperation('redo'))
1359
1739
  return Promise.resolve();
1740
+ this.finalizeActiveTextEditingIfNeeded();
1360
1741
  const context = this.buildCallbackContext('redo', true);
1361
1742
  const job = this.animQueue.add(async () => {
1362
1743
  if (this.isDisposed)
@@ -1422,7 +1803,7 @@ export class ImageEditor {
1422
1803
  buildCreateMaskContext() {
1423
1804
  return {
1424
1805
  fabric: this.fabricModule,
1425
- canvas: this.canvas,
1806
+ canvas: this.getLiveCanvasOrThrow('createMask'),
1426
1807
  options: this.getRuntimeOptions(),
1427
1808
  getLastMask: () => this.lastMask,
1428
1809
  setLastMask: (maskObject) => {
@@ -1445,7 +1826,7 @@ export class ImageEditor {
1445
1826
  }
1446
1827
  buildRemoveMaskContext() {
1447
1828
  return {
1448
- canvas: this.canvas,
1829
+ canvas: this.getLiveCanvasOrThrow('removeMask'),
1449
1830
  removeLabelForMask: (mask) => {
1450
1831
  this.removeLabelForMask(mask);
1451
1832
  },
@@ -1495,11 +1876,35 @@ export class ImageEditor {
1495
1876
  return;
1496
1877
  showLabelForMask(context, mask);
1497
1878
  }
1879
+ handleObjectMovingScalingRotating(target) {
1880
+ if (isMaskObject(target)) {
1881
+ this.syncMaskLabel(target);
1882
+ }
1883
+ }
1884
+ handleObjectModified(target) {
1885
+ if (isMaskObject(target)) {
1886
+ this.syncMaskLabel(target);
1887
+ const context = this.buildCallbackContext('saveState', false);
1888
+ this.saveState();
1889
+ this.emitMasksChanged(context);
1890
+ this.emitImageChanged(context);
1891
+ return;
1892
+ }
1893
+ if (isAnnotationObject(target)) {
1894
+ if (isAnnotationLocked(target))
1895
+ return;
1896
+ const context = this.buildCallbackContext('updateAnnotation', false);
1897
+ this.saveState();
1898
+ this.emitAnnotationsChanged(context);
1899
+ this.emitImageChanged(context);
1900
+ }
1901
+ }
1498
1902
  handleSelectionChanged(selected) {
1499
- var _a, _b, _c;
1903
+ var _a, _b, _c, _d;
1500
1904
  if (!this.canvas)
1501
1905
  return;
1502
1906
  const selectedMask = (_a = selected.find(isMaskObject)) !== null && _a !== void 0 ? _a : null;
1907
+ const selectedAnnotation = (_b = selected.find(isAnnotationObject)) !== null && _b !== void 0 ? _b : null;
1503
1908
  const masks = this.canvas.getObjects().filter(isMaskObject);
1504
1909
  masks.forEach((maskObject) => {
1505
1910
  if (maskObject !== selectedMask) {
@@ -1515,9 +1920,10 @@ export class ImageEditor {
1515
1920
  if (selectedMask)
1516
1921
  this.showLabelForMask(selectedMask);
1517
1922
  this.updateMaskListSelection(selectedMask);
1923
+ this.updateAnnotationListSelection(selectedAnnotation);
1518
1924
  this.canvas.requestRenderAll();
1519
1925
  this.updateUi();
1520
- const context = (_b = this.nextSelectionChangeContext) !== null && _b !== void 0 ? _b : this.buildCallbackContext((_c = this.activeStateRestoreOperation) !== null && _c !== void 0 ? _c : 'createMask', this.activeStateRestoreOperation === 'undo' ||
1926
+ const context = (_c = this.nextSelectionChangeContext) !== null && _c !== void 0 ? _c : this.buildCallbackContext((_d = this.activeStateRestoreOperation) !== null && _d !== void 0 ? _d : 'createMask', this.activeStateRestoreOperation === 'undo' ||
1521
1927
  this.activeStateRestoreOperation === 'redo');
1522
1928
  this.emitOptionCallback('onSelectionChange', [this.buildSelection(selected), context]);
1523
1929
  }
@@ -1534,11 +1940,426 @@ export class ImageEditor {
1534
1940
  updateMaskListSelection(selectedMask) {
1535
1941
  updateMaskListSelection(this.buildMaskListContext(), selectedMask);
1536
1942
  }
1943
+ enterTextMode() {
1944
+ if (!this.canvas)
1945
+ return;
1946
+ if (!this.canRunIdleOperation('enterTextMode'))
1947
+ return;
1948
+ if (this.isToolModeActive())
1949
+ return;
1950
+ enterTextModeImpl(this.buildTextControllerContext());
1951
+ const callbackContext = this.buildCallbackContext('enterTextMode', false);
1952
+ this.emitBusyChangeIfChanged(callbackContext);
1953
+ this.emitImageChanged(callbackContext);
1954
+ }
1955
+ exitTextMode() {
1956
+ if (!this.canvas || !this.textSession)
1957
+ return;
1958
+ if (!this.canRunIdleOperation('exitTextMode'))
1959
+ return;
1960
+ exitTextModeImpl(this.buildTextControllerContext());
1961
+ const callbackContext = this.buildCallbackContext('exitTextMode', false);
1962
+ this.emitBusyChangeIfChanged(callbackContext);
1963
+ this.emitImageChanged(callbackContext);
1964
+ }
1965
+ isTextMode() {
1966
+ return this.textSession !== null;
1967
+ }
1968
+ createTextAnnotation(config = {}) {
1969
+ if (!this.canvas)
1970
+ return null;
1971
+ if (!this.canRunIdleOperation('createTextAnnotation'))
1972
+ return null;
1973
+ return createTextAnnotationImpl(this.buildTextControllerContext(), config);
1974
+ }
1975
+ enterDrawMode() {
1976
+ if (!this.canvas)
1977
+ return;
1978
+ if (!this.canRunIdleOperation('enterDrawMode'))
1979
+ return;
1980
+ if (this.isToolModeActive())
1981
+ return;
1982
+ enterDrawModeImpl(this.buildDrawControllerContext());
1983
+ const callbackContext = this.buildCallbackContext('enterDrawMode', false);
1984
+ this.emitBusyChangeIfChanged(callbackContext);
1985
+ this.emitImageChanged(callbackContext);
1986
+ }
1987
+ exitDrawMode() {
1988
+ if (!this.canvas || !this.drawSession)
1989
+ return;
1990
+ if (!this.canRunIdleOperation('exitDrawMode'))
1991
+ return;
1992
+ exitDrawModeImpl(this.buildDrawControllerContext());
1993
+ const callbackContext = this.buildCallbackContext('exitDrawMode', false);
1994
+ this.emitBusyChangeIfChanged(callbackContext);
1995
+ this.emitImageChanged(callbackContext);
1996
+ }
1997
+ isDrawMode() {
1998
+ return this.drawSession !== null;
1999
+ }
2000
+ getTextConfig() {
2001
+ return cloneResolvedTextAnnotationConfig(this.currentTextConfig);
2002
+ }
2003
+ setTextConfig(config) {
2004
+ this.applyTextConfigPatch(config, 'setTextConfig');
2005
+ }
2006
+ resetTextConfig() {
2007
+ this.applyTextConfigPatch(this.defaultTextConfig, 'resetTextConfig');
2008
+ }
2009
+ setTextColor(color) {
2010
+ this.applyTextConfigPatch({ fill: color }, 'setTextColor');
2011
+ }
2012
+ setTextFontSize(size) {
2013
+ this.applyTextConfigPatch({ fontSize: size }, 'setTextFontSize');
2014
+ }
2015
+ getDrawConfig() {
2016
+ return cloneResolvedDrawConfig(this.currentDrawConfig);
2017
+ }
2018
+ setDrawConfig(config) {
2019
+ this.applyDrawConfigPatch(config, 'setDrawConfig');
2020
+ }
2021
+ resetDrawConfig() {
2022
+ this.applyDrawConfigPatch(this.defaultDrawConfig, 'resetDrawConfig');
2023
+ }
2024
+ setDrawColor(color) {
2025
+ this.applyDrawConfigPatch({ color }, 'setDrawColor');
2026
+ }
2027
+ setDrawBrushSize(size) {
2028
+ this.applyDrawConfigPatch({ brushSize: size }, 'setDrawBrushSize');
2029
+ }
2030
+ removeSelectedAnnotation() {
2031
+ if (!this.canvas)
2032
+ return;
2033
+ if (!this.canRunIdleOperation('removeSelectedAnnotation'))
2034
+ return;
2035
+ const before = this.getAnnotations().length;
2036
+ const callbackContext = this.buildCallbackContext('removeSelectedAnnotation', false);
2037
+ this.withSelectionChangeContext(callbackContext, () => {
2038
+ removeSelectedAnnotationImpl(this.buildAnnotationManagerContext());
2039
+ });
2040
+ this.updateAnnotationList();
2041
+ this.updateUi();
2042
+ if (this.getAnnotations().length !== before) {
2043
+ this.emitAnnotationsChanged(callbackContext);
2044
+ this.emitImageChanged(callbackContext);
2045
+ }
2046
+ }
2047
+ removeAllAnnotations(options = {}) {
2048
+ if (!this.canvas)
2049
+ return;
2050
+ if (!this.canRunIdleOperation('removeAllAnnotations', options))
2051
+ return;
2052
+ const before = this.getAnnotations().length;
2053
+ const callbackContext = this.buildCallbackContext('removeAllAnnotations', false);
2054
+ this.withSelectionChangeContext(callbackContext, () => {
2055
+ removeAllAnnotationsImpl(this.buildAnnotationManagerContext(), options);
2056
+ });
2057
+ this.updateAnnotationList();
2058
+ this.updateUi();
2059
+ if (this.getAnnotations().length !== before) {
2060
+ this.emitAnnotationsChanged(callbackContext);
2061
+ this.emitImageChanged(callbackContext);
2062
+ }
2063
+ }
2064
+ updateAnnotation(annotationId, config) {
2065
+ if (!this.canvas)
2066
+ return;
2067
+ if (!this.canRunIdleOperation('updateAnnotation'))
2068
+ return;
2069
+ const callbackContext = this.buildCallbackContext('updateAnnotation', false);
2070
+ const changed = updateAnnotationImpl(this.buildAnnotationManagerContext(), annotationId, config);
2071
+ if (changed) {
2072
+ this.updateAnnotationList();
2073
+ this.emitAnnotationsChanged(callbackContext);
2074
+ this.emitImageChanged(callbackContext);
2075
+ }
2076
+ }
2077
+ updateSelectedAnnotation(config) {
2078
+ if (!this.canvas)
2079
+ return;
2080
+ if (!this.canRunIdleOperation('updateSelectedAnnotation'))
2081
+ return;
2082
+ const callbackContext = this.buildCallbackContext('updateSelectedAnnotation', false);
2083
+ const changed = updateSelectedAnnotationImpl(this.buildAnnotationManagerContext(), config);
2084
+ if (changed) {
2085
+ this.updateAnnotationList();
2086
+ this.emitAnnotationsChanged(callbackContext);
2087
+ this.emitImageChanged(callbackContext);
2088
+ }
2089
+ }
2090
+ deleteSelectedObject() {
2091
+ if (!this.canvas)
2092
+ return;
2093
+ if (!this.canRunIdleOperation('deleteSelectedObject'))
2094
+ return;
2095
+ this.finalizeActiveTextEditingIfNeeded();
2096
+ const selectedObjects = this.getSelectedCanvasObjects();
2097
+ const selectedMasks = selectedObjects.filter(isMaskObject);
2098
+ const selectedAnnotations = selectedObjects.filter((object) => isAnnotationObject(object) && isAnnotationUnlocked(object));
2099
+ if (selectedMasks.length === 0 && selectedAnnotations.length === 0)
2100
+ return;
2101
+ const canvas = this.getLiveCanvasOrThrow('deleteSelectedObject');
2102
+ const callbackContext = this.buildCallbackContext('deleteSelectedObject', false);
2103
+ this.withSelectionChangeContext(callbackContext, () => {
2104
+ for (const mask of selectedMasks) {
2105
+ this.removeLabelForMask(mask);
2106
+ canvas.remove(mask);
2107
+ }
2108
+ removeAnnotationObjects(this.buildAnnotationManagerContext(), selectedAnnotations, {
2109
+ saveHistory: false,
2110
+ force: true,
2111
+ });
2112
+ canvas.discardActiveObject();
2113
+ canvas.renderAll();
2114
+ this.saveState();
2115
+ });
2116
+ this.updateMaskList();
2117
+ this.updateAnnotationList();
2118
+ this.updateUi();
2119
+ if (selectedMasks.length > 0)
2120
+ this.emitMasksChanged(callbackContext);
2121
+ if (selectedAnnotations.length > 0)
2122
+ this.emitAnnotationsChanged(callbackContext);
2123
+ this.emitImageChanged(callbackContext);
2124
+ }
2125
+ bringSelectedObjectForward() {
2126
+ this.moveSelectedEditableObject('bringSelectedObjectForward');
2127
+ }
2128
+ sendSelectedObjectBackward() {
2129
+ this.moveSelectedEditableObject('sendSelectedObjectBackward');
2130
+ }
2131
+ bringSelectedObjectToFront() {
2132
+ this.moveSelectedEditableObject('bringSelectedObjectToFront');
2133
+ }
2134
+ sendSelectedObjectToBack() {
2135
+ this.moveSelectedEditableObject('sendSelectedObjectToBack');
2136
+ }
2137
+ buildAnnotationManagerContext() {
2138
+ return {
2139
+ canvas: this.getLiveCanvasOrThrow('annotationManager'),
2140
+ saveCanvasState: () => this.saveState(),
2141
+ updateUi: () => this.updateUi(),
2142
+ };
2143
+ }
2144
+ buildAnnotationListContext() {
2145
+ return {
2146
+ canvas: this.canvas,
2147
+ getListElementId: () => this.elements.annotationList,
2148
+ onAnnotationSelected: (annotation) => this.handleSelectionChanged([annotation]),
2149
+ };
2150
+ }
2151
+ updateAnnotationList() {
2152
+ renderAnnotationList(this.buildAnnotationListContext());
2153
+ }
2154
+ updateAnnotationListSelection(selectedAnnotation) {
2155
+ updateAnnotationListSelection(this.buildAnnotationListContext(), selectedAnnotation);
2156
+ }
2157
+ buildTextControllerContext() {
2158
+ return {
2159
+ fabric: this.fabricModule,
2160
+ canvas: this.getLiveCanvasOrThrow('textController'),
2161
+ options: this.options,
2162
+ getOriginalImage: () => this.originalImage,
2163
+ getTextConfig: () => this.currentTextConfig,
2164
+ isImageLoaded: () => this.isImageLoaded(),
2165
+ getAnnotationCounter: () => this.annotationCounter,
2166
+ setAnnotationCounter: (value) => {
2167
+ this.annotationCounter = value;
2168
+ },
2169
+ getTextSession: () => this.textSession,
2170
+ setTextSession: (session) => {
2171
+ this.textSession = session;
2172
+ },
2173
+ saveCanvasState: () => this.saveState(),
2174
+ updateAnnotationList: () => this.updateAnnotationList(),
2175
+ updateUi: () => this.updateUi(),
2176
+ emitAnnotationsChanged: (context) => this.emitAnnotationsChanged(context),
2177
+ emitImageChanged: (context) => this.emitImageChanged(context),
2178
+ buildCallbackContext: (operation) => this.buildCallbackContext(operation, false),
2179
+ };
2180
+ }
2181
+ buildDrawControllerContext() {
2182
+ return {
2183
+ fabric: this.fabricModule,
2184
+ canvas: this.getLiveCanvasOrThrow('drawController'),
2185
+ options: this.options,
2186
+ getDrawConfig: () => this.currentDrawConfig,
2187
+ isImageLoaded: () => this.isImageLoaded(),
2188
+ getAnnotationCounter: () => this.annotationCounter,
2189
+ setAnnotationCounter: (value) => {
2190
+ this.annotationCounter = value;
2191
+ },
2192
+ getDrawSession: () => this.drawSession,
2193
+ setDrawSession: (session) => {
2194
+ this.drawSession = session;
2195
+ },
2196
+ saveCanvasState: () => this.saveState(),
2197
+ updateAnnotationList: () => this.updateAnnotationList(),
2198
+ updateUi: () => this.updateUi(),
2199
+ emitAnnotationsChanged: (context) => this.emitAnnotationsChanged(context),
2200
+ emitImageChanged: (context) => this.emitImageChanged(context),
2201
+ buildCallbackContext: (operation) => this.buildCallbackContext(operation, false),
2202
+ };
2203
+ }
2204
+ applyTextConfigPatch(config, operation) {
2205
+ if (!this.canRunIdleOperation(operation))
2206
+ return;
2207
+ const invalidFields = getInvalidTextAnnotationConfigFields(config);
2208
+ if (invalidFields.length > 0) {
2209
+ reportWarning(this.options, null, `${operation} ignored invalid Text config fields: ${invalidFields.join(', ')}.`);
2210
+ }
2211
+ const next = mergeTextAnnotationConfigPatch(this.currentTextConfig, config, this.defaultTextConfig);
2212
+ if (areResolvedTextAnnotationConfigsEqual(this.currentTextConfig, next))
2213
+ return;
2214
+ this.currentTextConfig = next;
2215
+ this.updateInputs();
2216
+ this.updateUi();
2217
+ this.emitImageChanged(this.buildCallbackContext(operation, false));
2218
+ }
2219
+ applyDrawConfigPatch(config, operation) {
2220
+ if (!this.canRunIdleOperation(operation))
2221
+ return;
2222
+ const invalidFields = getInvalidDrawConfigFields(config);
2223
+ if (invalidFields.length > 0) {
2224
+ reportWarning(this.options, null, `${operation} ignored invalid Draw config fields: ${invalidFields.join(', ')}.`);
2225
+ }
2226
+ const next = mergeDrawConfigPatch(this.currentDrawConfig, config, this.defaultDrawConfig);
2227
+ if (areResolvedDrawConfigsEqual(this.currentDrawConfig, next))
2228
+ return;
2229
+ this.currentDrawConfig = next;
2230
+ updateDrawBrush(this.buildDrawControllerContext());
2231
+ this.updateInputs();
2232
+ this.updateUi();
2233
+ this.emitImageChanged(this.buildCallbackContext(operation, false));
2234
+ }
2235
+ applyTextColorInput(color) {
2236
+ var _a;
2237
+ if (this.isTextMode()) {
2238
+ this.setTextColor(color);
2239
+ return;
2240
+ }
2241
+ const selected = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
2242
+ if (selected && isTextAnnotationObject(selected)) {
2243
+ this.updateSelectedAnnotation({ fill: color });
2244
+ return;
2245
+ }
2246
+ this.setTextColor(color);
2247
+ }
2248
+ applyTextFontSizeInput(size) {
2249
+ var _a;
2250
+ if (this.isTextMode()) {
2251
+ this.setTextFontSize(size);
2252
+ return;
2253
+ }
2254
+ const selected = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
2255
+ if (selected && isTextAnnotationObject(selected)) {
2256
+ this.updateSelectedAnnotation({ fontSize: size });
2257
+ return;
2258
+ }
2259
+ this.setTextFontSize(size);
2260
+ }
2261
+ applyDrawColorInput(color) {
2262
+ var _a;
2263
+ if (this.isDrawMode()) {
2264
+ this.setDrawColor(color);
2265
+ return;
2266
+ }
2267
+ const selected = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
2268
+ if (selected && isDrawAnnotationObject(selected)) {
2269
+ this.updateSelectedAnnotation({ stroke: color });
2270
+ return;
2271
+ }
2272
+ this.setDrawColor(color);
2273
+ }
2274
+ applyDrawBrushSizeInput(size) {
2275
+ var _a;
2276
+ if (this.isDrawMode()) {
2277
+ this.setDrawBrushSize(size);
2278
+ return;
2279
+ }
2280
+ const selected = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
2281
+ if (selected && isDrawAnnotationObject(selected)) {
2282
+ this.updateSelectedAnnotation({ strokeWidth: size });
2283
+ return;
2284
+ }
2285
+ this.setDrawBrushSize(size);
2286
+ }
2287
+ getSelectedCanvasObjects() {
2288
+ var _a, _b, _c;
2289
+ if (!this.canvas)
2290
+ return [];
2291
+ const activeObject = this.canvas.getActiveObject();
2292
+ if (!activeObject)
2293
+ return [];
2294
+ const type = typeof activeObject.type === 'string' ? activeObject.type.toLowerCase() : '';
2295
+ const isActiveSelection = type === 'activeselection' ||
2296
+ ((_c = (_b = (_a = activeObject).isType) === null || _b === void 0 ? void 0 : _b.call(_a, 'ActiveSelection')) !== null && _c !== void 0 ? _c : false);
2297
+ if (!isActiveSelection)
2298
+ return [activeObject];
2299
+ const getObjects = activeObject
2300
+ .getObjects;
2301
+ return typeof getObjects === 'function' ? getObjects.call(activeObject) : [];
2302
+ }
2303
+ moveSelectedEditableObject(operation) {
2304
+ if (!this.canvas)
2305
+ return;
2306
+ if (!this.canRunIdleOperation(operation))
2307
+ return;
2308
+ const selected = this.getSelectedCanvasObjects().filter(isEditableOverlayObject);
2309
+ if (selected.length !== 1) {
2310
+ if (selected.length > 1) {
2311
+ reportWarning(this.options, null, `${operation} skipped: ActiveSelection layer moves are not supported.`);
2312
+ }
2313
+ return;
2314
+ }
2315
+ const object = selected[0];
2316
+ const range = getEditableOverlayRange(this.canvas);
2317
+ const overlays = range.overlays;
2318
+ const currentOverlayIndex = overlays.indexOf(object);
2319
+ if (currentOverlayIndex < 0)
2320
+ return;
2321
+ let nextOverlayIndex = currentOverlayIndex;
2322
+ if (operation === 'bringSelectedObjectForward') {
2323
+ nextOverlayIndex = Math.min(overlays.length - 1, currentOverlayIndex + 1);
2324
+ }
2325
+ else if (operation === 'sendSelectedObjectBackward') {
2326
+ nextOverlayIndex = Math.max(0, currentOverlayIndex - 1);
2327
+ }
2328
+ else if (operation === 'bringSelectedObjectToFront') {
2329
+ nextOverlayIndex = overlays.length - 1;
2330
+ }
2331
+ else if (operation === 'sendSelectedObjectToBack') {
2332
+ nextOverlayIndex = 0;
2333
+ }
2334
+ if (nextOverlayIndex === currentOverlayIndex)
2335
+ return;
2336
+ const reordered = overlays.slice();
2337
+ reordered.splice(currentOverlayIndex, 1);
2338
+ reordered.splice(nextOverlayIndex, 0, object);
2339
+ reordered.forEach((overlay, index) => {
2340
+ var _a, _b;
2341
+ (_b = (_a = this.canvas).moveObjectTo) === null || _b === void 0 ? void 0 : _b.call(_a, overlay, range.start + index);
2342
+ });
2343
+ normalizeLayerOrder(this.canvas);
2344
+ this.canvas.setActiveObject(object);
2345
+ this.canvas.renderAll();
2346
+ this.saveState();
2347
+ this.updateMaskList();
2348
+ this.updateAnnotationList();
2349
+ this.updateUi();
2350
+ const context = this.buildCallbackContext(operation, false);
2351
+ if (isMaskObject(object))
2352
+ this.emitMasksChanged(context);
2353
+ if (isAnnotationObject(object))
2354
+ this.emitAnnotationsChanged(context);
2355
+ this.emitImageChanged(context);
2356
+ }
1537
2357
  async mergeMasks() {
1538
2358
  if (!this.canvas)
1539
2359
  return;
1540
2360
  if (!this.canRunIdleOperation('mergeMasks'))
1541
2361
  return;
2362
+ this.finalizeActiveTextEditingIfNeeded();
1542
2363
  const hasMasks = this.canvas.getObjects().some(isMaskObject);
1543
2364
  if (!hasMasks)
1544
2365
  return;
@@ -1551,7 +2372,11 @@ export class ImageEditor {
1551
2372
  await mergeMasksImpl(mergeMasksContext);
1552
2373
  this.updateInputs();
1553
2374
  this.updateMaskList();
2375
+ this.updateAnnotationList();
1554
2376
  this.emitMasksChanged(callbackContext);
2377
+ if (this.getAnnotations().length > 0) {
2378
+ this.emitAnnotationsChanged(callbackContext);
2379
+ }
1555
2380
  this.emitImageChanged(callbackContext);
1556
2381
  }
1557
2382
  finally {
@@ -1560,17 +2385,18 @@ export class ImageEditor {
1560
2385
  this.updateUi();
1561
2386
  }
1562
2387
  }
1563
- downloadImage(fileName) {
2388
+ downloadImage(options) {
1564
2389
  if (!this.canvas)
1565
2390
  return;
1566
2391
  if (!this.canRunIdleOperation('downloadImage'))
1567
2392
  return;
2393
+ this.finalizeActiveTextEditingIfNeeded();
1568
2394
  const callbackContext = this.buildCallbackContext('downloadImage', false);
1569
2395
  const operationToken = this.operationGuard.beginBusyOperation('downloadImage');
1570
2396
  this.emitBusyChangeIfChanged(callbackContext);
1571
2397
  const exportContext = this.buildExportServiceContext();
1572
2398
  try {
1573
- downloadImageImpl(exportContext, fileName);
2399
+ downloadImageImpl(exportContext, options);
1574
2400
  }
1575
2401
  finally {
1576
2402
  this.operationGuard.endBusyOperation(operationToken);
@@ -1582,6 +2408,7 @@ export class ImageEditor {
1582
2408
  return '';
1583
2409
  if (!this.canRunIdleOperation('exportImageBase64', options))
1584
2410
  return '';
2411
+ this.finalizeActiveTextEditingIfNeeded();
1585
2412
  const callbackContext = this.buildCallbackContext('exportImageBase64', false);
1586
2413
  const operationToken = this.operationGuard.beginBusyOperation('exportImageBase64');
1587
2414
  this.emitBusyChangeIfChanged(callbackContext);
@@ -1596,6 +2423,7 @@ export class ImageEditor {
1596
2423
  }
1597
2424
  async exportImageFile(options) {
1598
2425
  this.assertIdleForOperation('exportImageFile', options);
2426
+ this.finalizeActiveTextEditingIfNeeded();
1599
2427
  const callbackContext = this.buildCallbackContext('exportImageFile', false);
1600
2428
  const operationToken = this.operationGuard.beginBusyOperation('exportImageFile');
1601
2429
  this.emitBusyChangeIfChanged(callbackContext);
@@ -1611,7 +2439,7 @@ export class ImageEditor {
1611
2439
  buildExportServiceContext() {
1612
2440
  return {
1613
2441
  fabric: this.fabricModule,
1614
- canvas: this.canvas,
2442
+ canvas: this.getLiveCanvasOrThrow('export'),
1615
2443
  options: this.options,
1616
2444
  isImageLoaded: () => this.isImageLoaded(),
1617
2445
  getOriginalImage: () => this.originalImage,
@@ -1627,24 +2455,74 @@ export class ImageEditor {
1627
2455
  await this.loadImageInternal(base64, this.withInternalOperationOptions(operationToken, providedOptions !== null && providedOptions !== void 0 ? providedOptions : {}));
1628
2456
  this.restoreMergedImageDisplayGeometry(geometry);
1629
2457
  },
1630
- saveState: () => this.captureSnapshotInternal(),
2458
+ captureSnapshot: () => this.captureSnapshotInternal(),
1631
2459
  loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withInternalOperationOptions(operationToken, this.withAnimationQueueBypass())),
2460
+ exportImageBase64: (options) => exportImageBase64Impl(this.buildExportServiceContext(), options),
2461
+ updateUi: () => this.updateUi(),
2462
+ updateInputs: () => this.updateInputs(),
1632
2463
  removeAllMasksNoHistory: () => {
1633
2464
  const context = this.buildRemoveMaskContext();
1634
2465
  removeAllMasksImpl(context, { saveHistory: false });
1635
2466
  },
2467
+ getAnnotations: () => this.getAnnotations(),
2468
+ restoreAnnotations: (objects) => {
2469
+ const canvas = this.getLiveCanvasOrThrow('restoreAnnotations');
2470
+ objects.forEach((annotation) => {
2471
+ canvas.add(annotation);
2472
+ });
2473
+ syncAnnotationRuntimeStates(objects);
2474
+ attachTextEditingHandlersToAnnotations(this.buildTextControllerContext(), objects);
2475
+ this.annotationCounter = Math.max(this.annotationCounter, ...objects.map((annotation) => annotation.annotationId), 0);
2476
+ this.updateAnnotationList();
2477
+ },
2478
+ };
2479
+ }
2480
+ buildMergeAnnotationsContext(operationToken) {
2481
+ return {
2482
+ ...this.buildExportServiceContext(),
2483
+ historyManager: this.historyManager,
2484
+ containerElement: this.containerElement,
2485
+ loadImage: async (base64, providedOptions) => {
2486
+ const geometry = this.captureImageDisplayGeometry();
2487
+ await this.loadImageInternal(base64, this.withInternalOperationOptions(operationToken, providedOptions !== null && providedOptions !== void 0 ? providedOptions : {}));
2488
+ this.restoreMergedImageDisplayGeometry(geometry);
2489
+ },
2490
+ captureSnapshot: () => this.captureSnapshotInternal(),
2491
+ loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withInternalOperationOptions(operationToken, this.withAnimationQueueBypass())),
2492
+ exportImageBase64: (options) => exportImageBase64Impl(this.buildExportServiceContext(), options),
2493
+ updateUi: () => this.updateUi(),
2494
+ updateInputs: () => this.updateInputs(),
2495
+ removeAllAnnotationsNoHistory: () => {
2496
+ removeAllAnnotationsImpl(this.buildAnnotationManagerContext(), {
2497
+ saveHistory: false,
2498
+ force: true,
2499
+ });
2500
+ },
2501
+ getMasks: () => this.getMasks(),
2502
+ restoreMasks: (objects) => {
2503
+ const canvas = this.getLiveCanvasOrThrow('restoreMasks');
2504
+ objects.forEach((mask) => {
2505
+ canvas.add(mask);
2506
+ reattachMaskHoverHandlers(mask);
2507
+ });
2508
+ this.lastMask = objects.reduce((lastMask, mask) => !lastMask || mask.maskId > lastMask.maskId ? mask : lastMask, null);
2509
+ this.maskCounter = Math.max(this.maskCounter, ...objects.map((mask) => mask.maskId), 0);
2510
+ this.updateMaskList();
2511
+ },
1636
2512
  };
1637
2513
  }
1638
2514
  captureSnapshotInternal() {
1639
- var _a;
2515
+ var _a, _b;
1640
2516
  if (!this.canvas) {
1641
2517
  throw new Error('[ImageEditor] Cannot capture canvas snapshot before init or after dispose.');
1642
2518
  }
1643
2519
  const activeMask = this.getActiveMaskForSnapshot();
2520
+ const activeAnnotation = this.getActiveAnnotationForSnapshot();
1644
2521
  this.hideAllMaskLabels();
1645
2522
  return saveStateImpl({
1646
2523
  canvas: this.canvas,
1647
2524
  activeMaskId: (_a = activeMask === null || activeMask === void 0 ? void 0 : activeMask.maskId) !== null && _a !== void 0 ? _a : null,
2525
+ activeAnnotationId: (_b = activeAnnotation === null || activeAnnotation === void 0 ? void 0 : activeAnnotation.annotationId) !== null && _b !== void 0 ? _b : null,
1648
2526
  currentScale: this.currentScale,
1649
2527
  currentRotation: this.currentRotation,
1650
2528
  baseImageScale: this.baseImageScale,
@@ -1663,6 +2541,12 @@ export class ImageEditor {
1663
2541
  .filter((object) => isMaskObject(object) && !!object.labelObject);
1664
2542
  return labeledMasks.length === 1 ? ((_a = labeledMasks[0]) !== null && _a !== void 0 ? _a : null) : null;
1665
2543
  }
2544
+ getActiveAnnotationForSnapshot() {
2545
+ if (!this.canvas)
2546
+ return null;
2547
+ const activeObject = this.canvas.getActiveObject();
2548
+ return activeObject && isAnnotationObject(activeObject) ? activeObject : null;
2549
+ }
1666
2550
  enterMosaicMode() {
1667
2551
  if (!this.canvas || !this.originalImage)
1668
2552
  return;
@@ -1746,7 +2630,7 @@ export class ImageEditor {
1746
2630
  buildMosaicControllerContext() {
1747
2631
  return {
1748
2632
  fabric: this.fabricModule,
1749
- canvas: this.canvas,
2633
+ canvas: this.getLiveCanvasOrThrow('mosaicController'),
1750
2634
  options: this.options,
1751
2635
  historyManager: this.historyManager,
1752
2636
  getMosaicConfig: () => cloneResolvedMosaicConfig(this.currentMosaicConfig),
@@ -1846,7 +2730,7 @@ export class ImageEditor {
1846
2730
  buildCropControllerContext(operationToken) {
1847
2731
  return {
1848
2732
  fabric: this.fabricModule,
1849
- canvas: this.canvas,
2733
+ canvas: this.getLiveCanvasOrThrow('cropController'),
1850
2734
  options: this.options,
1851
2735
  historyManager: this.historyManager,
1852
2736
  isImageLoaded: () => this.isImageLoaded(),
@@ -1868,26 +2752,82 @@ export class ImageEditor {
1868
2752
  },
1869
2753
  };
1870
2754
  }
2755
+ syncInputValue(inputElement, value) {
2756
+ if (!inputElement)
2757
+ return;
2758
+ const ownerDocument = inputElement.ownerDocument;
2759
+ if (ownerDocument.activeElement === inputElement && !inputElement.readOnly)
2760
+ return;
2761
+ if (inputElement.value !== value)
2762
+ inputElement.value = value;
2763
+ }
1871
2764
  updateInputs() {
1872
2765
  const scaleId = this.elements.scalePercentageInput;
1873
2766
  if (scaleId) {
1874
2767
  const scaleInputElement = document.getElementById(scaleId);
1875
- if (scaleInputElement) {
1876
- scaleInputElement.value = String(Math.round(this.currentScale * 100));
1877
- }
2768
+ this.syncInputValue(scaleInputElement, String(Math.round(this.currentScale * 100)));
1878
2769
  }
1879
2770
  const mosaicConfig = this.getMosaicConfig();
1880
2771
  const mosaicBrushSizeInputId = this.elements.mosaicBrushSizeInput;
1881
2772
  if (mosaicBrushSizeInputId) {
1882
2773
  const brushInput = document.getElementById(mosaicBrushSizeInputId);
1883
- if (brushInput)
1884
- brushInput.value = String(mosaicConfig.brushSize);
2774
+ this.syncInputValue(brushInput, String(mosaicConfig.brushSize));
1885
2775
  }
1886
2776
  const mosaicBlockSizeInputId = this.elements.mosaicBlockSizeInput;
1887
2777
  if (mosaicBlockSizeInputId) {
1888
2778
  const blockInput = document.getElementById(mosaicBlockSizeInputId);
1889
- if (blockInput)
1890
- blockInput.value = String(mosaicConfig.blockSize);
2779
+ this.syncInputValue(blockInput, String(mosaicConfig.blockSize));
2780
+ }
2781
+ const textConfig = this.getTextConfig();
2782
+ const textColorInputId = this.elements.textColorInput;
2783
+ if (textColorInputId) {
2784
+ const textColorInput = document.getElementById(textColorInputId);
2785
+ this.syncInputValue(textColorInput, textConfig.fill);
2786
+ }
2787
+ const textFontSizeInputId = this.elements.textFontSizeInput;
2788
+ if (textFontSizeInputId) {
2789
+ const fontInput = document.getElementById(textFontSizeInputId);
2790
+ this.syncInputValue(fontInput, String(textConfig.fontSize));
2791
+ }
2792
+ const drawConfig = this.getDrawConfig();
2793
+ const drawColorInputId = this.elements.drawColorInput;
2794
+ if (drawColorInputId) {
2795
+ const drawColorInput = document.getElementById(drawColorInputId);
2796
+ this.syncInputValue(drawColorInput, drawConfig.color);
2797
+ }
2798
+ const drawBrushSizeInputId = this.elements.drawBrushSizeInput;
2799
+ if (drawBrushSizeInputId) {
2800
+ const brushInput = document.getElementById(drawBrushSizeInputId);
2801
+ this.syncInputValue(brushInput, String(drawConfig.brushSize));
2802
+ }
2803
+ }
2804
+ async mergeAnnotations() {
2805
+ if (!this.canvas)
2806
+ return;
2807
+ if (!this.canRunIdleOperation('mergeAnnotations'))
2808
+ return;
2809
+ this.finalizeActiveTextEditingIfNeeded();
2810
+ const hasAnnotations = this.canvas.getObjects().some(isAnnotationObject);
2811
+ if (!hasAnnotations)
2812
+ return;
2813
+ const callbackContext = this.buildCallbackContext('mergeAnnotations', false);
2814
+ const operationToken = this.operationGuard.beginBusyOperation('mergeAnnotations');
2815
+ this.emitBusyChangeIfChanged(callbackContext);
2816
+ this.updateUi();
2817
+ try {
2818
+ await mergeAnnotationsImpl(this.buildMergeAnnotationsContext(operationToken));
2819
+ this.updateInputs();
2820
+ this.updateMaskList();
2821
+ this.updateAnnotationList();
2822
+ this.emitAnnotationsChanged(callbackContext);
2823
+ if (this.getMasks().length > 0)
2824
+ this.emitMasksChanged(callbackContext);
2825
+ this.emitImageChanged(callbackContext);
2826
+ }
2827
+ finally {
2828
+ this.operationGuard.endBusyOperation(operationToken);
2829
+ this.emitBusyChangeIfChanged(callbackContext);
2830
+ this.updateUi();
1891
2831
  }
1892
2832
  }
1893
2833
  updateUi() {
@@ -1896,14 +2836,20 @@ export class ImageEditor {
1896
2836
  return;
1897
2837
  const hasImage = !!this.originalImage;
1898
2838
  const masks = hasImage ? this.canvas.getObjects().filter(isMaskObject) : [];
2839
+ const annotations = hasImage ? this.canvas.getObjects().filter(isAnnotationObject) : [];
1899
2840
  const hasMasks = masks.length > 0;
2841
+ const hasAnnotations = annotations.length > 0;
1900
2842
  const activeObject = this.canvas.getActiveObject();
1901
2843
  const hasSelectedMask = !!(activeObject && isMaskObject(activeObject));
2844
+ const hasSelectedAnnotation = !!(activeObject && isAnnotationObject(activeObject));
2845
+ const hasSelectedEditableObject = !!activeObject && isEditableOverlayObject(activeObject);
1902
2846
  const isDefaultTransform = this.currentScale === 1 && this.currentRotation === 0;
1903
2847
  const canUndo = this.historyManager.canUndo();
1904
2848
  const canRedo = this.historyManager.canRedo();
1905
2849
  const isInCropMode = this.cropSession !== null;
1906
2850
  const isInMosaicMode = this.mosaicSession !== null;
2851
+ const isInTextMode = this.textSession !== null;
2852
+ const isInDrawMode = this.drawSession !== null;
1907
2853
  const isBusy = this.operationGuard.isBusy() || this.animQueue.isBusy();
1908
2854
  const isMosaicApplying = ((_a = this.mosaicSession) === null || _a === void 0 ? void 0 : _a.isApplying) === true;
1909
2855
  if (isInCropMode) {
@@ -1912,6 +2858,18 @@ export class ImageEditor {
1912
2858
  });
1913
2859
  return;
1914
2860
  }
2861
+ if (isInTextMode) {
2862
+ CROP_MODE_CONTROL_KEYS.forEach((key) => {
2863
+ this.setControlEnabled(key, !isBusy && TEXT_MODE_ENABLED_KEYS.includes(key));
2864
+ });
2865
+ return;
2866
+ }
2867
+ if (isInDrawMode) {
2868
+ CROP_MODE_CONTROL_KEYS.forEach((key) => {
2869
+ this.setControlEnabled(key, !isBusy && DRAW_MODE_ENABLED_KEYS.includes(key));
2870
+ });
2871
+ return;
2872
+ }
1915
2873
  if (isInMosaicMode) {
1916
2874
  MOSAIC_MODE_CONTROL_KEYS.forEach((key) => {
1917
2875
  this.setControlEnabled(key, !isBusy && !isMosaicApplying && MOSAIC_MODE_ENABLED_KEYS.includes(key));
@@ -1930,15 +2888,31 @@ export class ImageEditor {
1930
2888
  this.setControlEnabled('removeSelectedMaskButton', hasSelectedMask && !isBusy);
1931
2889
  this.setControlEnabled('removeAllMasksButton', hasMasks && !isBusy);
1932
2890
  this.setControlEnabled('mergeMasksButton', hasImage && hasMasks && !isBusy);
2891
+ this.setControlEnabled('removeSelectedAnnotationButton', hasSelectedAnnotation && !isBusy);
2892
+ this.setControlEnabled('removeAllAnnotationsButton', hasAnnotations && !isBusy);
2893
+ this.setControlEnabled('deleteSelectedObjectButton', hasSelectedEditableObject && !isBusy);
2894
+ this.setControlEnabled('mergeAnnotationsButton', hasImage && hasAnnotations && !isBusy);
2895
+ this.setControlEnabled('bringSelectedObjectForwardButton', hasSelectedEditableObject && !isBusy);
2896
+ this.setControlEnabled('sendSelectedObjectBackwardButton', hasSelectedEditableObject && !isBusy);
2897
+ this.setControlEnabled('bringSelectedObjectToFrontButton', hasSelectedEditableObject && !isBusy);
2898
+ this.setControlEnabled('sendSelectedObjectToBackButton', hasSelectedEditableObject && !isBusy);
1933
2899
  this.setControlEnabled('downloadImageButton', hasImage && !isBusy);
1934
2900
  this.setControlEnabled('resetImageTransformButton', hasImage && !isDefaultTransform && !isBusy);
1935
2901
  this.setControlEnabled('undoButton', hasImage && !isBusy && canUndo);
1936
2902
  this.setControlEnabled('redoButton', hasImage && !isBusy && canRedo);
1937
2903
  this.setControlEnabled('enterCropModeButton', hasImage && !isBusy);
1938
2904
  this.setControlEnabled('enterMosaicModeButton', hasImage && !isBusy);
2905
+ this.setControlEnabled('enterTextModeButton', hasImage && !isBusy);
2906
+ this.setControlEnabled('enterDrawModeButton', hasImage && !isBusy);
1939
2907
  this.setControlEnabled('exitMosaicModeButton', false);
2908
+ this.setControlEnabled('exitTextModeButton', false);
2909
+ this.setControlEnabled('exitDrawModeButton', false);
1940
2910
  this.setControlEnabled('mosaicBrushSizeInput', !this.isDisposed);
1941
2911
  this.setControlEnabled('mosaicBlockSizeInput', !this.isDisposed);
2912
+ this.setControlEnabled('textColorInput', !this.isDisposed);
2913
+ this.setControlEnabled('textFontSizeInput', !this.isDisposed);
2914
+ this.setControlEnabled('drawColorInput', !this.isDisposed);
2915
+ this.setControlEnabled('drawBrushSizeInput', !this.isDisposed);
1942
2916
  this.setControlEnabled('imageInput', !isBusy);
1943
2917
  this.setControlEnabled('applyCropButton', false);
1944
2918
  this.setControlEnabled('cancelCropButton', false);
@@ -1953,7 +2927,10 @@ export class ImageEditor {
1953
2927
  return;
1954
2928
  this.recordElementOriginalState(key, controlElement);
1955
2929
  if ('disabled' in controlElement) {
1956
- controlElement.disabled = !isEnabled;
2930
+ const formControl = controlElement;
2931
+ const nextDisabled = !isEnabled;
2932
+ if (formControl.disabled !== nextDisabled)
2933
+ formControl.disabled = nextDisabled;
1957
2934
  return;
1958
2935
  }
1959
2936
  if (!isEnabled) {
@@ -2026,6 +3003,15 @@ export class ImageEditor {
2026
3003
  this.operationGuard.markDisposed();
2027
3004
  this.animQueue.clear();
2028
3005
  (_a = this.domBindings) === null || _a === void 0 ? void 0 : _a.removeAll();
3006
+ if (this.keyboardHandler && this.keyboardDocument) {
3007
+ try {
3008
+ this.keyboardDocument.removeEventListener('keydown', this.keyboardHandler);
3009
+ }
3010
+ catch {
3011
+ }
3012
+ }
3013
+ this.keyboardHandler = null;
3014
+ this.keyboardDocument = null;
2029
3015
  this.restoreElementOriginalStates();
2030
3016
  if (this.cropSession && this.canvas) {
2031
3017
  try {
@@ -2044,6 +3030,22 @@ export class ImageEditor {
2044
3030
  }
2045
3031
  this.mosaicSession = null;
2046
3032
  }
3033
+ if (this.textSession && this.canvas) {
3034
+ try {
3035
+ exitTextModeImpl(this.buildTextControllerContext());
3036
+ }
3037
+ catch {
3038
+ }
3039
+ this.textSession = null;
3040
+ }
3041
+ if (this.drawSession && this.canvas) {
3042
+ try {
3043
+ exitDrawModeImpl(this.buildDrawControllerContext());
3044
+ }
3045
+ catch {
3046
+ }
3047
+ this.drawSession = null;
3048
+ }
2047
3049
  if (this.canvas) {
2048
3050
  try {
2049
3051
  void Promise.resolve(this.canvas.dispose()).catch(() => {
@@ -2059,6 +3061,7 @@ export class ImageEditor {
2059
3061
  this.currentImageMimeType = null;
2060
3062
  this.lastMask = null;
2061
3063
  this.maskCounter = 0;
3064
+ this.annotationCounter = 0;
2062
3065
  this.currentScale = 1;
2063
3066
  this.currentRotation = 0;
2064
3067
  this.baseImageScale = 1;