js-draw 0.3.1 → 0.3.2

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 (84) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.md +4 -1
  2. package/CHANGELOG.md +8 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +4 -1
  5. package/dist/src/Editor.js +117 -2
  6. package/dist/src/EditorImage.js +4 -1
  7. package/dist/src/SVGLoader.d.ts +4 -1
  8. package/dist/src/SVGLoader.js +78 -33
  9. package/dist/src/UndoRedoHistory.d.ts +1 -0
  10. package/dist/src/UndoRedoHistory.js +6 -0
  11. package/dist/src/Viewport.d.ts +1 -0
  12. package/dist/src/Viewport.js +12 -4
  13. package/dist/src/commands/lib.d.ts +2 -1
  14. package/dist/src/commands/lib.js +2 -1
  15. package/dist/src/commands/localization.d.ts +1 -0
  16. package/dist/src/commands/localization.js +1 -0
  17. package/dist/src/commands/uniteCommands.d.ts +4 -0
  18. package/dist/src/commands/uniteCommands.js +105 -0
  19. package/dist/src/components/AbstractComponent.d.ts +2 -0
  20. package/dist/src/components/AbstractComponent.js +41 -5
  21. package/dist/src/components/ImageComponent.d.ts +27 -0
  22. package/dist/src/components/ImageComponent.js +129 -0
  23. package/dist/src/components/builders/FreehandLineBuilder.js +2 -2
  24. package/dist/src/components/lib.d.ts +4 -2
  25. package/dist/src/components/lib.js +4 -2
  26. package/dist/src/components/localization.d.ts +2 -0
  27. package/dist/src/components/localization.js +2 -0
  28. package/dist/src/math/LineSegment2.d.ts +2 -0
  29. package/dist/src/math/LineSegment2.js +3 -0
  30. package/dist/src/rendering/localization.d.ts +3 -0
  31. package/dist/src/rendering/localization.js +3 -0
  32. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -0
  33. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  34. package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
  35. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
  36. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  37. package/dist/src/rendering/renderers/SVGRenderer.d.ts +5 -2
  38. package/dist/src/rendering/renderers/SVGRenderer.js +45 -20
  39. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
  40. package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
  41. package/dist/src/tools/BaseTool.d.ts +3 -1
  42. package/dist/src/tools/BaseTool.js +6 -0
  43. package/dist/src/tools/PasteHandler.d.ts +16 -0
  44. package/dist/src/tools/PasteHandler.js +142 -0
  45. package/dist/src/tools/SelectionTool.d.ts +7 -1
  46. package/dist/src/tools/SelectionTool.js +63 -5
  47. package/dist/src/tools/ToolController.js +36 -27
  48. package/dist/src/tools/lib.d.ts +1 -0
  49. package/dist/src/tools/lib.js +1 -0
  50. package/dist/src/tools/localization.d.ts +3 -0
  51. package/dist/src/tools/localization.js +3 -0
  52. package/dist/src/types.d.ts +13 -2
  53. package/dist/src/types.js +2 -0
  54. package/package.json +1 -1
  55. package/src/Editor.ts +131 -2
  56. package/src/EditorImage.ts +7 -1
  57. package/src/SVGLoader.ts +90 -36
  58. package/src/UndoRedoHistory.test.ts +33 -0
  59. package/src/UndoRedoHistory.ts +8 -0
  60. package/src/Viewport.ts +13 -4
  61. package/src/commands/lib.ts +2 -0
  62. package/src/commands/localization.ts +2 -0
  63. package/src/commands/uniteCommands.test.ts +23 -0
  64. package/src/commands/uniteCommands.ts +121 -0
  65. package/src/components/AbstractComponent.ts +55 -9
  66. package/src/components/ImageComponent.ts +153 -0
  67. package/src/components/builders/FreehandLineBuilder.ts +2 -2
  68. package/src/components/lib.ts +7 -2
  69. package/src/components/localization.ts +4 -0
  70. package/src/math/LineSegment2.test.ts +9 -0
  71. package/src/math/LineSegment2.ts +5 -0
  72. package/src/rendering/localization.ts +6 -0
  73. package/src/rendering/renderers/AbstractRenderer.ts +16 -0
  74. package/src/rendering/renderers/CanvasRenderer.ts +10 -1
  75. package/src/rendering/renderers/DummyRenderer.ts +6 -1
  76. package/src/rendering/renderers/SVGRenderer.ts +50 -21
  77. package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
  78. package/src/tools/BaseTool.ts +9 -1
  79. package/src/tools/PasteHandler.ts +156 -0
  80. package/src/tools/SelectionTool.ts +80 -8
  81. package/src/tools/ToolController.ts +51 -44
  82. package/src/tools/lib.ts +1 -0
  83. package/src/tools/localization.ts +8 -0
  84. package/src/types.ts +16 -2
@@ -13,6 +13,12 @@ export default class BaseTool {
13
13
  onWheel(_event) {
14
14
  return false;
15
15
  }
16
+ onCopy(_event) {
17
+ return false;
18
+ }
19
+ onPaste(_event) {
20
+ return false;
21
+ }
16
22
  onKeyPress(_event) {
17
23
  return false;
18
24
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A tool that handles paste events.
3
+ * @packageDocumentation
4
+ */
5
+ import Editor from '../Editor';
6
+ import { PasteEvent } from '../types';
7
+ import BaseTool from './BaseTool';
8
+ export default class PasteHandler extends BaseTool {
9
+ private editor;
10
+ constructor(editor: Editor);
11
+ onPaste(event: PasteEvent): boolean;
12
+ private addComponentsFromPaste;
13
+ private doSVGPaste;
14
+ private doTextPaste;
15
+ private doImagePaste;
16
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * A tool that handles paste events.
3
+ * @packageDocumentation
4
+ */
5
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
7
+ return new (P || (P = Promise))(function (resolve, reject) {
8
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
9
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
10
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
11
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
12
+ });
13
+ };
14
+ import { TextComponent } from '../components/lib';
15
+ import { uniteCommands } from '../commands/lib';
16
+ import SVGLoader from '../SVGLoader';
17
+ import { Mat33, Vec2 } from '../math/lib';
18
+ import BaseTool from './BaseTool';
19
+ import EditorImage from '../EditorImage';
20
+ import SelectionTool from './SelectionTool';
21
+ import TextTool from './TextTool';
22
+ import Color4 from '../Color4';
23
+ import ImageComponent from '../components/ImageComponent';
24
+ // { @inheritDoc PasteHandler! }
25
+ export default class PasteHandler extends BaseTool {
26
+ constructor(editor) {
27
+ super(editor.notifier, editor.localization.pasteHandler);
28
+ this.editor = editor;
29
+ }
30
+ onPaste(event) {
31
+ const mime = event.mime.toLowerCase();
32
+ if (mime === 'image/svg+xml') {
33
+ void this.doSVGPaste(event.data);
34
+ return true;
35
+ }
36
+ else if (mime === 'text/plain') {
37
+ void this.doTextPaste(event.data);
38
+ return true;
39
+ }
40
+ else if (mime === 'image/png' || mime === 'image/jpeg') {
41
+ void this.doImagePaste(event.data);
42
+ return true;
43
+ }
44
+ return false;
45
+ }
46
+ addComponentsFromPaste(components) {
47
+ return __awaiter(this, void 0, void 0, function* () {
48
+ let bbox = null;
49
+ for (const component of components) {
50
+ if (bbox) {
51
+ bbox = bbox.union(component.getBBox());
52
+ }
53
+ else {
54
+ bbox = component.getBBox();
55
+ }
56
+ }
57
+ if (!bbox) {
58
+ return;
59
+ }
60
+ // Find a transform that scales/moves bbox onto the screen.
61
+ const visibleRect = this.editor.viewport.visibleRect;
62
+ const scaleRatioX = visibleRect.width / bbox.width;
63
+ const scaleRatioY = visibleRect.height / bbox.height;
64
+ let scaleRatio = scaleRatioX;
65
+ if (bbox.width * scaleRatio > visibleRect.width || bbox.height * scaleRatio > visibleRect.height) {
66
+ scaleRatio = scaleRatioY;
67
+ }
68
+ scaleRatio *= 2 / 3;
69
+ const transfm = Mat33.translation(visibleRect.center.minus(bbox.center)).rightMul(Mat33.scaling2D(scaleRatio, bbox.center));
70
+ const commands = [];
71
+ for (const component of components) {
72
+ // To allow deserialization, we need to add first, then transform.
73
+ commands.push(EditorImage.addElement(component));
74
+ commands.push(component.transformBy(transfm));
75
+ }
76
+ const applyChunkSize = 100;
77
+ this.editor.dispatch(uniteCommands(commands, applyChunkSize), true);
78
+ for (const selectionTool of this.editor.toolController.getMatchingTools(SelectionTool)) {
79
+ selectionTool.setEnabled(true);
80
+ selectionTool.setSelection(components);
81
+ }
82
+ });
83
+ }
84
+ doSVGPaste(data) {
85
+ return __awaiter(this, void 0, void 0, function* () {
86
+ const sanitize = true;
87
+ const loader = SVGLoader.fromString(data, sanitize);
88
+ const components = [];
89
+ yield loader.start((component) => {
90
+ components.push(component);
91
+ }, (_countProcessed, _totalToProcess) => null);
92
+ yield this.addComponentsFromPaste(components);
93
+ });
94
+ }
95
+ doTextPaste(text) {
96
+ var _a, _b;
97
+ return __awaiter(this, void 0, void 0, function* () {
98
+ const textTools = this.editor.toolController.getMatchingTools(TextTool);
99
+ textTools.sort((a, b) => {
100
+ if (!a.isEnabled() && b.isEnabled()) {
101
+ return -1;
102
+ }
103
+ if (!b.isEnabled() && a.isEnabled()) {
104
+ return 1;
105
+ }
106
+ return 0;
107
+ });
108
+ const defaultTextStyle = { size: 12, fontFamily: 'sans', renderingStyle: { fill: Color4.red } };
109
+ const pastedTextStyle = (_b = (_a = textTools[0]) === null || _a === void 0 ? void 0 : _a.getTextStyle()) !== null && _b !== void 0 ? _b : defaultTextStyle;
110
+ const lines = text.split('\n');
111
+ let lastComponent = null;
112
+ const components = [];
113
+ for (const line of lines) {
114
+ let position = Vec2.zero;
115
+ if (lastComponent) {
116
+ const lineMargin = Math.floor(pastedTextStyle.size);
117
+ position = lastComponent.getBBox().bottomLeft.plus(Vec2.unitY.times(lineMargin));
118
+ }
119
+ const component = new TextComponent([line], Mat33.translation(position), pastedTextStyle);
120
+ components.push(component);
121
+ lastComponent = component;
122
+ }
123
+ if (components.length === 1) {
124
+ yield this.addComponentsFromPaste([components[0]]);
125
+ }
126
+ else {
127
+ // Wrap the existing `TextComponent`s --- dragging one component should drag all.
128
+ yield this.addComponentsFromPaste([
129
+ new TextComponent(components, Mat33.identity, pastedTextStyle)
130
+ ]);
131
+ }
132
+ });
133
+ }
134
+ doImagePaste(dataURL) {
135
+ return __awaiter(this, void 0, void 0, function* () {
136
+ const image = new Image();
137
+ image.src = dataURL;
138
+ const component = yield ImageComponent.fromImage(image, Mat33.identity);
139
+ yield this.addComponentsFromPaste([component]);
140
+ });
141
+ }
142
+ }
@@ -1,9 +1,10 @@
1
1
  import Command from '../commands/Command';
2
+ import AbstractComponent from '../components/AbstractComponent';
2
3
  import Editor from '../Editor';
3
4
  import Mat33 from '../math/Mat33';
4
5
  import Rect2 from '../math/Rect2';
5
6
  import { Point2, Vec2 } from '../math/Vec2';
6
- import { KeyPressEvent, KeyUpEvent, PointerEvt } from '../types';
7
+ import { CopyEvent, KeyPressEvent, KeyUpEvent, PointerEvt } from '../types';
7
8
  import BaseTool from './BaseTool';
8
9
  declare class Selection {
9
10
  startPoint: Point2;
@@ -27,6 +28,8 @@ declare class Selection {
27
28
  appendBackgroundBoxTo(elem: HTMLElement): void;
28
29
  setToPoint(point: Point2): void;
29
30
  cancelSelection(): void;
31
+ setSelectedObjects(objects: AbstractComponent[], bbox: Rect2): void;
32
+ getSelectedObjects(): AbstractComponent[];
30
33
  resolveToObjects(): boolean;
31
34
  recomputeRegion(): boolean;
32
35
  getMinCanvasSize(): number;
@@ -43,6 +46,7 @@ export default class SelectionTool extends BaseTool {
43
46
  private prevSelectionBox;
44
47
  private selectionBox;
45
48
  constructor(editor: Editor, description: string);
49
+ private makeSelectionBox;
46
50
  onPointerDown(event: PointerEvt): boolean;
47
51
  onPointerMove(event: PointerEvt): void;
48
52
  private onGestureEnd;
@@ -52,8 +56,10 @@ export default class SelectionTool extends BaseTool {
52
56
  private static handleableKeys;
53
57
  onKeyPress(event: KeyPressEvent): boolean;
54
58
  onKeyUp(evt: KeyUpEvent): boolean;
59
+ onCopy(event: CopyEvent): boolean;
55
60
  setEnabled(enabled: boolean): void;
56
61
  getSelection(): Selection | null;
62
+ setSelection(objects: AbstractComponent[]): void;
57
63
  clearSelection(): void;
58
64
  }
59
65
  export {};
@@ -20,6 +20,7 @@ import { EditorEventType } from '../types';
20
20
  import Viewport from '../Viewport';
21
21
  import BaseTool from './BaseTool';
22
22
  import SerializableCommand from '../commands/SerializableCommand';
23
+ import SVGRenderer from '../rendering/renderers/SVGRenderer';
23
24
  const handleScreenSize = 30;
24
25
  const styles = `
25
26
  .handleOverlay {
@@ -262,6 +263,14 @@ class Selection {
262
263
  }
263
264
  this.region = Rect2.empty;
264
265
  }
266
+ setSelectedObjects(objects, bbox) {
267
+ this.region = bbox;
268
+ this.selectedElems = objects;
269
+ this.updateUI();
270
+ }
271
+ getSelectedObjects() {
272
+ return this.selectedElems;
273
+ }
265
274
  // Find the objects corresponding to this in the document,
266
275
  // select them.
267
276
  // Returns false iff nothing was selected.
@@ -427,13 +436,16 @@ export default class SelectionTool extends BaseTool {
427
436
  });
428
437
  this.editor.handleKeyEventsFrom(this.handleOverlay);
429
438
  }
439
+ makeSelectionBox(selectionStartPos) {
440
+ this.prevSelectionBox = this.selectionBox;
441
+ this.selectionBox = new Selection(selectionStartPos, this.editor);
442
+ // Remove any previous selection rects
443
+ this.handleOverlay.replaceChildren();
444
+ this.selectionBox.appendBackgroundBoxTo(this.handleOverlay);
445
+ }
430
446
  onPointerDown(event) {
431
447
  if (event.allPointers.length === 1 && event.current.isPrimary) {
432
- this.prevSelectionBox = this.selectionBox;
433
- this.selectionBox = new Selection(event.current.canvasPos, this.editor);
434
- // Remove any previous selection rects
435
- this.handleOverlay.replaceChildren();
436
- this.selectionBox.appendBackgroundBoxTo(this.handleOverlay);
448
+ this.makeSelectionBox(event.current.canvasPos);
437
449
  return true;
438
450
  }
439
451
  return false;
@@ -542,6 +554,11 @@ export default class SelectionTool extends BaseTool {
542
554
  Math.max(0.5, scaledSize.x / region.size.x), Math.max(0.5, scaledSize.y / region.size.y)), region.topLeft).rightMul(Mat33.zRotation(rotationSteps * rotateStepSize, region.center)).rightMul(Mat33.translation(Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize)));
543
555
  this.selectionBox.transformPreview(transform);
544
556
  }
557
+ if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
558
+ this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
559
+ this.clearSelection();
560
+ handled = true;
561
+ }
545
562
  return handled;
546
563
  }
547
564
  onKeyUp(evt) {
@@ -551,6 +568,28 @@ export default class SelectionTool extends BaseTool {
551
568
  }
552
569
  return false;
553
570
  }
571
+ onCopy(event) {
572
+ if (!this.selectionBox) {
573
+ return false;
574
+ }
575
+ const selectedElems = this.selectionBox.getSelectedObjects();
576
+ const bbox = this.selectionBox.region;
577
+ if (selectedElems.length === 0) {
578
+ return false;
579
+ }
580
+ const exportViewport = new Viewport(this.editor.notifier);
581
+ exportViewport.updateScreenSize(Vec2.of(bbox.w, bbox.h));
582
+ exportViewport.resetTransform(Mat33.translation(bbox.topLeft));
583
+ const svgNameSpace = 'http://www.w3.org/2000/svg';
584
+ const exportElem = document.createElementNS(svgNameSpace, 'svg');
585
+ const sanitize = true;
586
+ const renderer = new SVGRenderer(exportElem, exportViewport, sanitize);
587
+ for (const elem of selectedElems) {
588
+ elem.render(renderer);
589
+ }
590
+ event.setData('image/svg+xml', exportElem.outerHTML);
591
+ return true;
592
+ }
554
593
  setEnabled(enabled) {
555
594
  super.setEnabled(enabled);
556
595
  // Clear the selection
@@ -569,6 +608,25 @@ export default class SelectionTool extends BaseTool {
569
608
  getSelection() {
570
609
  return this.selectionBox;
571
610
  }
611
+ setSelection(objects) {
612
+ let bbox = null;
613
+ for (const object of objects) {
614
+ if (bbox) {
615
+ bbox = bbox.union(object.getBBox());
616
+ }
617
+ else {
618
+ bbox = object.getBBox();
619
+ }
620
+ }
621
+ if (!bbox) {
622
+ return;
623
+ }
624
+ this.clearSelection();
625
+ if (!this.selectionBox) {
626
+ this.makeSelectionBox(bbox.topLeft);
627
+ }
628
+ this.selectionBox.setSelectedObjects(objects, bbox);
629
+ }
572
630
  clearSelection() {
573
631
  this.handleOverlay.replaceChildren();
574
632
  this.prevSelectionBox = this.selectionBox;
@@ -9,6 +9,7 @@ import UndoRedoShortcut from './UndoRedoShortcut';
9
9
  import TextTool from './TextTool';
10
10
  import PipetteTool from './PipetteTool';
11
11
  import ToolSwitcherShortcut from './ToolSwitcherShortcut';
12
+ import PasteHandler from './PasteHandler';
12
13
  export default class ToolController {
13
14
  /** @internal */
14
15
  constructor(editor, localization) {
@@ -35,6 +36,7 @@ export default class ToolController {
35
36
  keyboardPanZoomTool,
36
37
  new UndoRedoShortcut(editor),
37
38
  new ToolSwitcherShortcut(editor),
39
+ new PasteHandler(editor),
38
40
  ];
39
41
  primaryTools.forEach(tool => tool.setToolGroup(primaryToolGroup));
40
42
  panZoomTool.setEnabled(true);
@@ -97,42 +99,49 @@ export default class ToolController {
97
99
  this.activeTool = null;
98
100
  handled = true;
99
101
  }
100
- else if (event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent || event.kind === InputEvtType.KeyUpEvent) {
101
- const isKeyPressEvt = event.kind === InputEvtType.KeyPressEvent;
102
- const isKeyReleaseEvt = event.kind === InputEvtType.KeyUpEvent;
103
- const isWheelEvt = event.kind === InputEvtType.WheelEvt;
102
+ else if (event.kind === InputEvtType.PointerMoveEvt) {
103
+ if (this.activeTool !== null) {
104
+ this.activeTool.onPointerMove(event);
105
+ handled = true;
106
+ }
107
+ }
108
+ else if (event.kind === InputEvtType.GestureCancelEvt) {
109
+ if (this.activeTool !== null) {
110
+ this.activeTool.onGestureCancel();
111
+ this.activeTool = null;
112
+ }
113
+ }
114
+ else {
115
+ let allCasesHandledGuard;
104
116
  for (const tool of this.tools) {
105
117
  if (!tool.isEnabled()) {
106
118
  continue;
107
119
  }
108
- const wheelResult = isWheelEvt && tool.onWheel(event);
109
- const keyPressResult = isKeyPressEvt && tool.onKeyPress(event);
110
- const keyReleaseResult = isKeyReleaseEvt && tool.onKeyUp(event);
111
- handled = keyPressResult || wheelResult || keyReleaseResult;
120
+ switch (event.kind) {
121
+ case InputEvtType.KeyPressEvent:
122
+ handled = tool.onKeyPress(event);
123
+ break;
124
+ case InputEvtType.KeyUpEvent:
125
+ handled = tool.onKeyUp(event);
126
+ break;
127
+ case InputEvtType.WheelEvt:
128
+ handled = tool.onWheel(event);
129
+ break;
130
+ case InputEvtType.CopyEvent:
131
+ handled = tool.onCopy(event);
132
+ break;
133
+ case InputEvtType.PasteEvent:
134
+ handled = tool.onPaste(event);
135
+ break;
136
+ default:
137
+ allCasesHandledGuard = event;
138
+ return allCasesHandledGuard;
139
+ }
112
140
  if (handled) {
113
141
  break;
114
142
  }
115
143
  }
116
144
  }
117
- else if (this.activeTool !== null) {
118
- let allCasesHandledGuard;
119
- switch (event.kind) {
120
- case InputEvtType.PointerMoveEvt:
121
- this.activeTool.onPointerMove(event);
122
- break;
123
- case InputEvtType.GestureCancelEvt:
124
- this.activeTool.onGestureCancel();
125
- this.activeTool = null;
126
- break;
127
- default:
128
- allCasesHandledGuard = event;
129
- return allCasesHandledGuard;
130
- }
131
- handled = true;
132
- }
133
- else {
134
- handled = false;
135
- }
136
145
  return handled;
137
146
  }
138
147
  getMatchingTools(type) {
@@ -11,3 +11,4 @@ export { default as PenTool, PenStyle } from './Pen';
11
11
  export { default as TextTool } from './TextTool';
12
12
  export { default as SelectionTool } from './SelectionTool';
13
13
  export { default as EraserTool } from './Eraser';
14
+ export { default as PasteHandler } from './PasteHandler';
@@ -11,3 +11,4 @@ export { default as PenTool } from './Pen';
11
11
  export { default as TextTool } from './TextTool';
12
12
  export { default as SelectionTool } from './SelectionTool';
13
13
  export { default as EraserTool } from './Eraser';
14
+ export { default as PasteHandler } from './PasteHandler';
@@ -11,6 +11,9 @@ export interface ToolLocalization {
11
11
  textTool: string;
12
12
  enterTextToInsert: string;
13
13
  changeTool: string;
14
+ pasteHandler: string;
15
+ copied: (count: number, description: string) => string;
16
+ pasted: (count: number, description: string) => string;
14
17
  toolEnabledAnnouncement: (toolName: string) => string;
15
18
  toolDisabledAnnouncement: (toolName: string) => string;
16
19
  }
@@ -11,6 +11,9 @@ export const defaultToolLocalization = {
11
11
  textTool: 'Text',
12
12
  enterTextToInsert: 'Text to insert',
13
13
  changeTool: 'Change tool',
14
+ pasteHandler: 'Copy paste handler',
15
+ copied: (count, description) => `Copied ${count} ${description}`,
16
+ pasted: (count, description) => `Pasted ${count} ${description}`,
14
17
  toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
15
18
  toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,
16
19
  };
@@ -22,7 +22,9 @@ export declare enum InputEvtType {
22
22
  GestureCancelEvt = 3,
23
23
  WheelEvt = 4,
24
24
  KeyPressEvent = 5,
25
- KeyUpEvent = 6
25
+ KeyUpEvent = 6,
26
+ CopyEvent = 7,
27
+ PasteEvent = 8
26
28
  }
27
29
  export interface WheelEvt {
28
30
  readonly kind: InputEvtType.WheelEvt;
@@ -39,6 +41,15 @@ export interface KeyUpEvent {
39
41
  readonly key: string;
40
42
  readonly ctrlKey: boolean;
41
43
  }
44
+ export interface CopyEvent {
45
+ readonly kind: InputEvtType.CopyEvent;
46
+ setData(mime: string, data: string): void;
47
+ }
48
+ export interface PasteEvent {
49
+ readonly kind: InputEvtType.PasteEvent;
50
+ readonly data: string;
51
+ readonly mime: string;
52
+ }
42
53
  export interface GestureCancelEvt {
43
54
  readonly kind: InputEvtType.GestureCancelEvt;
44
55
  }
@@ -56,7 +67,7 @@ export interface PointerUpEvt extends PointerEvtBase {
56
67
  readonly kind: InputEvtType.PointerUpEvt;
57
68
  }
58
69
  export declare type PointerEvt = PointerDownEvt | PointerMoveEvt | PointerUpEvt;
59
- export declare type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt;
70
+ export declare type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt | CopyEvent | PasteEvent;
60
71
  export declare type EditorNotifier = EventDispatcher<EditorEventType, EditorEventDataType>;
61
72
  export declare enum EditorEventType {
62
73
  ToolEnabled = 0,
package/dist/src/types.js CHANGED
@@ -8,6 +8,8 @@ export var InputEvtType;
8
8
  InputEvtType[InputEvtType["WheelEvt"] = 4] = "WheelEvt";
9
9
  InputEvtType[InputEvtType["KeyPressEvent"] = 5] = "KeyPressEvent";
10
10
  InputEvtType[InputEvtType["KeyUpEvent"] = 6] = "KeyUpEvent";
11
+ InputEvtType[InputEvtType["CopyEvent"] = 7] = "CopyEvent";
12
+ InputEvtType[InputEvtType["PasteEvent"] = 8] = "PasteEvent";
11
13
  })(InputEvtType || (InputEvtType = {}));
12
14
  export var EditorEventType;
13
15
  (function (EditorEventType) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "./dist/src/lib.d.ts",
6
6
  "types": "./dist/src/lib.js",
package/src/Editor.ts CHANGED
@@ -118,6 +118,7 @@ export class Editor {
118
118
  private loadingWarning: HTMLElement;
119
119
  private accessibilityAnnounceArea: HTMLElement;
120
120
  private accessibilityControlArea: HTMLTextAreaElement;
121
+ private eventListenerTargets: HTMLElement[] = [];
121
122
 
122
123
  private settings: EditorSettings;
123
124
 
@@ -435,6 +436,121 @@ export class Editor {
435
436
  this.accessibilityControlArea.addEventListener('input', () => {
436
437
  this.accessibilityControlArea.value = '';
437
438
  });
439
+
440
+ document.addEventListener('copy', evt => {
441
+ if (!this.isEventSink(document.querySelector(':focus'))) {
442
+ return;
443
+ }
444
+
445
+ const clipboardData = evt.clipboardData;
446
+
447
+ if (this.toolController.dispatchInputEvent({
448
+ kind: InputEvtType.CopyEvent,
449
+ setData: (mime, data) => {
450
+ clipboardData?.setData(mime, data);
451
+ },
452
+ })) {
453
+ evt.preventDefault();
454
+ }
455
+ });
456
+
457
+ document.addEventListener('paste', evt => {
458
+ this.handlePaste(evt);
459
+ });
460
+ }
461
+
462
+ private isEventSink(evtTarget: Element|EventTarget|null) {
463
+ let currentElem: Element|null = evtTarget as Element|null;
464
+ while (currentElem !== null) {
465
+ for (const elem of this.eventListenerTargets) {
466
+ if (elem === currentElem) {
467
+ return true;
468
+ }
469
+ }
470
+
471
+ currentElem = (currentElem as Element).parentElement;
472
+ }
473
+ return false;
474
+ }
475
+
476
+ private async handlePaste(evt: DragEvent|ClipboardEvent) {
477
+ const target = document.querySelector(':focus') ?? evt.target;
478
+ if (!this.isEventSink(target)) {
479
+ return;
480
+ }
481
+
482
+ const clipboardData: DataTransfer = (evt as any).dataTransfer ?? (evt as any).clipboardData;
483
+ if (!clipboardData) {
484
+ return;
485
+ }
486
+
487
+ // Handle SVG files (prefer to PNG/JPEG)
488
+ for (const file of clipboardData.files) {
489
+ if (file.type.toLowerCase() === 'image/svg+xml') {
490
+ const text = await file.text();
491
+ if (this.toolController.dispatchInputEvent({
492
+ kind: InputEvtType.PasteEvent,
493
+ mime: file.type,
494
+ data: text,
495
+ })) {
496
+ evt.preventDefault();
497
+ return;
498
+ }
499
+ }
500
+ }
501
+
502
+ // Handle image files.
503
+ for (const file of clipboardData.files) {
504
+ const fileType = file.type.toLowerCase();
505
+ if (fileType === 'image/png' || fileType === 'image/jpg') {
506
+ const reader = new FileReader();
507
+
508
+ this.showLoadingWarning(0);
509
+ try {
510
+ const data = await new Promise((resolve: (result: string|null)=>void, reject) => {
511
+ reader.onload = () => resolve(reader.result as string|null);
512
+ reader.onerror = reject;
513
+ reader.onabort = reject;
514
+ reader.onprogress = (evt) => {
515
+ this.showLoadingWarning(evt.loaded / evt.total);
516
+ };
517
+
518
+ reader.readAsDataURL(file);
519
+ });
520
+ if (data && this.toolController.dispatchInputEvent({
521
+ kind: InputEvtType.PasteEvent,
522
+ mime: fileType,
523
+ data: data,
524
+ })) {
525
+ evt.preventDefault();
526
+ this.hideLoadingWarning();
527
+ return;
528
+ }
529
+ } catch (e) {
530
+ console.error('Error reading image:', e);
531
+ }
532
+ this.hideLoadingWarning();
533
+ }
534
+ }
535
+
536
+ // Supported MIMEs for text data, in order of preference
537
+ const supportedMIMEs = [
538
+ 'image/svg+xml',
539
+ 'text/plain',
540
+ ];
541
+
542
+ for (const mime of supportedMIMEs) {
543
+ const data = clipboardData.getData(mime);
544
+
545
+ if (data && this.toolController.dispatchInputEvent({
546
+ kind: InputEvtType.PasteEvent,
547
+ mime,
548
+ data,
549
+ })) {
550
+ evt.preventDefault();
551
+ return;
552
+ }
553
+ }
438
554
  }
439
555
 
440
556
  /** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
@@ -463,6 +579,18 @@ export class Editor {
463
579
  evt.preventDefault();
464
580
  }
465
581
  });
582
+
583
+ // Allow drop.
584
+ elem.ondragover = evt => {
585
+ evt.preventDefault();
586
+ };
587
+
588
+ elem.ondrop = evt => {
589
+ evt.preventDefault();
590
+ this.handlePaste(evt);
591
+ };
592
+
593
+ this.eventListenerTargets.push(elem);
466
594
  }
467
595
 
468
596
  /** `apply` a command. `command` will be announced for accessibility. */
@@ -509,6 +637,7 @@ export class Editor {
509
637
  public async asyncApplyOrUnapplyCommands(
510
638
  commands: Command[], apply: boolean, updateChunkSize: number
511
639
  ) {
640
+ console.assert(updateChunkSize > 0);
512
641
  this.display.setDraftMode(true);
513
642
  for (let i = 0; i < commands.length; i += updateChunkSize) {
514
643
  this.showLoadingWarning(i / commands.length);
@@ -739,8 +868,8 @@ export class Editor {
739
868
  * This is particularly useful when accessing a bundled version of the editor,
740
869
  * where `SVGLoader.fromString` is unavailable.
741
870
  */
742
- public async loadFromSVG(svgData: string) {
743
- const loader = SVGLoader.fromString(svgData);
871
+ public async loadFromSVG(svgData: string, sanitize: boolean = false) {
872
+ const loader = SVGLoader.fromString(svgData, sanitize);
744
873
  await this.loadFrom(loader);
745
874
  }
746
875
  }