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
@@ -1,31 +1,55 @@
1
+ import { EditorEventType } from '../types';
1
2
  import BaseTool from './BaseTool';
3
+ import { Vec2 } from '../math/Vec2';
2
4
  import LineSegment2 from '../math/LineSegment2';
3
5
  import Erase from '../commands/Erase';
4
6
  import { PointerDevice } from '../Pointer';
7
+ import Color4 from '../Color4';
8
+ import Rect2 from '../math/Rect2';
5
9
  export default class Eraser extends BaseTool {
6
10
  constructor(editor, description) {
7
11
  super(editor.notifier, description);
8
12
  this.editor = editor;
13
+ this.lastPoint = null;
14
+ this.isFirstEraseEvt = true;
15
+ this.thickness = 10;
9
16
  // Commands that each remove one element
10
17
  this.partialCommands = [];
11
18
  }
12
- onPointerDown(event) {
13
- if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
14
- this.lastPoint = event.current.canvasPos;
15
- this.toRemove = [];
16
- return true;
17
- }
18
- return false;
19
+ clearPreview() {
20
+ this.editor.clearWetInk();
19
21
  }
20
- onPointerMove(event) {
21
- const currentPoint = event.current.canvasPos;
22
- if (currentPoint.minus(this.lastPoint).magnitude() === 0) {
22
+ getSizeOnCanvas() {
23
+ return this.thickness / this.editor.viewport.getScaleFactor();
24
+ }
25
+ drawPreviewAt(point) {
26
+ this.clearPreview();
27
+ const size = this.getSizeOnCanvas();
28
+ const renderer = this.editor.display.getWetInkRenderer();
29
+ const rect = this.getEraserRect(point);
30
+ const fill = {
31
+ fill: Color4.gray,
32
+ };
33
+ renderer.drawRect(rect, size / 4, fill);
34
+ }
35
+ getEraserRect(centerPoint) {
36
+ const size = this.getSizeOnCanvas();
37
+ const halfSize = Vec2.of(size / 2, size / 2);
38
+ return Rect2.fromCorners(centerPoint.minus(halfSize), centerPoint.plus(halfSize));
39
+ }
40
+ eraseTo(currentPoint) {
41
+ if (!this.isFirstEraseEvt && currentPoint.minus(this.lastPoint).magnitude() === 0) {
23
42
  return;
24
43
  }
44
+ this.isFirstEraseEvt = false;
45
+ // Currently only objects within eraserRect or that intersect a straight line
46
+ // from the center of the current rect to the previous are erased. TODO: Erase
47
+ // all objects as if there were pointerMove events between the two points.
48
+ const eraserRect = this.getEraserRect(currentPoint);
25
49
  const line = new LineSegment2(this.lastPoint, currentPoint);
26
- const region = line.bbox;
50
+ const region = Rect2.union(line.bbox, eraserRect);
27
51
  const intersectingElems = this.editor.image.getElementsIntersectingRegion(region).filter(component => {
28
- return component.intersects(line);
52
+ return component.intersects(line) || component.intersectsRect(eraserRect);
29
53
  });
30
54
  // Remove any intersecting elements.
31
55
  this.toRemove.push(...intersectingElems);
@@ -33,9 +57,25 @@ export default class Eraser extends BaseTool {
33
57
  const newPartialCommands = intersectingElems.map(elem => new Erase([elem]));
34
58
  newPartialCommands.forEach(cmd => cmd.apply(this.editor));
35
59
  this.partialCommands.push(...newPartialCommands);
60
+ this.drawPreviewAt(currentPoint);
36
61
  this.lastPoint = currentPoint;
37
62
  }
38
- onPointerUp(_event) {
63
+ onPointerDown(event) {
64
+ if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
65
+ this.lastPoint = event.current.canvasPos;
66
+ this.toRemove = [];
67
+ this.isFirstEraseEvt = true;
68
+ this.drawPreviewAt(event.current.canvasPos);
69
+ return true;
70
+ }
71
+ return false;
72
+ }
73
+ onPointerMove(event) {
74
+ const currentPoint = event.current.canvasPos;
75
+ this.eraseTo(currentPoint);
76
+ }
77
+ onPointerUp(event) {
78
+ this.eraseTo(event.current.canvasPos);
39
79
  if (this.toRemove.length > 0) {
40
80
  // Undo commands for each individual component and unite into a single command.
41
81
  this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
@@ -43,9 +83,21 @@ export default class Eraser extends BaseTool {
43
83
  const command = new Erase(this.toRemove);
44
84
  this.editor.dispatch(command); // dispatch: Makes undo-able.
45
85
  }
86
+ this.clearPreview();
46
87
  }
47
88
  onGestureCancel() {
48
89
  this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
49
90
  this.partialCommands = [];
91
+ this.clearPreview();
92
+ }
93
+ getThickness() {
94
+ return this.thickness;
95
+ }
96
+ setThickness(thickness) {
97
+ this.thickness = thickness;
98
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
99
+ kind: EditorEventType.ToolUpdated,
100
+ tool: this,
101
+ });
50
102
  }
51
103
  }
@@ -19,6 +19,7 @@ export default class Selection {
19
19
  private backgroundElem;
20
20
  private hasParent;
21
21
  constructor(startPoint: Point2, editor: Editor);
22
+ getBackgroundElem(): HTMLElement;
22
23
  getTransform(): Mat33;
23
24
  get preTransformRegion(): Rect2;
24
25
  get region(): Rect2;
@@ -36,7 +37,9 @@ export default class Selection {
36
37
  getMinCanvasSize(): number;
37
38
  getSelectedItemCount(): number;
38
39
  updateUI(): void;
40
+ private removedFromImage;
39
41
  private addRemoveSelectionFromImage;
42
+ private removeDeletedElemsFromSelection;
40
43
  private targetHandle;
41
44
  private backgroundDragging;
42
45
  onDragStart(pointer: Pointer, target: EventTarget): boolean;
@@ -45,7 +48,7 @@ export default class Selection {
45
48
  onDragCancel(): void;
46
49
  scrollTo(): Promise<void>;
47
50
  deleteSelectedObjects(): Command;
48
- duplicateSelectedObjects(): Command;
51
+ duplicateSelectedObjects(): Promise<Command>;
49
52
  addTo(elem: HTMLElement): void;
50
53
  setToPoint(point: Point2): void;
51
54
  cancelSelection(): void;
@@ -32,6 +32,8 @@ export default class Selection {
32
32
  this.transform = Mat33.identity;
33
33
  this.selectedElems = [];
34
34
  this.hasParent = true;
35
+ // Maps IDs to whether we removed the component from the image
36
+ this.removedFromImage = {};
35
37
  this.targetHandle = null;
36
38
  this.backgroundDragging = false;
37
39
  this.originalRegion = new Rect2(startPoint.x, startPoint.y, 0, 0);
@@ -58,6 +60,10 @@ export default class Selection {
58
60
  handle.addTo(this.backgroundElem);
59
61
  }
60
62
  }
63
+ // @internal Intended for unit tests
64
+ getBackgroundElem() {
65
+ return this.backgroundElem;
66
+ }
61
67
  getTransform() {
62
68
  return this.transform;
63
69
  }
@@ -140,16 +146,7 @@ export default class Selection {
140
146
  singleItemSelectionMode = true;
141
147
  }
142
148
  this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
143
- if (this.region.containsRect(elem.getBBox())) {
144
- return true;
145
- }
146
- // Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
147
- // As such, test with more lines than just this' edges.
148
- const testLines = [];
149
- for (const subregion of this.region.divideIntoGrid(2, 2)) {
150
- testLines.push(...subregion.getEdges());
151
- }
152
- return testLines.some(edge => elem.intersects(edge));
149
+ return elem.intersectsRect(this.region);
153
150
  });
154
151
  if (singleItemSelectionMode && this.selectedElems.length > 0) {
155
152
  this.selectedElems = [this.selectedElems[this.selectedElems.length - 1]];
@@ -215,30 +212,45 @@ export default class Selection {
215
212
  //
216
213
  // If removed from the image, selected elements are drawn as wet ink.
217
214
  addRemoveSelectionFromImage(inImage) {
218
- return __awaiter(this, void 0, void 0, function* () {
219
- // Don't hide elements if doing so will be slow.
220
- if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
221
- return;
215
+ // Don't hide elements if doing so will be slow.
216
+ if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
217
+ return;
218
+ }
219
+ for (const elem of this.selectedElems) {
220
+ const parent = this.editor.image.findParent(elem);
221
+ if (!inImage && parent) {
222
+ this.removedFromImage[elem.getId()] = true;
223
+ parent.remove();
222
224
  }
223
- for (const elem of this.selectedElems) {
224
- const parent = this.editor.image.findParent(elem);
225
- if (!inImage) {
226
- parent === null || parent === void 0 ? void 0 : parent.remove();
227
- }
228
- // If we're making things visible and the selected object wasn't previously
229
- // visible,
230
- else if (!parent) {
231
- EditorImage.addElement(elem).apply(this.editor);
232
- }
225
+ // If we're making things visible and the selected object wasn't previously
226
+ // visible,
227
+ else if (!parent && this.removedFromImage[elem.getId()]) {
228
+ EditorImage.addElement(elem).apply(this.editor);
229
+ this.removedFromImage[elem.getId()] = false;
230
+ delete this.removedFromImage[elem.getId()];
233
231
  }
234
- yield this.editor.queueRerender();
232
+ }
233
+ // Don't await queueRerender. If we're running in a test, the re-render might never
234
+ // happen.
235
+ this.editor.queueRerender().then(() => {
235
236
  if (!inImage) {
236
237
  this.previewTransformCmds();
237
238
  }
238
239
  });
239
240
  }
241
+ removeDeletedElemsFromSelection() {
242
+ // Remove any deleted elements from the selection.
243
+ this.selectedElems = this.selectedElems.filter(elem => {
244
+ const hasParent = !!this.editor.image.findParent(elem);
245
+ // If we removed the element and haven't added it back yet, don't remove it
246
+ // from the selection.
247
+ const weRemoved = this.removedFromImage[elem.getId()];
248
+ return hasParent || weRemoved;
249
+ });
250
+ }
240
251
  onDragStart(pointer, target) {
241
- void this.addRemoveSelectionFromImage(false);
252
+ this.removeDeletedElemsFromSelection();
253
+ this.addRemoveSelectionFromImage(false);
242
254
  for (const handle of this.handles) {
243
255
  if (handle.isTarget(target)) {
244
256
  handle.handleDragStart(pointer);
@@ -299,10 +311,34 @@ export default class Selection {
299
311
  });
300
312
  }
301
313
  deleteSelectedObjects() {
314
+ if (this.backgroundDragging || this.targetHandle) {
315
+ this.onDragEnd();
316
+ }
302
317
  return new Erase(this.selectedElems);
303
318
  }
304
319
  duplicateSelectedObjects() {
305
- return new Duplicate(this.selectedElems);
320
+ return __awaiter(this, void 0, void 0, function* () {
321
+ const wasTransforming = this.backgroundDragging || this.targetHandle;
322
+ let tmpApplyCommand = null;
323
+ if (wasTransforming) {
324
+ // Don't update the selection's focus when redoing/undoing
325
+ const selectionToUpdate = null;
326
+ tmpApplyCommand = new Selection.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
327
+ // Transform to ensure that the duplicates are in the correct location
328
+ yield tmpApplyCommand.apply(this.editor);
329
+ // Show items again
330
+ this.addRemoveSelectionFromImage(true);
331
+ }
332
+ const duplicateCommand = new Duplicate(this.selectedElems);
333
+ if (wasTransforming) {
334
+ // Move the selected objects back to the correct location.
335
+ yield (tmpApplyCommand === null || tmpApplyCommand === void 0 ? void 0 : tmpApplyCommand.unapply(this.editor));
336
+ this.addRemoveSelectionFromImage(false);
337
+ this.previewTransformCmds();
338
+ this.updateUI();
339
+ }
340
+ return duplicateCommand;
341
+ });
306
342
  }
307
343
  addTo(elem) {
308
344
  if (this.container.parentElement) {
@@ -323,6 +359,7 @@ export default class Selection {
323
359
  this.hasParent = false;
324
360
  }
325
361
  setSelectedObjects(objects, bbox) {
362
+ this.addRemoveSelectionFromImage(true);
326
363
  this.originalRegion = bbox;
327
364
  this.selectedElems = objects.filter(object => object.isSelectable());
328
365
  this.updateUI();
@@ -241,7 +241,9 @@ export default class SelectionTool extends BaseTool {
241
241
  }
242
242
  else if (evt.ctrlKey) {
243
243
  if (this.selectionBox && evt.key === 'd') {
244
- this.editor.dispatch(this.selectionBox.duplicateSelectedObjects());
244
+ this.selectionBox.duplicateSelectedObjects().then(command => {
245
+ this.editor.dispatch(command);
246
+ });
245
247
  return true;
246
248
  }
247
249
  else if (evt.key === 'a') {
@@ -69,12 +69,13 @@ export default class TextTool extends BaseTool {
69
69
  flushInput(removeInput = true) {
70
70
  if (this.textInputElem && this.textTargetPosition) {
71
71
  const content = this.textInputElem.value.trimEnd();
72
+ this.textInputElem.value = '';
72
73
  if (removeInput) {
73
- this.textInputElem.remove();
74
+ // In some browsers, .remove() triggers a .blur event (synchronously).
75
+ // Clear this.textInputElem before removal
76
+ const input = this.textInputElem;
74
77
  this.textInputElem = null;
75
- }
76
- else {
77
- this.textInputElem.value = '';
78
+ input.remove();
78
79
  }
79
80
  if (content === '') {
80
81
  return;
@@ -145,9 +146,12 @@ export default class TextTool extends BaseTool {
145
146
  // Delay removing the input -- flushInput may be called within a blur()
146
147
  // event handler
147
148
  const removeInput = false;
148
- this.flushInput(removeInput);
149
149
  const input = this.textInputElem;
150
- setTimeout(() => input === null || input === void 0 ? void 0 : input.remove(), 0);
150
+ this.flushInput(removeInput);
151
+ this.textInputElem = null;
152
+ setTimeout(() => {
153
+ input === null || input === void 0 ? void 0 : input.remove();
154
+ }, 0);
151
155
  };
152
156
  this.textInputElem.onkeyup = (evt) => {
153
157
  var _a, _b;
@@ -129,8 +129,8 @@ export interface ToolbarDropdownShownEvent {
129
129
  readonly parentWidget: BaseWidget;
130
130
  }
131
131
  export type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | CommandDoneEvent | CommandUndoneEvent | ColorPickerToggled | ColorPickerColorSelected | ToolbarDropdownShownEvent;
132
- export type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null;
133
- export type ComponentAddedListener = (component: AbstractComponent) => void;
132
+ export type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null | void;
133
+ export type ComponentAddedListener = (component: AbstractComponent) => Promise<void> | void;
134
134
  export type OnDetermineExportRectListener = (exportRect: Rect2) => void;
135
135
  export interface ImageLoader {
136
136
  start(onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.11.3",
3
+ "version": "0.12.0",
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/Color4.ts CHANGED
@@ -176,5 +176,6 @@ export default class Color4 {
176
176
  public static yellow = Color4.ofRGB(1, 1, 0.1);
177
177
  public static clay = Color4.ofRGB(0.8, 0.4, 0.2);
178
178
  public static black = Color4.ofRGB(0, 0, 0);
179
+ public static gray = Color4.ofRGB(0.5, 0.5, 0.5);
179
180
  public static white = Color4.ofRGB(1, 1, 1);
180
181
  }
package/src/Editor.ts CHANGED
@@ -292,8 +292,7 @@ export class Editor {
292
292
  const toolbar = new HTMLToolbar(this, this.container, this.localization);
293
293
 
294
294
  if (defaultLayout) {
295
- toolbar.addDefaultToolWidgets();
296
- toolbar.addDefaultActionButtons();
295
+ toolbar.addDefaults();
297
296
  }
298
297
 
299
298
  return toolbar;
@@ -946,8 +945,8 @@ export class Editor {
946
945
  this.showLoadingWarning(0);
947
946
  this.display.setDraftMode(true);
948
947
 
949
- await loader.start((component) => {
950
- this.dispatchNoAnnounce(EditorImage.addElement(component));
948
+ await loader.start(async (component) => {
949
+ await this.dispatchNoAnnounce(EditorImage.addElement(component));
951
950
  }, (countProcessed: number, totalToProcess: number) => {
952
951
  if (countProcessed % 500 === 0) {
953
952
  this.showLoadingWarning(countProcessed / totalToProcess);
package/src/SVGLoader.ts CHANGED
@@ -156,7 +156,7 @@ export default class SVGLoader implements ImageLoader {
156
156
  }
157
157
 
158
158
  // Adds a stroke with a single path
159
- private addPath(node: SVGPathElement) {
159
+ private async addPath(node: SVGPathElement) {
160
160
  let elem: AbstractComponent;
161
161
  try {
162
162
  const strokeData = this.strokeDataFromElem(node);
@@ -181,7 +181,7 @@ export default class SVGLoader implements ImageLoader {
181
181
  return;
182
182
  }
183
183
  }
184
- this.onAddComponent?.(elem);
184
+ await this.onAddComponent?.(elem);
185
185
  }
186
186
 
187
187
  // If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
@@ -274,10 +274,10 @@ export default class SVGLoader implements ImageLoader {
274
274
  return result;
275
275
  }
276
276
 
277
- private addText(elem: SVGTextElement|SVGTSpanElement) {
277
+ private async addText(elem: SVGTextElement|SVGTSpanElement) {
278
278
  try {
279
279
  const textElem = this.makeText(elem);
280
- this.onAddComponent?.(textElem);
280
+ await this.onAddComponent?.(textElem);
281
281
  } catch (e) {
282
282
  console.error('Invalid text object in node', elem, '. Continuing.... Error:', e);
283
283
  this.addUnknownNode(elem);
@@ -300,17 +300,17 @@ export default class SVGLoader implements ImageLoader {
300
300
  new Set([ 'transform' ])
301
301
  );
302
302
 
303
- this.onAddComponent?.(imageElem);
303
+ await this.onAddComponent?.(imageElem);
304
304
  } catch (e) {
305
305
  console.error('Error loading image:', e, '. Element: ', elem, '. Continuing...');
306
- this.addUnknownNode(elem);
306
+ await this.addUnknownNode(elem);
307
307
  }
308
308
  }
309
309
 
310
- private addUnknownNode(node: SVGElement) {
310
+ private async addUnknownNode(node: SVGElement) {
311
311
  if (this.storeUnknown) {
312
312
  const component = new UnknownSVGObject(node);
313
- this.onAddComponent?.(component);
313
+ await this.onAddComponent?.(component);
314
314
  }
315
315
  }
316
316
 
@@ -335,9 +335,9 @@ export default class SVGLoader implements ImageLoader {
335
335
  this.onDetermineExportRect?.(this.rootViewBox);
336
336
  }
337
337
 
338
- private updateSVGAttrs(node: SVGSVGElement) {
338
+ private async updateSVGAttrs(node: SVGSVGElement) {
339
339
  if (this.storeUnknown) {
340
- this.onAddComponent?.(new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
340
+ await this.onAddComponent?.(new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
341
341
  }
342
342
  }
343
343
 
@@ -350,10 +350,10 @@ export default class SVGLoader implements ImageLoader {
350
350
  // Continue -- visit the node's children.
351
351
  break;
352
352
  case 'path':
353
- this.addPath(node as SVGPathElement);
353
+ await this.addPath(node as SVGPathElement);
354
354
  break;
355
355
  case 'text':
356
- this.addText(node as SVGTextElement);
356
+ await this.addText(node as SVGTextElement);
357
357
  visitChildren = false;
358
358
  break;
359
359
  case 'image':
@@ -367,7 +367,7 @@ export default class SVGLoader implements ImageLoader {
367
367
  this.updateSVGAttrs(node as SVGSVGElement);
368
368
  break;
369
369
  case 'style':
370
- this.addUnknownNode(node as SVGStyleElement);
370
+ await this.addUnknownNode(node as SVGStyleElement);
371
371
  break;
372
372
  default:
373
373
  console.warn('Unknown SVG element,', node);
@@ -377,7 +377,7 @@ export default class SVGLoader implements ImageLoader {
377
377
  );
378
378
  }
379
379
 
380
- this.addUnknownNode(node as SVGElement);
380
+ await this.addUnknownNode(node as SVGElement);
381
381
  return;
382
382
  }
383
383
 
@@ -78,6 +78,25 @@ export default abstract class AbstractComponent {
78
78
  public abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
79
79
  public abstract intersects(lineSegment: LineSegment2): boolean;
80
80
 
81
+ public intersectsRect(rect: Rect2): boolean {
82
+ // If this component intersects rect,
83
+ // it is either contained entirely within rect or intersects one of rect's edges.
84
+
85
+ // If contained within,
86
+ if (rect.containsRect(this.getBBox())) {
87
+ return true;
88
+ }
89
+
90
+ // Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
91
+ // As such, test with more lines than just the rect's edges.
92
+ const testLines = [];
93
+ for (const subregion of rect.divideIntoGrid(2, 2)) {
94
+ testLines.push(...subregion.getEdges());
95
+ }
96
+
97
+ return testLines.some(edge => this.intersects(edge));
98
+ }
99
+
81
100
  // Return null iff this object cannot be safely serialized/deserialized.
82
101
  protected abstract serializeToJSON(): any[]|Record<string, any>|number|string|null;
83
102
 
@@ -16,12 +16,25 @@ import SelectionToolWidget from './widgets/SelectionToolWidget';
16
16
  import TextToolWidget from './widgets/TextToolWidget';
17
17
  import HandToolWidget from './widgets/HandToolWidget';
18
18
  import BaseWidget from './widgets/BaseWidget';
19
- import { ActionButtonWidget, InsertImageWidget } from './lib';
19
+ import ActionButtonWidget from './widgets/ActionButtonWidget';
20
+ import InsertImageWidget from './widgets/InsertImageWidget';
20
21
 
21
22
  export const toolbarCSSPrefix = 'toolbar-';
22
23
 
23
24
  type UpdateColorisCallback = ()=>void;
24
25
 
26
+ interface SpacerOptions {
27
+ // Defaults to 0. If a non-zero number, determines the rate at which the
28
+ // spacer should grow (like flexGrow).
29
+ grow: number;
30
+
31
+ // Minimum size (e.g. "23px")
32
+ minSize: string;
33
+
34
+ // Maximum size (e.g. "50px")
35
+ maxSize: string;
36
+ }
37
+
25
38
  export default class HTMLToolbar {
26
39
  private container: HTMLElement;
27
40
 
@@ -121,8 +134,17 @@ export default class HTMLToolbar {
121
134
  });
122
135
  }
123
136
 
124
- // Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
125
- // (i.e. its `addTo` method should not have been called).
137
+ /**
138
+ * Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
139
+ * (i.e. its `addTo` method should not have been called).
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * const toolbar = editor.addToolbar();
144
+ * const insertImageWidget = new InsertImageWidget(editor);
145
+ * toolbar.addWidget(insertImageWidget);
146
+ * ```
147
+ */
126
148
  public addWidget(widget: BaseWidget) {
127
149
  // Prevent name collisions
128
150
  const id = widget.getUniqueIdIn(this.widgets);
@@ -135,6 +157,46 @@ export default class HTMLToolbar {
135
157
  this.setupColorPickers();
136
158
  }
137
159
 
160
+ /**
161
+ * Adds a spacer.
162
+ *
163
+ * @example
164
+ * Adding a save button that moves to the very right edge of the toolbar
165
+ * while keeping the other buttons centered:
166
+ * ```ts
167
+ * const toolbar = editor.addToolbar(false);
168
+ *
169
+ * toolbar.addSpacer({ grow: 1 });
170
+ * toolbar.addDefaults();
171
+ * toolbar.addSpacer({ grow: 1 });
172
+ *
173
+ * toolbar.addActionButton({
174
+ * label: 'Save',
175
+ * icon: editor.icons.makeSaveIcon(),
176
+ * }, () => {
177
+ * saveCallback();
178
+ * });
179
+ * ```
180
+ */
181
+ public addSpacer(options: Partial<SpacerOptions> = {}) {
182
+ const spacer = document.createElement('div');
183
+ spacer.classList.add(`${toolbarCSSPrefix}spacer`);
184
+
185
+ if (options.grow) {
186
+ spacer.style.flexGrow = `${options.grow}`;
187
+ }
188
+
189
+ if (options.minSize) {
190
+ spacer.style.minWidth = options.minSize;
191
+ }
192
+
193
+ if (options.maxSize) {
194
+ spacer.style.maxWidth = options.maxSize;
195
+ }
196
+
197
+ this.container.appendChild(spacer);
198
+ }
199
+
138
200
  public serializeState(): string {
139
201
  const result: Record<string, any> = {};
140
202
 
@@ -145,8 +207,10 @@ export default class HTMLToolbar {
145
207
  return JSON.stringify(result);
146
208
  }
147
209
 
148
- // Deserialize toolbar widgets from the given state.
149
- // Assumes that toolbar widgets are in the same order as when state was serialized.
210
+ /**
211
+ * Deserialize toolbar widgets from the given state.
212
+ * Assumes that toolbar widgets are in the same order as when state was serialized.
213
+ */
150
214
  public deserializeState(state: string) {
151
215
  const data = JSON.parse(state);
152
216
 
@@ -247,4 +311,16 @@ export default class HTMLToolbar {
247
311
  public addDefaultActionButtons() {
248
312
  this.addUndoRedoButtons();
249
313
  }
314
+
315
+ /**
316
+ * Adds both the default tool widgets and action buttons. Equivalent to
317
+ * ```ts
318
+ * toolbar.addDefaultToolWidgets();
319
+ * toolbar.addDefaultActionButtons();
320
+ * ```
321
+ */
322
+ public addDefaults() {
323
+ this.addDefaultToolWidgets();
324
+ this.addDefaultActionButtons();
325
+ }
250
326
  }