js-draw 0.11.3 → 0.12.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 (40) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Color4.d.ts +1 -0
  4. package/dist/src/Color4.js +1 -0
  5. package/dist/src/Editor.js +4 -5
  6. package/dist/src/SVGLoader.js +43 -35
  7. package/dist/src/components/AbstractComponent.d.ts +1 -0
  8. package/dist/src/components/AbstractComponent.js +15 -0
  9. package/dist/src/toolbar/HTMLToolbar.d.ts +51 -0
  10. package/dist/src/toolbar/HTMLToolbar.js +63 -5
  11. package/dist/src/toolbar/IconProvider.d.ts +1 -1
  12. package/dist/src/toolbar/IconProvider.js +33 -6
  13. package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +8 -1
  14. package/dist/src/toolbar/widgets/EraserToolWidget.js +45 -4
  15. package/dist/src/toolbar/widgets/PenToolWidget.js +2 -2
  16. package/dist/src/toolbar/widgets/SelectionToolWidget.js +12 -3
  17. package/dist/src/tools/Eraser.d.ts +10 -1
  18. package/dist/src/tools/Eraser.js +65 -13
  19. package/dist/src/tools/SelectionTool/Selection.d.ts +4 -1
  20. package/dist/src/tools/SelectionTool/Selection.js +64 -27
  21. package/dist/src/tools/SelectionTool/SelectionTool.js +3 -1
  22. package/dist/src/tools/TextTool.js +10 -6
  23. package/dist/src/types.d.ts +2 -2
  24. package/package.json +1 -1
  25. package/src/Color4.ts +1 -0
  26. package/src/Editor.ts +3 -4
  27. package/src/SVGLoader.ts +14 -14
  28. package/src/components/AbstractComponent.ts +19 -0
  29. package/src/toolbar/HTMLToolbar.ts +81 -5
  30. package/src/toolbar/IconProvider.ts +34 -6
  31. package/src/toolbar/widgets/EraserToolWidget.ts +64 -5
  32. package/src/toolbar/widgets/PenToolWidget.ts +2 -2
  33. package/src/toolbar/widgets/SelectionToolWidget.ts +2 -2
  34. package/src/tools/Eraser.test.ts +79 -0
  35. package/src/tools/Eraser.ts +81 -17
  36. package/src/tools/SelectionTool/Selection.ts +73 -23
  37. package/src/tools/SelectionTool/SelectionTool.test.ts +138 -21
  38. package/src/tools/SelectionTool/SelectionTool.ts +3 -1
  39. package/src/tools/TextTool.ts +14 -8
  40. package/src/types.ts +2 -2
@@ -5,8 +5,11 @@ import EditorImage from '../../EditorImage';
5
5
  import Path from '../../math/Path';
6
6
  import { Vec2 } from '../../math/Vec2';
7
7
  import { InputEvtType } from '../../types';
8
+ import Selection from './Selection';
8
9
  import SelectionTool from './SelectionTool';
9
10
  import createEditor from '../../testing/createEditor';
11
+ import Pointer from '../../Pointer';
12
+ import { Rect2 } from '../../lib';
10
13
 
11
14
  const getSelectionTool = (editor: Editor): SelectionTool => {
12
15
  return editor.toolController.getMatchingTools(SelectionTool)[0];
@@ -22,6 +25,33 @@ const createSquareStroke = (size: number = 1) => {
22
25
  return { testStroke, addTestStrokeCommand };
23
26
  };
24
27
 
28
+ const createEditorWithSingleObjectSelection = (objectSize: number = 50) => {
29
+ const { testStroke, addTestStrokeCommand } = createSquareStroke(objectSize);
30
+ const editor = createEditor();
31
+ editor.dispatch(addTestStrokeCommand);
32
+
33
+ // Select the object
34
+ const selectionTool = getSelectionTool(editor);
35
+ selectionTool.setEnabled(true);
36
+ editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
37
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10));
38
+ editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(5, 5));
39
+
40
+ return { editor, testStroke, selectionTool };
41
+ };
42
+
43
+ const dragSelection = (editor: Editor, selection: Selection, startPt: Vec2, endPt: Vec2) => {
44
+ const backgroundElem = selection.getBackgroundElem();
45
+
46
+ selection.onDragStart(Pointer.ofCanvasPoint(startPt, true, editor.viewport), backgroundElem);
47
+ jest.advanceTimersByTime(100);
48
+
49
+ selection.onDragUpdate(Pointer.ofCanvasPoint(endPt, true, editor.viewport));
50
+ jest.advanceTimersByTime(100);
51
+
52
+ selection.onDragEnd();
53
+ };
54
+
25
55
  describe('SelectionTool', () => {
26
56
  it('selection should shrink/grow to bounding box of selected objects', () => {
27
57
  const { addTestStrokeCommand } = createSquareStroke();
@@ -47,17 +77,7 @@ describe('SelectionTool', () => {
47
77
  });
48
78
 
49
79
  it('sending keyboard events to the selected region should move selected items', () => {
50
- const { testStroke, addTestStrokeCommand } = createSquareStroke(50);
51
- const editor = createEditor();
52
- editor.dispatch(addTestStrokeCommand);
53
-
54
- // Select the object
55
- const selectionTool = getSelectionTool(editor);
56
- selectionTool.setEnabled(true);
57
- editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
58
- editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10));
59
- editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(5, 5));
60
-
80
+ const { editor, selectionTool, testStroke } = createEditorWithSingleObjectSelection(50);
61
81
  const selection = selectionTool.getSelection();
62
82
  expect(selection).not.toBeNull();
63
83
 
@@ -78,16 +98,7 @@ describe('SelectionTool', () => {
78
98
  });
79
99
 
80
100
  it('moving the selection with a keyboard should move the view to keep the selection in view', () => {
81
- const { addTestStrokeCommand } = createSquareStroke(100);
82
- const editor = createEditor();
83
- editor.dispatch(addTestStrokeCommand);
84
-
85
- // Select the stroke
86
- const selectionTool = getSelectionTool(editor);
87
- selectionTool.setEnabled(true);
88
- editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
89
- editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10));
90
- editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(100, 100));
101
+ const { editor, selectionTool } = createEditorWithSingleObjectSelection(50);
91
102
 
92
103
  const selection = selectionTool.getSelection();
93
104
  if (selection === null) {
@@ -140,4 +151,110 @@ describe('SelectionTool', () => {
140
151
  editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(201, 201));
141
152
  expect(selectionTool.getSelectedObjects()).toHaveLength(0);
142
153
  });
154
+
155
+ it('should remove the selection from the document while dragging', () => {
156
+ const { editor, selectionTool } = createEditorWithSingleObjectSelection(50);
157
+
158
+ const selection = selectionTool.getSelection()!;
159
+ const backgroundElem = selection.getBackgroundElem();
160
+ selection.onDragStart(Pointer.ofCanvasPoint(Vec2.of(0, 0), true, editor.viewport), backgroundElem);
161
+ jest.advanceTimersByTime(100);
162
+ selection.onDragUpdate(Pointer.ofCanvasPoint(Vec2.of(20, 0), true, editor.viewport));
163
+ jest.advanceTimersByTime(100);
164
+
165
+ // Expect the selection to not be in the image while dragging
166
+ expect(editor.image.getAllElements()).toHaveLength(0);
167
+
168
+ selection.onDragEnd();
169
+
170
+ expect(editor.image.getAllElements()).toHaveLength(1);
171
+ });
172
+
173
+ it('should drag objects horizontally', () => {
174
+ const { editor, selectionTool, testStroke } = createEditorWithSingleObjectSelection(50);
175
+
176
+ expect(editor.image.findParent(testStroke)).not.toBeNull();
177
+ expect(testStroke.getBBox().topLeft).objEq(Vec2.of(0, 0));
178
+
179
+ const selection = selectionTool.getSelection()!;
180
+ dragSelection(editor, selection, Vec2.of(0, 0), Vec2.of(10, 0));
181
+
182
+ expect(editor.image.findParent(testStroke)).not.toBeNull();
183
+ expect(testStroke.getBBox().topLeft).objEq(Vec2.of(10, 0));
184
+ });
185
+
186
+ it('should round changes in objects positions when dragging', () => {
187
+ const { editor, selectionTool, testStroke } = createEditorWithSingleObjectSelection(50);
188
+
189
+ expect(editor.image.findParent(testStroke)).not.toBeNull();
190
+ expect(testStroke.getBBox().topLeft).objEq(Vec2.of(0, 0));
191
+
192
+ const selection = selectionTool.getSelection()!;
193
+ dragSelection(editor, selection, Vec2.of(0, 0), Vec2.of(9.999, 0));
194
+
195
+ expect(editor.image.findParent(testStroke)).not.toBeNull();
196
+ expect(testStroke.getBBox().topLeft).objEq(Vec2.of(10, 0));
197
+ });
198
+
199
+ it('dragCancel should return a selection to its original position', () => {
200
+ const { editor, selectionTool, testStroke } = createEditorWithSingleObjectSelection(150);
201
+
202
+ const selection = selectionTool.getSelection()!;
203
+ const dragBackground = selection.getBackgroundElem();
204
+
205
+ expect(testStroke.getBBox().topLeft).objEq(Vec2.zero);
206
+
207
+ selection.onDragStart(Pointer.ofCanvasPoint(Vec2.of(10, 0), true, editor.viewport), dragBackground);
208
+ jest.advanceTimersByTime(100);
209
+ selection.onDragUpdate(Pointer.ofCanvasPoint(Vec2.of(200, 10), true, editor.viewport));
210
+ jest.advanceTimersByTime(100);
211
+ selection.onDragCancel();
212
+
213
+ expect(testStroke.getBBox().topLeft).objEq(Vec2.zero);
214
+ expect(editor.image.findParent(testStroke)).not.toBeNull();
215
+ });
216
+
217
+ it('duplicateSelectedObjects should duplicate a selection while dragging', async () => {
218
+ const { editor, selectionTool, testStroke } = createEditorWithSingleObjectSelection(150);
219
+
220
+ const selection = selectionTool.getSelection()!;
221
+ const dragBackground = selection.getBackgroundElem();
222
+
223
+ selection.onDragStart(Pointer.ofCanvasPoint(Vec2.of(0, 0), true, editor.viewport), dragBackground);
224
+ jest.advanceTimersByTime(100);
225
+ selection.onDragUpdate(Pointer.ofCanvasPoint(Vec2.of(20, 0), true, editor.viewport));
226
+
227
+ // The selection should not be in the document while dragging
228
+ expect(editor.image.findParent(testStroke)).toBeNull();
229
+
230
+ await editor.dispatch(await selection.duplicateSelectedObjects());
231
+ jest.advanceTimersByTime(100);
232
+
233
+ // The duplicate stroke should be added to the document, but the original should not.
234
+ expect(editor.image.findParent(testStroke)).toBeNull();
235
+
236
+ const allObjectsInImage = editor.image.getAllElements();
237
+ expect(allObjectsInImage).toHaveLength(1);
238
+
239
+ const duplicateObject = allObjectsInImage[0];
240
+
241
+ // The duplicate stroke should be translated
242
+ expect(duplicateObject.getBBox()).objEq(new Rect2(20, 0, 150, 150));
243
+
244
+ // The duplicate stroke should be selected.
245
+ expect(selection.getSelectedObjects()).toHaveLength(1);
246
+
247
+ // The test stroke should not be added to the document
248
+ // (esp if we continue dragging)
249
+ selection.onDragUpdate(Pointer.ofCanvasPoint(Vec2.of(30, 10), true, editor.viewport));
250
+ jest.advanceTimersByTime(100);
251
+
252
+ expect(editor.image.findParent(testStroke)).toBeNull();
253
+
254
+ // The test stroke should be translated when we finish dragging.
255
+ selection.onDragEnd();
256
+
257
+ expect(editor.image.findParent(testStroke)).not.toBeNull();
258
+ expect(testStroke.getBBox()).objEq(new Rect2(30, 10, 150, 150));
259
+ });
143
260
  });
@@ -296,7 +296,9 @@ export default class SelectionTool extends BaseTool {
296
296
  }
297
297
  else if (evt.ctrlKey) {
298
298
  if (this.selectionBox && evt.key === 'd') {
299
- this.editor.dispatch(this.selectionBox.duplicateSelectedObjects());
299
+ this.selectionBox.duplicateSelectedObjects().then(command => {
300
+ this.editor.dispatch(command);
301
+ });
300
302
  return true;
301
303
  }
302
304
  else if (evt.key === 'a') {
@@ -80,11 +80,14 @@ export default class TextTool extends BaseTool {
80
80
  if (this.textInputElem && this.textTargetPosition) {
81
81
  const content = this.textInputElem.value.trimEnd();
82
82
 
83
+ this.textInputElem.value = '';
84
+
83
85
  if (removeInput) {
84
- this.textInputElem.remove();
86
+ // In some browsers, .remove() triggers a .blur event (synchronously).
87
+ // Clear this.textInputElem before removal
88
+ const input = this.textInputElem;
85
89
  this.textInputElem = null;
86
- } else {
87
- this.textInputElem.value = '';
90
+ input.remove();
88
91
  }
89
92
 
90
93
  if (content === '') {
@@ -100,14 +103,14 @@ export default class TextTool extends BaseTool {
100
103
  ).rightMul(
101
104
  Mat33.zRotation(this.textRotation)
102
105
  );
103
-
106
+
104
107
  const textComponent = TextComponent.fromLines(content.split('\n'), textTransform, this.textStyle);
105
108
 
106
109
  const action = EditorImage.addElement(textComponent);
107
110
  if (this.removeExistingCommand) {
108
111
  // Unapply so that `removeExistingCommand` can be added to the undo stack.
109
112
  this.removeExistingCommand.unapply(this.editor);
110
-
113
+
111
114
  this.editor.dispatch(uniteCommands([ this.removeExistingCommand, action ]));
112
115
  this.removeExistingCommand = null;
113
116
  } else {
@@ -177,10 +180,13 @@ export default class TextTool extends BaseTool {
177
180
  // Delay removing the input -- flushInput may be called within a blur()
178
181
  // event handler
179
182
  const removeInput = false;
180
- this.flushInput(removeInput);
181
-
182
183
  const input = this.textInputElem;
183
- setTimeout(() => input?.remove(), 0);
184
+
185
+ this.flushInput(removeInput);
186
+ this.textInputElem = null;
187
+ setTimeout(() => {
188
+ input?.remove();
189
+ }, 0);
184
190
  };
185
191
  this.textInputElem.onkeyup = (evt) => {
186
192
  if (evt.key === 'Enter' && !evt.shiftKey) {
package/src/types.ts CHANGED
@@ -198,9 +198,9 @@ export type EditorEventDataType = EditorToolEvent | EditorObjectEvent
198
198
  // Returns null to continue loading without pause.
199
199
  // [totalToProcess] can be an estimate and may change if a better estimate becomes available.
200
200
  export type OnProgressListener =
201
- (amountProcessed: number, totalToProcess: number)=> Promise<void>|null;
201
+ (amountProcessed: number, totalToProcess: number)=> Promise<void>|null|void;
202
202
 
203
- export type ComponentAddedListener = (component: AbstractComponent)=> void;
203
+ export type ComponentAddedListener = (component: AbstractComponent)=> Promise<void>|void;
204
204
 
205
205
  // Called when a new estimate for the import/export rect has been generated. This can be called multiple times.
206
206
  // Only the last call to this listener must be accurate.