js-draw 0.10.3 → 0.11.1

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 (56) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +72 -0
  2. package/CHANGELOG.md +9 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +12 -3
  5. package/dist/src/Editor.js +72 -25
  6. package/dist/src/EditorImage.js +1 -1
  7. package/dist/src/SVGLoader.js +3 -2
  8. package/dist/src/components/AbstractComponent.d.ts +1 -0
  9. package/dist/src/components/AbstractComponent.js +15 -6
  10. package/dist/src/components/ImageComponent.d.ts +3 -0
  11. package/dist/src/components/ImageComponent.js +12 -1
  12. package/dist/src/localizations/es.js +1 -1
  13. package/dist/src/rendering/renderers/SVGRenderer.js +9 -5
  14. package/dist/src/toolbar/HTMLToolbar.js +2 -1
  15. package/dist/src/toolbar/IconProvider.d.ts +1 -0
  16. package/dist/src/toolbar/IconProvider.js +7 -0
  17. package/dist/src/toolbar/localization.d.ts +8 -0
  18. package/dist/src/toolbar/localization.js +8 -0
  19. package/dist/src/toolbar/widgets/InsertImageWidget.d.ts +19 -0
  20. package/dist/src/toolbar/widgets/InsertImageWidget.js +169 -0
  21. package/dist/src/toolbar/widgets/lib.d.ts +1 -0
  22. package/dist/src/toolbar/widgets/lib.js +1 -0
  23. package/dist/src/tools/Eraser.d.ts +1 -1
  24. package/dist/src/tools/Eraser.js +16 -18
  25. package/dist/src/tools/PanZoom.js +10 -0
  26. package/dist/src/tools/PasteHandler.js +1 -39
  27. package/dist/src/tools/SelectionTool/Selection.d.ts +2 -3
  28. package/dist/src/tools/SelectionTool/Selection.js +63 -26
  29. package/dist/src/tools/SelectionTool/SelectionTool.js +9 -0
  30. package/dist/src/util/fileToBase64.d.ts +3 -0
  31. package/dist/src/util/fileToBase64.js +13 -0
  32. package/dist/src/util/waitForTimeout.d.ts +2 -0
  33. package/dist/src/util/waitForTimeout.js +7 -0
  34. package/package.json +1 -1
  35. package/src/Editor.ts +90 -27
  36. package/src/EditorImage.ts +1 -1
  37. package/src/SVGLoader.ts +1 -0
  38. package/src/components/AbstractComponent.ts +18 -4
  39. package/src/components/ImageComponent.ts +15 -0
  40. package/src/localizations/es.ts +3 -0
  41. package/src/rendering/renderers/SVGRenderer.ts +6 -1
  42. package/src/toolbar/HTMLToolbar.ts +3 -1
  43. package/src/toolbar/IconProvider.ts +8 -0
  44. package/src/toolbar/localization.ts +19 -1
  45. package/src/toolbar/toolbar.css +2 -0
  46. package/src/toolbar/widgets/InsertImageWidget.css +44 -0
  47. package/src/toolbar/widgets/InsertImageWidget.ts +222 -0
  48. package/src/toolbar/widgets/lib.ts +2 -0
  49. package/src/tools/Eraser.ts +19 -15
  50. package/src/tools/PanZoom.test.ts +65 -0
  51. package/src/tools/PanZoom.ts +12 -0
  52. package/src/tools/PasteHandler.ts +2 -51
  53. package/src/tools/SelectionTool/Selection.ts +62 -22
  54. package/src/tools/SelectionTool/SelectionTool.ts +12 -0
  55. package/src/util/fileToBase64.ts +18 -0
  56. package/src/util/waitForTimeout.ts +9 -0
@@ -29,6 +29,7 @@ import Pointer from './Pointer';
29
29
  import Rect2 from './math/Rect2';
30
30
  import { EditorLocalization } from './localization';
31
31
  import IconProvider from './toolbar/IconProvider';
32
+ import AbstractComponent from './components/AbstractComponent';
32
33
  type HTMLPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel';
33
34
  type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent) => boolean;
34
35
  export interface EditorSettings {
@@ -157,7 +158,7 @@ export declare class Editor {
157
158
  /** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
158
159
  handleKeyEventsFrom(elem: HTMLElement): void;
159
160
  /** `apply` a command. `command` will be announced for accessibility. */
160
- dispatch(command: Command, addToHistory?: boolean): void;
161
+ dispatch(command: Command, addToHistory?: boolean): void | Promise<void>;
161
162
  /**
162
163
  * Dispatches a command without announcing it. By default, does not add to history.
163
164
  * Use this to show finalized commands that don't need to have `announceForAccessibility`
@@ -173,7 +174,7 @@ export declare class Editor {
173
174
  * editor.dispatchNoAnnounce(editor.viewport.zoomTo(someRectangle), addToHistory);
174
175
  * ```
175
176
  */
176
- dispatchNoAnnounce(command: Command, addToHistory?: boolean): void;
177
+ dispatchNoAnnounce(command: Command, addToHistory?: boolean): void | Promise<void>;
177
178
  /**
178
179
  * Apply a large transformation in chunks.
179
180
  * If `apply` is `false`, the commands are unapplied.
@@ -185,8 +186,15 @@ export declare class Editor {
185
186
  asyncUnapplyCommands(commands: Command[], chunkSize: number): Promise<void>;
186
187
  private announceUndoCallback;
187
188
  private announceRedoCallback;
189
+ private nextRerenderListeners;
188
190
  private rerenderQueued;
189
- queueRerender(): void;
191
+ /**
192
+ * Schedule a re-render for some time in the near future. Does not schedule an additional
193
+ * re-render if a re-render is already queued.
194
+ *
195
+ * @returns a promise that resolves when
196
+ */
197
+ queueRerender(): Promise<void>;
190
198
  rerender(showImageBounds?: boolean): void;
191
199
  drawWetInk(...path: RenderablePathSpec[]): void;
192
200
  clearWetInk(): void;
@@ -197,6 +205,7 @@ export declare class Editor {
197
205
  addStyleSheet(content: string): HTMLStyleElement;
198
206
  sendKeyboardEvent(eventType: InputEvtType.KeyPressEvent | InputEvtType.KeyUpEvent, key: string, ctrlKey?: boolean, altKey?: boolean): void;
199
207
  sendPenEvent(eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, point: Point2, allPointers?: Pointer[]): void;
208
+ addAndCenterComponents(components: AbstractComponent[], selectComponents?: boolean): Promise<void>;
200
209
  toDataURL(format?: 'image/png' | 'image/jpeg' | 'image/webp'): string;
201
210
  toSVG(): SVGElement;
202
211
  loadFrom(loader: ImageLoader): Promise<void>;
@@ -45,6 +45,9 @@ import IconProvider from './toolbar/IconProvider';
45
45
  import { toRoundedString } from './math/rounding';
46
46
  import CanvasRenderer from './rendering/renderers/CanvasRenderer';
47
47
  import untilNextAnimationFrame from './util/untilNextAnimationFrame';
48
+ import fileToBase64 from './util/fileToBase64';
49
+ import uniteCommands from './commands/uniteCommands';
50
+ import SelectionTool from './tools/SelectionTool/SelectionTool';
48
51
  // { @inheritDoc Editor! }
49
52
  export class Editor {
50
53
  /**
@@ -81,6 +84,8 @@ export class Editor {
81
84
  this.announceRedoCallback = (command) => {
82
85
  this.announceForAccessibility(this.localization.redoAnnouncement(command.description(this, this.localization)));
83
86
  };
87
+ // Listeners to be called once at the end of the next re-render.
88
+ this.nextRerenderListeners = [];
84
89
  this.rerenderQueued = false;
85
90
  this.localization = Object.assign(Object.assign({}, getLocalizationTable()), settings.localization);
86
91
  // Fill default settings.
@@ -372,18 +377,12 @@ export class Editor {
372
377
  for (const file of clipboardData.files) {
373
378
  const fileType = file.type.toLowerCase();
374
379
  if (fileType === 'image/png' || fileType === 'image/jpg') {
375
- const reader = new FileReader();
376
380
  this.showLoadingWarning(0);
381
+ const onprogress = (evt) => {
382
+ this.showLoadingWarning(evt.loaded / evt.total);
383
+ };
377
384
  try {
378
- const data = yield new Promise((resolve, reject) => {
379
- reader.onload = () => resolve(reader.result);
380
- reader.onerror = reject;
381
- reader.onabort = reject;
382
- reader.onprogress = (evt) => {
383
- this.showLoadingWarning(evt.loaded / evt.total);
384
- };
385
- reader.readAsDataURL(file);
386
- });
385
+ const data = yield fileToBase64(file, onprogress);
387
386
  if (data && this.toolController.dispatchInputEvent({
388
387
  kind: InputEvtType.PasteEvent,
389
388
  mime: fileType,
@@ -477,14 +476,9 @@ export class Editor {
477
476
  }
478
477
  /** `apply` a command. `command` will be announced for accessibility. */
479
478
  dispatch(command, addToHistory = true) {
480
- if (addToHistory) {
481
- // .push applies [command] to this
482
- this.history.push(command);
483
- }
484
- else {
485
- command.apply(this);
486
- }
479
+ const dispatchResult = this.dispatchNoAnnounce(command, addToHistory);
487
480
  this.announceForAccessibility(command.description(this, this.localization));
481
+ return dispatchResult;
488
482
  }
489
483
  /**
490
484
  * Dispatches a command without announcing it. By default, does not add to history.
@@ -503,11 +497,10 @@ export class Editor {
503
497
  */
504
498
  dispatchNoAnnounce(command, addToHistory = false) {
505
499
  if (addToHistory) {
506
- this.history.push(command);
507
- }
508
- else {
509
- command.apply(this);
500
+ const apply = false; // Don't double-apply
501
+ this.history.push(command, apply);
510
502
  }
503
+ return command.apply(this);
511
504
  }
512
505
  /**
513
506
  * Apply a large transformation in chunks.
@@ -550,16 +543,27 @@ export class Editor {
550
543
  asyncUnapplyCommands(commands, chunkSize) {
551
544
  return this.asyncApplyOrUnapplyCommands(commands, false, chunkSize);
552
545
  }
553
- // Schedule a re-render for some time in the near future. Does not schedule an additional
554
- // re-render if a re-render is already queued.
546
+ /**
547
+ * Schedule a re-render for some time in the near future. Does not schedule an additional
548
+ * re-render if a re-render is already queued.
549
+ *
550
+ * @returns a promise that resolves when
551
+ */
555
552
  queueRerender() {
556
553
  if (!this.rerenderQueued) {
557
554
  this.rerenderQueued = true;
558
555
  requestAnimationFrame(() => {
559
- this.rerender();
560
- this.rerenderQueued = false;
556
+ // If .rerender was called manually, we might not need to
557
+ // re-render.
558
+ if (this.rerenderQueued) {
559
+ this.rerender();
560
+ this.rerenderQueued = false;
561
+ }
561
562
  });
562
563
  }
564
+ return new Promise(resolve => {
565
+ this.nextRerenderListeners.push(() => resolve());
566
+ });
563
567
  }
564
568
  rerender(showImageBounds = true) {
565
569
  this.display.startRerender();
@@ -576,6 +580,8 @@ export class Editor {
576
580
  renderer.drawRect(this.importExportViewport.visibleRect, exportRectStrokeWidth, exportRectFill);
577
581
  }
578
582
  this.rerenderQueued = false;
583
+ this.nextRerenderListeners.forEach(listener => listener());
584
+ this.nextRerenderListeners = [];
579
585
  }
580
586
  drawWetInk(...path) {
581
587
  for (const part of path) {
@@ -628,6 +634,47 @@ export class Editor {
628
634
  current: mainPointer,
629
635
  });
630
636
  }
637
+ addAndCenterComponents(components, selectComponents = true) {
638
+ return __awaiter(this, void 0, void 0, function* () {
639
+ let bbox = null;
640
+ for (const component of components) {
641
+ if (bbox) {
642
+ bbox = bbox.union(component.getBBox());
643
+ }
644
+ else {
645
+ bbox = component.getBBox();
646
+ }
647
+ }
648
+ if (!bbox) {
649
+ return;
650
+ }
651
+ // Find a transform that scales/moves bbox onto the screen.
652
+ const visibleRect = this.viewport.visibleRect;
653
+ const scaleRatioX = visibleRect.width / bbox.width;
654
+ const scaleRatioY = visibleRect.height / bbox.height;
655
+ let scaleRatio = scaleRatioX;
656
+ if (bbox.width * scaleRatio > visibleRect.width || bbox.height * scaleRatio > visibleRect.height) {
657
+ scaleRatio = scaleRatioY;
658
+ }
659
+ scaleRatio *= 2 / 3;
660
+ scaleRatio = Viewport.roundScaleRatio(scaleRatio);
661
+ const transfm = Mat33.translation(visibleRect.center.minus(bbox.center)).rightMul(Mat33.scaling2D(scaleRatio, bbox.center));
662
+ const commands = [];
663
+ for (const component of components) {
664
+ // To allow deserialization, we need to add first, then transform.
665
+ commands.push(EditorImage.addElement(component));
666
+ commands.push(component.transformBy(transfm));
667
+ }
668
+ const applyChunkSize = 100;
669
+ yield this.dispatch(uniteCommands(commands, applyChunkSize), true);
670
+ if (selectComponents) {
671
+ for (const selectionTool of this.toolController.getMatchingTools(SelectionTool)) {
672
+ selectionTool.setEnabled(true);
673
+ selectionTool.setSelection(components);
674
+ }
675
+ }
676
+ });
677
+ }
631
678
  // Get a data URL (e.g. as produced by `HTMLCanvasElement::toDataURL`).
632
679
  // If `format` is not `image/png`, a PNG image URL may still be returned (as in the
633
680
  // case of `HTMLCanvasElement::toDataURL`).
@@ -2,7 +2,7 @@ var _a;
2
2
  import AbstractComponent from './components/AbstractComponent';
3
3
  import Rect2 from './math/Rect2';
4
4
  import SerializableCommand from './commands/SerializableCommand';
5
- // @internal
5
+ // @internal Sort by z-index, low to high
6
6
  export const sortLeavesByZIndex = (leaves) => {
7
7
  leaves.sort((a, b) => a.getContent().getZIndex() - b.getContent().getZIndex());
8
8
  };
@@ -233,16 +233,17 @@ export default class SVGLoader {
233
233
  }
234
234
  }
235
235
  addImage(elem) {
236
- var _a, _b;
236
+ var _a, _b, _c;
237
237
  return __awaiter(this, void 0, void 0, function* () {
238
238
  const image = new Image();
239
239
  image.src = (_a = elem.getAttribute('xlink:href')) !== null && _a !== void 0 ? _a : elem.href.baseVal;
240
+ image.setAttribute('alt', (_b = elem.getAttribute('aria-label')) !== null && _b !== void 0 ? _b : '');
240
241
  try {
241
242
  const supportedAttrs = [];
242
243
  const transform = this.getTransform(elem, supportedAttrs);
243
244
  const imageElem = yield ImageComponent.fromImage(image, transform);
244
245
  this.attachUnrecognisedAttrs(imageElem, elem, new Set(supportedAttrs), new Set(['transform']));
245
- (_b = this.onAddComponent) === null || _b === void 0 ? void 0 : _b.call(this, imageElem);
246
+ (_c = this.onAddComponent) === null || _c === void 0 ? void 0 : _c.call(this, imageElem);
246
247
  }
247
248
  catch (e) {
248
249
  console.error('Error loading image:', e, '. Element: ', elem, '. Continuing...');
@@ -28,6 +28,7 @@ export default abstract class AbstractComponent {
28
28
  protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
29
29
  protected abstract applyTransformation(affineTransfm: Mat33): void;
30
30
  transformBy(affineTransfm: Mat33): SerializableCommand;
31
+ setZIndex(newZIndex: number): SerializableCommand;
31
32
  isSelectable(): boolean;
32
33
  getProportionalRenderingTime(): number;
33
34
  private static transformElementCommandId;
@@ -48,6 +48,10 @@ export default class AbstractComponent {
48
48
  transformBy(affineTransfm) {
49
49
  return new AbstractComponent.TransformElementCommand(affineTransfm, this);
50
50
  }
51
+ // Returns a command that updates this component's z-index.
52
+ setZIndex(newZIndex) {
53
+ return new AbstractComponent.TransformElementCommand(Mat33.identity, this, newZIndex);
54
+ }
51
55
  // @returns true iff this component can be selected (e.g. by the selection tool.)
52
56
  isSelectable() {
53
57
  return true;
@@ -126,10 +130,11 @@ AbstractComponent.zIndexCounter = 0;
126
130
  AbstractComponent.deserializationCallbacks = {};
127
131
  AbstractComponent.transformElementCommandId = 'transform-element';
128
132
  AbstractComponent.UnresolvedTransformElementCommand = class extends SerializableCommand {
129
- constructor(affineTransfm, componentID) {
133
+ constructor(affineTransfm, componentID, targetZIndex) {
130
134
  super(AbstractComponent.transformElementCommandId);
131
135
  this.affineTransfm = affineTransfm;
132
136
  this.componentID = componentID;
137
+ this.targetZIndex = targetZIndex;
133
138
  this.command = null;
134
139
  }
135
140
  resolveCommand(editor) {
@@ -140,7 +145,7 @@ AbstractComponent.UnresolvedTransformElementCommand = class extends Serializable
140
145
  if (!component) {
141
146
  throw new Error(`Unable to resolve component with ID ${this.componentID}`);
142
147
  }
143
- this.command = new AbstractComponent.TransformElementCommand(this.affineTransfm, component);
148
+ this.command = new AbstractComponent.TransformElementCommand(this.affineTransfm, component, this.targetZIndex);
144
149
  }
145
150
  apply(editor) {
146
151
  this.resolveCommand(editor);
@@ -157,15 +162,17 @@ AbstractComponent.UnresolvedTransformElementCommand = class extends Serializable
157
162
  return {
158
163
  id: this.componentID,
159
164
  transfm: this.affineTransfm.toArray(),
165
+ targetZIndex: this.targetZIndex,
160
166
  };
161
167
  }
162
168
  };
163
169
  AbstractComponent.TransformElementCommand = (_a = class extends SerializableCommand {
164
- constructor(affineTransfm, component) {
170
+ constructor(affineTransfm, component, targetZIndex) {
165
171
  super(AbstractComponent.transformElementCommandId);
166
172
  this.affineTransfm = affineTransfm;
167
173
  this.component = component;
168
174
  this.origZIndex = component.zIndex;
175
+ this.targetZIndex = targetZIndex !== null && targetZIndex !== void 0 ? targetZIndex : AbstractComponent.zIndexCounter++;
169
176
  }
170
177
  updateTransform(editor, newTransfm) {
171
178
  // Any parent should have only one direct child.
@@ -182,7 +189,7 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
182
189
  }
183
190
  }
184
191
  apply(editor) {
185
- this.component.zIndex = AbstractComponent.zIndexCounter++;
192
+ this.component.zIndex = this.targetZIndex;
186
193
  this.updateTransform(editor, this.affineTransfm);
187
194
  editor.queueRerender();
188
195
  }
@@ -198,6 +205,7 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
198
205
  return {
199
206
  id: this.component.getId(),
200
207
  transfm: this.affineTransfm.toArray(),
208
+ targetZIndex: this.targetZIndex,
201
209
  };
202
210
  }
203
211
  },
@@ -205,10 +213,11 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
205
213
  SerializableCommand.register(AbstractComponent.transformElementCommandId, (json, editor) => {
206
214
  const elem = editor.image.lookupElement(json.id);
207
215
  const transform = new Mat33(...json.transfm);
216
+ const targetZIndex = json.targetZIndex;
208
217
  if (!elem) {
209
- return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id);
218
+ return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id, targetZIndex);
210
219
  }
211
- return new AbstractComponent.TransformElementCommand(transform, elem);
220
+ return new AbstractComponent.TransformElementCommand(transform, elem, targetZIndex);
212
221
  });
213
222
  })(),
214
223
  _a);
@@ -23,6 +23,9 @@ export default class ImageComponent extends AbstractComponent {
23
23
  };
24
24
  protected applyTransformation(affineTransfm: Mat33): void;
25
25
  description(localizationTable: ImageComponentLocalization): string;
26
+ getAltText(): string | undefined;
27
+ getURL(): string;
28
+ getTransformation(): Mat33;
26
29
  protected createClone(): AbstractComponent;
27
30
  static deserializeFromJSON(data: any): ImageComponent;
28
31
  }
@@ -33,7 +33,7 @@ export default class ImageComponent extends AbstractComponent {
33
33
  }
34
34
  // Load from an image. Waits for the image to load if incomplete.
35
35
  static fromImage(elem, transform) {
36
- var _a;
36
+ var _a, _b, _c;
37
37
  return __awaiter(this, void 0, void 0, function* () {
38
38
  if (!elem.complete) {
39
39
  yield new Promise((resolve, reject) => {
@@ -70,6 +70,8 @@ export default class ImageComponent extends AbstractComponent {
70
70
  image.width = width;
71
71
  image.height = height;
72
72
  }
73
+ image.setAttribute('alt', (_b = elem.getAttribute('alt')) !== null && _b !== void 0 ? _b : '');
74
+ image.setAttribute('aria-label', (_c = elem.getAttribute('aria-label')) !== null && _c !== void 0 ? _c : '');
73
75
  return new ImageComponent({
74
76
  image,
75
77
  base64Url: url,
@@ -111,6 +113,15 @@ export default class ImageComponent extends AbstractComponent {
111
113
  description(localizationTable) {
112
114
  return this.image.label ? localizationTable.imageNode(this.image.label) : localizationTable.unlabeledImageNode;
113
115
  }
116
+ getAltText() {
117
+ return this.image.label;
118
+ }
119
+ getURL() {
120
+ return this.image.base64Url;
121
+ }
122
+ getTransformation() {
123
+ return this.image.transform;
124
+ }
114
125
  createClone() {
115
126
  return new ImageComponent(Object.assign({}, this.image));
116
127
  }
@@ -14,5 +14,5 @@ const localization = Object.assign(Object.assign({}, defaultEditorLocalization),
14
14
  return `Color fue cambiado a ${color}`;
15
15
  }, keyboardPanZoom: 'Mover la pantalla con el teclado', penTool: function (penId) {
16
16
  return `Lapiz ${penId}`;
17
- }, selectionTool: 'Selecciona', eraserTool: 'Borrador', textTool: 'Texto', enterTextToInsert: 'Entra texto', rerenderAsText: 'Redibuja la pantalla al texto' });
17
+ }, selectionTool: 'Selecciona', eraserTool: 'Borrador', textTool: 'Texto', enterTextToInsert: 'Entra texto', rerenderAsText: 'Redibuja la pantalla al texto', image: 'Imagen', imageSize: (size, units) => `Tamaño del imagen: ${size} ${units}`, imageLoadError: (message) => `Error cargando imagen: ${message}` });
18
18
  export default localization;
@@ -188,15 +188,19 @@ export default class SVGRenderer extends AbstractRenderer {
188
188
  }
189
189
  }
190
190
  drawImage(image) {
191
- var _a, _b, _c, _d, _e;
191
+ var _a, _b, _c, _d, _e, _f;
192
+ let label = (_b = (_a = image.label) !== null && _a !== void 0 ? _a : image.image.getAttribute('aria-label')) !== null && _b !== void 0 ? _b : '';
193
+ if (label === '') {
194
+ label = (_c = image.image.getAttribute('alt')) !== null && _c !== void 0 ? _c : '';
195
+ }
192
196
  const svgImgElem = document.createElementNS(svgNameSpace, 'image');
193
197
  svgImgElem.setAttribute('href', image.base64Url);
194
- svgImgElem.setAttribute('width', (_a = image.image.getAttribute('width')) !== null && _a !== void 0 ? _a : '');
195
- svgImgElem.setAttribute('height', (_b = image.image.getAttribute('height')) !== null && _b !== void 0 ? _b : '');
196
- svgImgElem.setAttribute('aria-label', (_d = (_c = image.image.getAttribute('aria-label')) !== null && _c !== void 0 ? _c : image.image.getAttribute('alt')) !== null && _d !== void 0 ? _d : '');
198
+ svgImgElem.setAttribute('width', (_d = image.image.getAttribute('width')) !== null && _d !== void 0 ? _d : '');
199
+ svgImgElem.setAttribute('height', (_e = image.image.getAttribute('height')) !== null && _e !== void 0 ? _e : '');
200
+ svgImgElem.setAttribute('aria-label', label);
197
201
  this.transformFrom(image.transform, svgImgElem);
198
202
  this.elem.appendChild(svgImgElem);
199
- (_e = this.objectElems) === null || _e === void 0 ? void 0 : _e.push(svgImgElem);
203
+ (_f = this.objectElems) === null || _f === void 0 ? void 0 : _f.push(svgImgElem);
200
204
  }
201
205
  startObject(boundingBox) {
202
206
  super.startObject(boundingBox);
@@ -12,7 +12,7 @@ import EraserWidget from './widgets/EraserToolWidget';
12
12
  import SelectionToolWidget from './widgets/SelectionToolWidget';
13
13
  import TextToolWidget from './widgets/TextToolWidget';
14
14
  import HandToolWidget from './widgets/HandToolWidget';
15
- import { ActionButtonWidget } from './lib';
15
+ import { ActionButtonWidget, InsertImageWidget } from './lib';
16
16
  export const toolbarCSSPrefix = 'toolbar-';
17
17
  export default class HTMLToolbar {
18
18
  /** @internal */
@@ -179,6 +179,7 @@ export default class HTMLToolbar {
179
179
  for (const tool of toolController.getMatchingTools(TextTool)) {
180
180
  this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
181
181
  }
182
+ this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
182
183
  const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
183
184
  if (panZoomTool) {
184
185
  this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
@@ -19,6 +19,7 @@ export default class IconProvider {
19
19
  makeAllDevicePanningIcon(): IconType;
20
20
  makeZoomIcon(): IconType;
21
21
  makeRotationLockIcon(): IconType;
22
+ makeInsertImageIcon(): IconType;
22
23
  makeTextIcon(textStyle: TextStyle): IconType;
23
24
  makePenIcon(tipThickness: number, color: string | Color4, roundedTip?: boolean): IconType;
24
25
  makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): IconType;
@@ -284,6 +284,13 @@ export default class IconProvider {
284
284
  icon.setAttribute('viewBox', '10 10 70 70');
285
285
  return icon;
286
286
  }
287
+ makeInsertImageIcon() {
288
+ return this.makeIconFromPath(`
289
+ M 5 10 L 5 90 L 95 90 L 95 10 L 5 10 z
290
+ M 10 15 L 90 15 L 90 50 L 70 75 L 40 50 L 10 75 L 10 15 z
291
+ M 22.5 25 A 7.5 7.5 0 0 0 15 32.5 A 7.5 7.5 0 0 0 22.5 40 A 7.5 7.5 0 0 0 30 32.5 A 7.5 7.5 0 0 0 22.5 25 z
292
+ `);
293
+ }
287
294
  makeTextIcon(textStyle) {
288
295
  var _a, _b;
289
296
  const icon = document.createElementNS(svgNamespace, 'svg');
@@ -7,6 +7,11 @@ export interface ToolbarLocalization {
7
7
  filledRectanglePen: string;
8
8
  linePen: string;
9
9
  arrowPen: string;
10
+ image: string;
11
+ inputAltText: string;
12
+ chooseFile: string;
13
+ cancel: string;
14
+ submit: string;
10
15
  freehandPen: string;
11
16
  pressureSensitiveFreehandPen: string;
12
17
  selectObjectType: string;
@@ -27,9 +32,12 @@ export interface ToolbarLocalization {
27
32
  resetView: string;
28
33
  selectionToolKeyboardShortcuts: string;
29
34
  paste: string;
35
+ errorImageHasZeroSize: string;
30
36
  dropdownShown: (toolName: string) => string;
31
37
  dropdownHidden: (toolName: string) => string;
32
38
  zoomLevel: (zoomPercentage: number) => string;
33
39
  colorChangedAnnouncement: (color: string) => string;
40
+ imageSize: (size: number, units: string) => string;
41
+ imageLoadError: (message: string) => string;
34
42
  }
35
43
  export declare const defaultToolbarLocalization: ToolbarLocalization;
@@ -4,6 +4,11 @@ export const defaultToolbarLocalization = {
4
4
  select: 'Select',
5
5
  handTool: 'Pan',
6
6
  zoom: 'Zoom',
7
+ image: 'Image',
8
+ inputAltText: 'Alt text: ',
9
+ chooseFile: 'Choose file: ',
10
+ submit: 'Submit',
11
+ cancel: 'Cancel',
7
12
  resetView: 'Reset view',
8
13
  thicknessLabel: 'Thickness: ',
9
14
  colorLabel: 'Color: ',
@@ -31,4 +36,7 @@ export const defaultToolbarLocalization = {
31
36
  dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
32
37
  zoomLevel: (zoomPercent) => `Zoom: ${zoomPercent}%`,
33
38
  colorChangedAnnouncement: (color) => `Color changed to ${color}`,
39
+ imageSize: (size, units) => `Image size: ${size} ${units}`,
40
+ errorImageHasZeroSize: 'Error: Image has zero size',
41
+ imageLoadError: (message) => `Error loading image: ${message}`,
34
42
  };
@@ -0,0 +1,19 @@
1
+ import Editor from '../../Editor';
2
+ import { ToolbarLocalization } from '../localization';
3
+ import ActionButtonWidget from './ActionButtonWidget';
4
+ export default class InsertImageWidget extends ActionButtonWidget {
5
+ private imageSelectionOverlay;
6
+ private imagePreview;
7
+ private imageFileInput;
8
+ private imageAltTextInput;
9
+ private statusView;
10
+ private imageBase64URL;
11
+ private submitButton;
12
+ constructor(editor: Editor, localization?: ToolbarLocalization);
13
+ private static nextInputId;
14
+ private fillOverlay;
15
+ private hideDialog;
16
+ private updateImageSizeDisplay;
17
+ private clearInputs;
18
+ private onClicked;
19
+ }