js-draw 0.3.1 → 0.4.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 (132) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.md +4 -1
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +1 -3
  4. package/dist/bundle.js +1 -1
  5. package/dist/src/Editor.d.ts +15 -1
  6. package/dist/src/Editor.js +221 -78
  7. package/dist/src/EditorImage.js +4 -1
  8. package/dist/src/Pointer.d.ts +1 -1
  9. package/dist/src/Pointer.js +8 -3
  10. package/dist/src/SVGLoader.d.ts +4 -1
  11. package/dist/src/SVGLoader.js +78 -33
  12. package/dist/src/UndoRedoHistory.d.ts +1 -0
  13. package/dist/src/UndoRedoHistory.js +6 -0
  14. package/dist/src/Viewport.d.ts +2 -0
  15. package/dist/src/Viewport.js +26 -5
  16. package/dist/src/commands/lib.d.ts +2 -1
  17. package/dist/src/commands/lib.js +2 -1
  18. package/dist/src/commands/localization.d.ts +1 -0
  19. package/dist/src/commands/localization.js +1 -0
  20. package/dist/src/commands/uniteCommands.d.ts +4 -0
  21. package/dist/src/commands/uniteCommands.js +105 -0
  22. package/dist/src/components/AbstractComponent.d.ts +2 -0
  23. package/dist/src/components/AbstractComponent.js +41 -5
  24. package/dist/src/components/ImageComponent.d.ts +27 -0
  25. package/dist/src/components/ImageComponent.js +129 -0
  26. package/dist/src/components/builders/FreehandLineBuilder.js +2 -2
  27. package/dist/src/components/lib.d.ts +4 -2
  28. package/dist/src/components/lib.js +4 -2
  29. package/dist/src/components/localization.d.ts +2 -0
  30. package/dist/src/components/localization.js +2 -0
  31. package/dist/src/language/assertions.d.ts +1 -0
  32. package/dist/src/language/assertions.js +5 -0
  33. package/dist/src/math/LineSegment2.d.ts +2 -0
  34. package/dist/src/math/LineSegment2.js +3 -0
  35. package/dist/src/math/Mat33.d.ts +38 -2
  36. package/dist/src/math/Mat33.js +30 -1
  37. package/dist/src/math/Path.d.ts +1 -1
  38. package/dist/src/math/Path.js +10 -8
  39. package/dist/src/math/Vec3.d.ts +11 -1
  40. package/dist/src/math/Vec3.js +15 -0
  41. package/dist/src/math/rounding.d.ts +1 -0
  42. package/dist/src/math/rounding.js +13 -6
  43. package/dist/src/rendering/localization.d.ts +3 -0
  44. package/dist/src/rendering/localization.js +3 -0
  45. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -0
  46. package/dist/src/rendering/renderers/AbstractRenderer.js +2 -1
  47. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  48. package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
  49. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
  50. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  51. package/dist/src/rendering/renderers/SVGRenderer.d.ts +5 -2
  52. package/dist/src/rendering/renderers/SVGRenderer.js +45 -20
  53. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
  54. package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
  55. package/dist/src/toolbar/HTMLToolbar.js +5 -4
  56. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
  57. package/dist/src/tools/BaseTool.d.ts +3 -1
  58. package/dist/src/tools/BaseTool.js +6 -0
  59. package/dist/src/tools/PasteHandler.d.ts +16 -0
  60. package/dist/src/tools/PasteHandler.js +144 -0
  61. package/dist/src/tools/Pen.js +1 -1
  62. package/dist/src/tools/SelectionTool/Selection.d.ts +54 -0
  63. package/dist/src/tools/SelectionTool/Selection.js +337 -0
  64. package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +35 -0
  65. package/dist/src/tools/SelectionTool/SelectionHandle.js +75 -0
  66. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +31 -0
  67. package/dist/src/tools/SelectionTool/SelectionTool.js +276 -0
  68. package/dist/src/tools/SelectionTool/TransformMode.d.ts +34 -0
  69. package/dist/src/tools/SelectionTool/TransformMode.js +98 -0
  70. package/dist/src/tools/SelectionTool/types.d.ts +9 -0
  71. package/dist/src/tools/SelectionTool/types.js +11 -0
  72. package/dist/src/tools/ToolController.js +37 -28
  73. package/dist/src/tools/lib.d.ts +2 -1
  74. package/dist/src/tools/lib.js +2 -1
  75. package/dist/src/tools/localization.d.ts +3 -0
  76. package/dist/src/tools/localization.js +3 -0
  77. package/dist/src/types.d.ts +14 -3
  78. package/dist/src/types.js +2 -0
  79. package/package.json +1 -1
  80. package/src/Editor.css +1 -0
  81. package/src/Editor.ts +275 -109
  82. package/src/EditorImage.ts +7 -1
  83. package/src/Pointer.ts +8 -3
  84. package/src/SVGLoader.ts +90 -36
  85. package/src/UndoRedoHistory.test.ts +33 -0
  86. package/src/UndoRedoHistory.ts +8 -0
  87. package/src/Viewport.ts +30 -6
  88. package/src/commands/lib.ts +2 -0
  89. package/src/commands/localization.ts +2 -0
  90. package/src/commands/uniteCommands.test.ts +23 -0
  91. package/src/commands/uniteCommands.ts +121 -0
  92. package/src/components/AbstractComponent.ts +53 -11
  93. package/src/components/ImageComponent.ts +149 -0
  94. package/src/components/Text.ts +2 -6
  95. package/src/components/builders/FreehandLineBuilder.ts +2 -2
  96. package/src/components/lib.ts +7 -2
  97. package/src/components/localization.ts +4 -0
  98. package/src/language/assertions.ts +6 -0
  99. package/src/math/LineSegment2.test.ts +9 -0
  100. package/src/math/LineSegment2.ts +5 -0
  101. package/src/math/Mat33.test.ts +14 -0
  102. package/src/math/Mat33.ts +43 -2
  103. package/src/math/Path.toString.test.ts +12 -1
  104. package/src/math/Path.ts +11 -9
  105. package/src/math/Vec3.ts +22 -1
  106. package/src/math/rounding.test.ts +30 -5
  107. package/src/math/rounding.ts +16 -7
  108. package/src/rendering/localization.ts +6 -0
  109. package/src/rendering/renderers/AbstractRenderer.ts +19 -2
  110. package/src/rendering/renderers/CanvasRenderer.ts +10 -1
  111. package/src/rendering/renderers/DummyRenderer.ts +6 -1
  112. package/src/rendering/renderers/SVGRenderer.ts +50 -21
  113. package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
  114. package/src/toolbar/HTMLToolbar.ts +5 -4
  115. package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
  116. package/src/tools/BaseTool.ts +9 -1
  117. package/src/tools/PasteHandler.ts +159 -0
  118. package/src/tools/Pen.ts +1 -1
  119. package/src/tools/SelectionTool/Selection.ts +455 -0
  120. package/src/tools/SelectionTool/SelectionHandle.ts +99 -0
  121. package/src/tools/SelectionTool/SelectionTool.css +22 -0
  122. package/src/tools/{SelectionTool.test.ts → SelectionTool/SelectionTool.test.ts} +21 -21
  123. package/src/tools/SelectionTool/SelectionTool.ts +335 -0
  124. package/src/tools/SelectionTool/TransformMode.ts +114 -0
  125. package/src/tools/SelectionTool/types.ts +11 -0
  126. package/src/tools/ToolController.ts +52 -45
  127. package/src/tools/lib.ts +2 -1
  128. package/src/tools/localization.ts +8 -0
  129. package/src/types.ts +17 -3
  130. package/dist/src/tools/SelectionTool.d.ts +0 -59
  131. package/dist/src/tools/SelectionTool.js +0 -589
  132. package/src/tools/SelectionTool.ts +0 -725
@@ -28,6 +28,8 @@ import Display, { RenderingMode } from './rendering/Display';
28
28
  import Pointer from './Pointer';
29
29
  import Rect2 from './math/Rect2';
30
30
  import { EditorLocalization } from './localization';
31
+ declare type HTMLPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel';
32
+ declare type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent) => boolean;
31
33
  export interface EditorSettings {
32
34
  /** Defaults to `RenderingMode.CanvasRenderer` */
33
35
  renderingMode: RenderingMode;
@@ -94,6 +96,7 @@ export declare class Editor {
94
96
  private loadingWarning;
95
97
  private accessibilityAnnounceArea;
96
98
  private accessibilityControlArea;
99
+ private eventListenerTargets;
97
100
  private settings;
98
101
  /**
99
102
  * @example
@@ -139,6 +142,16 @@ export declare class Editor {
139
142
  */
140
143
  addToolbar(defaultLayout?: boolean): HTMLToolbar;
141
144
  private registerListeners;
145
+ private pointers;
146
+ private getPointerList;
147
+ /**
148
+ * Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
149
+ * as the content of the editor.
150
+ */
151
+ handleHTMLPointerEvent(eventType: 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel', evt: PointerEvent): boolean;
152
+ private isEventSink;
153
+ private handlePaste;
154
+ handlePointerEventsFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter): void;
142
155
  /** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
143
156
  handleKeyEventsFrom(elem: HTMLElement): void;
144
157
  /** `apply` a command. `command` will be announced for accessibility. */
@@ -180,6 +193,7 @@ export declare class Editor {
180
193
  remove: () => void;
181
194
  };
182
195
  addStyleSheet(content: string): HTMLStyleElement;
196
+ sendKeyboardEvent(eventType: InputEvtType.KeyPressEvent | InputEvtType.KeyUpEvent, key: string, ctrlKey?: boolean): void;
183
197
  sendPenEvent(eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, point: Point2, allPointers?: Pointer[]): void;
184
198
  toSVG(): SVGElement;
185
199
  loadFrom(loader: ImageLoader): Promise<void>;
@@ -191,6 +205,6 @@ export declare class Editor {
191
205
  * This is particularly useful when accessing a bundled version of the editor,
192
206
  * where `SVGLoader.fromString` is unavailable.
193
207
  */
194
- loadFromSVG(svgData: string): Promise<void>;
208
+ loadFromSVG(svgData: string, sanitize?: boolean): Promise<void>;
195
209
  }
196
210
  export default Editor;
@@ -68,7 +68,9 @@ export class Editor {
68
68
  */
69
69
  constructor(parent, settings = {}) {
70
70
  var _a, _b, _c, _d;
71
+ this.eventListenerTargets = [];
71
72
  this.previousAccessibilityAnnouncement = '';
73
+ this.pointers = {};
72
74
  this.announceUndoCallback = (command) => {
73
75
  this.announceForAccessibility(this.localization.undoAnnouncement(command.description(this, this.localization)));
74
76
  };
@@ -181,81 +183,7 @@ export class Editor {
181
183
  return toolbar;
182
184
  }
183
185
  registerListeners() {
184
- const pointers = {};
185
- const getPointerList = () => {
186
- const nowTime = (new Date()).getTime();
187
- const res = [];
188
- for (const id in pointers) {
189
- const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
190
- if (pointers[id] && (nowTime - pointers[id].timeStamp) < maxUnupdatedTime) {
191
- res.push(pointers[id]);
192
- }
193
- }
194
- return res;
195
- };
196
- // May be required to prevent text selection on iOS/Safari:
197
- // See https://stackoverflow.com/a/70992717/17055750
198
- this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
199
- this.renderingRegion.addEventListener('contextmenu', evt => {
200
- // Don't show a context menu
201
- evt.preventDefault();
202
- });
203
- this.renderingRegion.addEventListener('pointerdown', evt => {
204
- const pointer = Pointer.ofEvent(evt, true, this.viewport);
205
- pointers[pointer.id] = pointer;
206
- this.renderingRegion.setPointerCapture(pointer.id);
207
- const event = {
208
- kind: InputEvtType.PointerDownEvt,
209
- current: pointer,
210
- allPointers: getPointerList(),
211
- };
212
- this.toolController.dispatchInputEvent(event);
213
- return true;
214
- });
215
- this.renderingRegion.addEventListener('pointermove', evt => {
216
- var _a, _b;
217
- const pointer = Pointer.ofEvent(evt, (_b = (_a = pointers[evt.pointerId]) === null || _a === void 0 ? void 0 : _a.down) !== null && _b !== void 0 ? _b : false, this.viewport);
218
- if (pointer.down) {
219
- const prevData = pointers[pointer.id];
220
- if (prevData) {
221
- const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
222
- // If the pointer moved less than two pixels, don't send a new event.
223
- if (distanceMoved < 2) {
224
- return;
225
- }
226
- }
227
- pointers[pointer.id] = pointer;
228
- if (this.toolController.dispatchInputEvent({
229
- kind: InputEvtType.PointerMoveEvt,
230
- current: pointer,
231
- allPointers: getPointerList(),
232
- })) {
233
- evt.preventDefault();
234
- }
235
- }
236
- });
237
- const pointerEnd = (evt) => {
238
- const pointer = Pointer.ofEvent(evt, false, this.viewport);
239
- if (!pointers[pointer.id]) {
240
- return;
241
- }
242
- pointers[pointer.id] = pointer;
243
- this.renderingRegion.releasePointerCapture(pointer.id);
244
- if (this.toolController.dispatchInputEvent({
245
- kind: InputEvtType.PointerUpEvt,
246
- current: pointer,
247
- allPointers: getPointerList(),
248
- })) {
249
- evt.preventDefault();
250
- }
251
- delete pointers[pointer.id];
252
- };
253
- this.renderingRegion.addEventListener('pointerup', evt => {
254
- pointerEnd(evt);
255
- });
256
- this.renderingRegion.addEventListener('pointercancel', evt => {
257
- pointerEnd(evt);
258
- });
186
+ this.handlePointerEventsFrom(this.renderingRegion);
259
187
  this.handleKeyEventsFrom(this.renderingRegion);
260
188
  this.container.addEventListener('wheel', evt => {
261
189
  let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
@@ -281,7 +209,9 @@ export class Editor {
281
209
  if (evt.ctrlKey) {
282
210
  delta = Vec3.of(0, 0, evt.deltaY);
283
211
  }
284
- const pos = Vec2.of(evt.offsetX, evt.offsetY);
212
+ // Ensure that `pos` is relative to `this.container`
213
+ const bbox = this.container.getBoundingClientRect();
214
+ const pos = Vec2.of(evt.clientX, evt.clientY).minus(Vec2.of(bbox.left, bbox.top));
285
215
  if (this.toolController.dispatchInputEvent({
286
216
  kind: InputEvtType.WheelEvt,
287
217
  delta,
@@ -305,6 +235,200 @@ export class Editor {
305
235
  this.accessibilityControlArea.addEventListener('input', () => {
306
236
  this.accessibilityControlArea.value = '';
307
237
  });
238
+ document.addEventListener('copy', evt => {
239
+ if (!this.isEventSink(document.querySelector(':focus'))) {
240
+ return;
241
+ }
242
+ const clipboardData = evt.clipboardData;
243
+ if (this.toolController.dispatchInputEvent({
244
+ kind: InputEvtType.CopyEvent,
245
+ setData: (mime, data) => {
246
+ clipboardData === null || clipboardData === void 0 ? void 0 : clipboardData.setData(mime, data);
247
+ },
248
+ })) {
249
+ evt.preventDefault();
250
+ }
251
+ });
252
+ document.addEventListener('paste', evt => {
253
+ this.handlePaste(evt);
254
+ });
255
+ }
256
+ getPointerList() {
257
+ const nowTime = (new Date()).getTime();
258
+ const res = [];
259
+ for (const id in this.pointers) {
260
+ const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
261
+ if (this.pointers[id] && (nowTime - this.pointers[id].timeStamp) < maxUnupdatedTime) {
262
+ res.push(this.pointers[id]);
263
+ }
264
+ }
265
+ return res;
266
+ }
267
+ /**
268
+ * Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
269
+ * as the content of the editor.
270
+ */
271
+ handleHTMLPointerEvent(eventType, evt) {
272
+ var _a, _b, _c;
273
+ const eventsRelativeTo = this.renderingRegion;
274
+ const eventTarget = (_a = evt.target) !== null && _a !== void 0 ? _a : this.renderingRegion;
275
+ if (eventType === 'pointerdown') {
276
+ const pointer = Pointer.ofEvent(evt, true, this.viewport, eventsRelativeTo);
277
+ this.pointers[pointer.id] = pointer;
278
+ eventTarget.setPointerCapture(pointer.id);
279
+ const event = {
280
+ kind: InputEvtType.PointerDownEvt,
281
+ current: pointer,
282
+ allPointers: this.getPointerList(),
283
+ };
284
+ this.toolController.dispatchInputEvent(event);
285
+ return true;
286
+ }
287
+ else if (eventType === 'pointermove') {
288
+ const pointer = Pointer.ofEvent(evt, (_c = (_b = this.pointers[evt.pointerId]) === null || _b === void 0 ? void 0 : _b.down) !== null && _c !== void 0 ? _c : false, this.viewport, eventsRelativeTo);
289
+ if (pointer.down) {
290
+ const prevData = this.pointers[pointer.id];
291
+ if (prevData) {
292
+ const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
293
+ // If the pointer moved less than two pixels, don't send a new event.
294
+ if (distanceMoved < 2) {
295
+ return false;
296
+ }
297
+ }
298
+ this.pointers[pointer.id] = pointer;
299
+ if (this.toolController.dispatchInputEvent({
300
+ kind: InputEvtType.PointerMoveEvt,
301
+ current: pointer,
302
+ allPointers: this.getPointerList(),
303
+ })) {
304
+ evt.preventDefault();
305
+ }
306
+ }
307
+ return true;
308
+ }
309
+ else if (eventType === 'pointercancel' || eventType === 'pointerup') {
310
+ const pointer = Pointer.ofEvent(evt, false, this.viewport, eventsRelativeTo);
311
+ if (!this.pointers[pointer.id]) {
312
+ return false;
313
+ }
314
+ this.pointers[pointer.id] = pointer;
315
+ eventTarget.releasePointerCapture(pointer.id);
316
+ if (this.toolController.dispatchInputEvent({
317
+ kind: InputEvtType.PointerUpEvt,
318
+ current: pointer,
319
+ allPointers: this.getPointerList(),
320
+ })) {
321
+ evt.preventDefault();
322
+ }
323
+ delete this.pointers[pointer.id];
324
+ return true;
325
+ }
326
+ return eventType;
327
+ }
328
+ isEventSink(evtTarget) {
329
+ let currentElem = evtTarget;
330
+ while (currentElem !== null) {
331
+ for (const elem of this.eventListenerTargets) {
332
+ if (elem === currentElem) {
333
+ return true;
334
+ }
335
+ }
336
+ currentElem = currentElem.parentElement;
337
+ }
338
+ return false;
339
+ }
340
+ handlePaste(evt) {
341
+ var _a, _b;
342
+ return __awaiter(this, void 0, void 0, function* () {
343
+ const target = (_a = document.querySelector(':focus')) !== null && _a !== void 0 ? _a : evt.target;
344
+ if (!this.isEventSink(target)) {
345
+ return;
346
+ }
347
+ const clipboardData = (_b = evt.dataTransfer) !== null && _b !== void 0 ? _b : evt.clipboardData;
348
+ if (!clipboardData) {
349
+ return;
350
+ }
351
+ // Handle SVG files (prefer to PNG/JPEG)
352
+ for (const file of clipboardData.files) {
353
+ if (file.type.toLowerCase() === 'image/svg+xml') {
354
+ const text = yield file.text();
355
+ if (this.toolController.dispatchInputEvent({
356
+ kind: InputEvtType.PasteEvent,
357
+ mime: file.type,
358
+ data: text,
359
+ })) {
360
+ evt.preventDefault();
361
+ return;
362
+ }
363
+ }
364
+ }
365
+ // Handle image files.
366
+ for (const file of clipboardData.files) {
367
+ const fileType = file.type.toLowerCase();
368
+ if (fileType === 'image/png' || fileType === 'image/jpg') {
369
+ const reader = new FileReader();
370
+ this.showLoadingWarning(0);
371
+ try {
372
+ const data = yield new Promise((resolve, reject) => {
373
+ reader.onload = () => resolve(reader.result);
374
+ reader.onerror = reject;
375
+ reader.onabort = reject;
376
+ reader.onprogress = (evt) => {
377
+ this.showLoadingWarning(evt.loaded / evt.total);
378
+ };
379
+ reader.readAsDataURL(file);
380
+ });
381
+ if (data && this.toolController.dispatchInputEvent({
382
+ kind: InputEvtType.PasteEvent,
383
+ mime: fileType,
384
+ data: data,
385
+ })) {
386
+ evt.preventDefault();
387
+ this.hideLoadingWarning();
388
+ return;
389
+ }
390
+ }
391
+ catch (e) {
392
+ console.error('Error reading image:', e);
393
+ }
394
+ this.hideLoadingWarning();
395
+ }
396
+ }
397
+ // Supported MIMEs for text data, in order of preference
398
+ const supportedMIMEs = [
399
+ 'image/svg+xml',
400
+ 'text/plain',
401
+ ];
402
+ for (const mime of supportedMIMEs) {
403
+ const data = clipboardData.getData(mime);
404
+ if (data && this.toolController.dispatchInputEvent({
405
+ kind: InputEvtType.PasteEvent,
406
+ mime,
407
+ data,
408
+ })) {
409
+ evt.preventDefault();
410
+ return;
411
+ }
412
+ }
413
+ });
414
+ }
415
+ handlePointerEventsFrom(elem, filter) {
416
+ // May be required to prevent text selection on iOS/Safari:
417
+ // See https://stackoverflow.com/a/70992717/17055750
418
+ elem.addEventListener('touchstart', evt => evt.preventDefault());
419
+ elem.addEventListener('contextmenu', evt => {
420
+ // Don't show a context menu
421
+ evt.preventDefault();
422
+ });
423
+ const eventNames = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'];
424
+ for (const eventName of eventNames) {
425
+ elem.addEventListener(eventName, evt => {
426
+ if (filter && !filter(eventName, evt)) {
427
+ return true;
428
+ }
429
+ return this.handleHTMLPointerEvent(eventName, evt);
430
+ });
431
+ }
308
432
  }
309
433
  /** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
310
434
  handleKeyEventsFrom(elem) {
@@ -333,6 +457,15 @@ export class Editor {
333
457
  evt.preventDefault();
334
458
  }
335
459
  });
460
+ // Allow drop.
461
+ elem.ondragover = evt => {
462
+ evt.preventDefault();
463
+ };
464
+ elem.ondrop = evt => {
465
+ evt.preventDefault();
466
+ this.handlePaste(evt);
467
+ };
468
+ this.eventListenerTargets.push(elem);
336
469
  }
337
470
  /** `apply` a command. `command` will be announced for accessibility. */
338
471
  dispatch(command, addToHistory = true) {
@@ -376,6 +509,7 @@ export class Editor {
376
509
  */
377
510
  asyncApplyOrUnapplyCommands(commands, apply, updateChunkSize) {
378
511
  return __awaiter(this, void 0, void 0, function* () {
512
+ console.assert(updateChunkSize > 0);
379
513
  this.display.setDraftMode(true);
380
514
  for (let i = 0; i < commands.length; i += updateChunkSize) {
381
515
  this.showLoadingWarning(i / commands.length);
@@ -462,6 +596,15 @@ export class Editor {
462
596
  this.container.appendChild(styleSheet);
463
597
  return styleSheet;
464
598
  }
599
+ // Dispatch a keyboard event to the currently selected tool.
600
+ // Intended for unit testing
601
+ sendKeyboardEvent(eventType, key, ctrlKey = false) {
602
+ this.toolController.dispatchInputEvent({
603
+ kind: eventType,
604
+ key,
605
+ ctrlKey
606
+ });
607
+ }
465
608
  // Dispatch a pen event to the currently selected tool.
466
609
  // Intended primarially for unit tests.
467
610
  sendPenEvent(eventType, point, allPointers) {
@@ -553,9 +696,9 @@ export class Editor {
553
696
  * This is particularly useful when accessing a bundled version of the editor,
554
697
  * where `SVGLoader.fromString` is unavailable.
555
698
  */
556
- loadFromSVG(svgData) {
699
+ loadFromSVG(svgData, sanitize = false) {
557
700
  return __awaiter(this, void 0, void 0, function* () {
558
- const loader = SVGLoader.fromString(svgData);
701
+ const loader = SVGLoader.fromString(svgData, sanitize);
559
702
  yield this.loadFrom(loader);
560
703
  });
561
704
  }
@@ -69,6 +69,9 @@ EditorImage.AddElementCommand = (_a = class extends SerializableCommand {
69
69
  super('add-element');
70
70
  this.element = element;
71
71
  this.applyByFlattening = applyByFlattening;
72
+ // Store the element's serialization --- .serializeToJSON may be called on this
73
+ // even when this is not at the top of the undo/redo stack.
74
+ this.serializedElem = element.serialize();
72
75
  if (isNaN(element.getBBox().area)) {
73
76
  throw new Error('Elements in the image cannot have NaN bounding boxes');
74
77
  }
@@ -93,7 +96,7 @@ EditorImage.AddElementCommand = (_a = class extends SerializableCommand {
93
96
  }
94
97
  serializeToJSON() {
95
98
  return {
96
- elemData: this.element.serialize(),
99
+ elemData: this.serializedElem,
97
100
  };
98
101
  }
99
102
  },
@@ -18,6 +18,6 @@ export default class Pointer {
18
18
  readonly id: number;
19
19
  readonly timeStamp: number;
20
20
  private constructor();
21
- static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport): Pointer;
21
+ static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer;
22
22
  static ofCanvasPoint(canvasPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null): Pointer;
23
23
  }
@@ -31,10 +31,15 @@ export default class Pointer {
31
31
  this.id = id;
32
32
  this.timeStamp = timeStamp;
33
33
  }
34
- // Creates a Pointer from a DOM event.
35
- static ofEvent(evt, isDown, viewport) {
34
+ // Creates a Pointer from a DOM event. If `relativeTo` is given, (0, 0) in screen coordinates is
35
+ // considered the top left of `relativeTo`.
36
+ static ofEvent(evt, isDown, viewport, relativeTo) {
36
37
  var _a, _b;
37
- const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
38
+ let screenPos = Vec2.of(evt.clientX, evt.clientY);
39
+ if (relativeTo) {
40
+ const bbox = relativeTo.getBoundingClientRect();
41
+ screenPos = screenPos.minus(Vec2.of(bbox.left, bbox.top));
42
+ }
38
43
  const pointerTypeToDevice = {
39
44
  'mouse': PointerDevice.PrimaryButtonMouse,
40
45
  'pen': PointerDevice.Pen,
@@ -12,6 +12,7 @@ export declare type SVGLoaderUnknownStyleAttribute = {
12
12
  export default class SVGLoader implements ImageLoader {
13
13
  private source;
14
14
  private onFinish?;
15
+ private readonly storeUnknown;
15
16
  private onAddComponent;
16
17
  private onProgress;
17
18
  private onDetermineExportRect;
@@ -23,13 +24,15 @@ export default class SVGLoader implements ImageLoader {
23
24
  private strokeDataFromElem;
24
25
  private attachUnrecognisedAttrs;
25
26
  private addPath;
27
+ private getTransform;
26
28
  private makeText;
27
29
  private addText;
30
+ private addImage;
28
31
  private addUnknownNode;
29
32
  private updateViewBox;
30
33
  private updateSVGAttrs;
31
34
  private visit;
32
35
  private getSourceAttrs;
33
36
  start(onAddComponent: ComponentAddedListener, onProgress: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener | null): Promise<void>;
34
- static fromString(text: string): SVGLoader;
37
+ static fromString(text: string, sanitize?: boolean): SVGLoader;
35
38
  }
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import Color4 from './Color4';
11
+ import ImageComponent from './components/ImageComponent';
11
12
  import Stroke from './components/Stroke';
12
13
  import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
13
14
  import Text from './components/Text';
@@ -22,9 +23,10 @@ export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
22
23
  export const svgAttributesDataKey = 'svgAttrs';
23
24
  export const svgStyleAttributesDataKey = 'svgStyleAttrs';
24
25
  export default class SVGLoader {
25
- constructor(source, onFinish) {
26
+ constructor(source, onFinish, storeUnknown = true) {
26
27
  this.source = source;
27
28
  this.onFinish = onFinish;
29
+ this.storeUnknown = storeUnknown;
28
30
  this.onAddComponent = null;
29
31
  this.onProgress = null;
30
32
  this.onDetermineExportRect = null;
@@ -88,6 +90,9 @@ export default class SVGLoader {
88
90
  return result;
89
91
  }
90
92
  attachUnrecognisedAttrs(elem, node, supportedAttrs, supportedStyleAttrs) {
93
+ if (!this.storeUnknown) {
94
+ return;
95
+ }
91
96
  for (const attr of node.getAttributeNames()) {
92
97
  if (supportedAttrs.has(attr) || (attr === 'style' && supportedStyleAttrs)) {
93
98
  continue;
@@ -123,10 +128,45 @@ export default class SVGLoader {
123
128
  }
124
129
  catch (e) {
125
130
  console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
126
- elem = new UnknownSVGObject(node);
131
+ if (this.storeUnknown) {
132
+ elem = new UnknownSVGObject(node);
133
+ }
134
+ else {
135
+ return;
136
+ }
127
137
  }
128
138
  (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem);
129
139
  }
140
+ // If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
141
+ // to prevent storing duplicate transform information when saving the component.
142
+ getTransform(elem, supportedAttrs, computedStyles) {
143
+ computedStyles !== null && computedStyles !== void 0 ? computedStyles : (computedStyles = window.getComputedStyle(elem));
144
+ let transformProperty = computedStyles.transform;
145
+ if (transformProperty === '' || transformProperty === 'none') {
146
+ transformProperty = elem.style.transform || 'none';
147
+ }
148
+ // Prefer the actual .style.transform
149
+ // to the computed stylesheet -- in some browsers, the computedStyles version
150
+ // can have lower precision.
151
+ let transform;
152
+ try {
153
+ transform = Mat33.fromCSSMatrix(elem.style.transform);
154
+ }
155
+ catch (_e) {
156
+ transform = Mat33.fromCSSMatrix(transformProperty);
157
+ }
158
+ const elemX = elem.getAttribute('x');
159
+ const elemY = elem.getAttribute('y');
160
+ if (elemX && elemY) {
161
+ const x = parseFloat(elemX);
162
+ const y = parseFloat(elemY);
163
+ if (!isNaN(x) && !isNaN(y)) {
164
+ supportedAttrs === null || supportedAttrs === void 0 ? void 0 : supportedAttrs.push('x', 'y');
165
+ transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
166
+ }
167
+ }
168
+ return transform;
169
+ }
130
170
  makeText(elem) {
131
171
  var _a;
132
172
  const contentList = [];
@@ -167,31 +207,8 @@ export default class SVGLoader {
167
207
  fill: Color4.fromString(computedStyles.fill)
168
208
  },
169
209
  };
170
- let transformProperty = computedStyles.transform;
171
- if (transformProperty === '' || transformProperty === 'none') {
172
- transformProperty = elem.style.transform || 'none';
173
- }
174
- // Compute transform matrix. Prefer the actual .style.transform
175
- // to the computed stylesheet -- in some browsers, the computedStyles version
176
- // can have lower precision.
177
- let transform;
178
- try {
179
- transform = Mat33.fromCSSMatrix(elem.style.transform);
180
- }
181
- catch (_e) {
182
- transform = Mat33.fromCSSMatrix(transformProperty);
183
- }
184
210
  const supportedAttrs = [];
185
- const elemX = elem.getAttribute('x');
186
- const elemY = elem.getAttribute('y');
187
- if (elemX && elemY) {
188
- const x = parseFloat(elemX);
189
- const y = parseFloat(elemY);
190
- if (!isNaN(x) && !isNaN(y)) {
191
- supportedAttrs.push('x', 'y');
192
- transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
193
- }
194
- }
211
+ const transform = this.getTransform(elem, supportedAttrs, computedStyles);
195
212
  const result = new Text(contentList, transform, style);
196
213
  this.attachUnrecognisedAttrs(result, elem, new Set(supportedAttrs), new Set(supportedStyleAttrs));
197
214
  return result;
@@ -203,14 +220,34 @@ export default class SVGLoader {
203
220
  (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, textElem);
204
221
  }
205
222
  catch (e) {
206
- console.error('Invalid text object in node', elem, '. Adding as an unknown object. Error:', e);
223
+ console.error('Invalid text object in node', elem, '. Continuing.... Error:', e);
207
224
  this.addUnknownNode(elem);
208
225
  }
209
226
  }
227
+ addImage(elem) {
228
+ var _a, _b;
229
+ return __awaiter(this, void 0, void 0, function* () {
230
+ const image = new Image();
231
+ image.src = (_a = elem.getAttribute('xlink:href')) !== null && _a !== void 0 ? _a : elem.href.baseVal;
232
+ try {
233
+ const supportedAttrs = [];
234
+ const transform = this.getTransform(elem, supportedAttrs);
235
+ const imageElem = yield ImageComponent.fromImage(image, transform);
236
+ this.attachUnrecognisedAttrs(imageElem, elem, new Set(supportedAttrs), new Set(['transform']));
237
+ (_b = this.onAddComponent) === null || _b === void 0 ? void 0 : _b.call(this, imageElem);
238
+ }
239
+ catch (e) {
240
+ console.error('Error loading image:', e, '. Element: ', elem, '. Continuing...');
241
+ this.addUnknownNode(elem);
242
+ }
243
+ });
244
+ }
210
245
  addUnknownNode(node) {
211
246
  var _a;
212
- const component = new UnknownSVGObject(node);
213
- (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, component);
247
+ if (this.storeUnknown) {
248
+ const component = new UnknownSVGObject(node);
249
+ (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, component);
250
+ }
214
251
  }
215
252
  updateViewBox(node) {
216
253
  var _a;
@@ -232,7 +269,9 @@ export default class SVGLoader {
232
269
  }
233
270
  updateSVGAttrs(node) {
234
271
  var _a;
235
- (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
272
+ if (this.storeUnknown) {
273
+ (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
274
+ }
236
275
  }
237
276
  visit(node) {
238
277
  var _a;
@@ -250,6 +289,11 @@ export default class SVGLoader {
250
289
  this.addText(node);
251
290
  visitChildren = false;
252
291
  break;
292
+ case 'image':
293
+ yield this.addImage(node);
294
+ // Images should not have children.
295
+ visitChildren = false;
296
+ break;
253
297
  case 'svg':
254
298
  this.updateViewBox(node);
255
299
  this.updateSVGAttrs(node);
@@ -257,7 +301,7 @@ export default class SVGLoader {
257
301
  default:
258
302
  console.warn('Unknown SVG element,', node);
259
303
  if (!(node instanceof SVGElement)) {
260
- console.warn('Element', node, 'is not an SVGElement! Continuing anyway.');
304
+ console.warn('Element', node, 'is not an SVGElement!', this.storeUnknown ? 'Continuing anyway.' : 'Skipping.');
261
305
  }
262
306
  this.addUnknownNode(node);
263
307
  return;
@@ -296,7 +340,8 @@ export default class SVGLoader {
296
340
  });
297
341
  }
298
342
  // TODO: Handling unsafe data! Tripple-check that this is secure!
299
- static fromString(text) {
343
+ // @param sanitize - if `true`, don't store unknown attributes.
344
+ static fromString(text, sanitize = false) {
300
345
  var _a, _b;
301
346
  const sandbox = document.createElement('iframe');
302
347
  sandbox.src = 'about:blank';
@@ -336,6 +381,6 @@ export default class SVGLoader {
336
381
  return new SVGLoader(svgElem, () => {
337
382
  svgElem.remove();
338
383
  sandbox.remove();
339
- });
384
+ }, !sanitize);
340
385
  }
341
386
  }
@@ -8,6 +8,7 @@ declare class UndoRedoHistory {
8
8
  private announceUndoCallback;
9
9
  private undoStack;
10
10
  private redoStack;
11
+ private maxUndoRedoStackSize;
11
12
  constructor(editor: Editor, announceRedoCallback: AnnounceRedoCallback, announceUndoCallback: AnnounceUndoCallback);
12
13
  private fireUpdateEvent;
13
14
  push(command: Command, apply?: boolean): void;