js-draw 1.10.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/Editor.css +6 -2
  2. package/dist/bundle.js +3 -3
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.d.ts +7 -0
  5. package/dist/cjs/Editor.js +18 -4
  6. package/dist/cjs/commands/invertCommand.js +5 -0
  7. package/dist/cjs/components/AbstractComponent.d.ts +8 -0
  8. package/dist/cjs/components/AbstractComponent.js +28 -8
  9. package/dist/cjs/components/BackgroundComponent.d.ts +1 -1
  10. package/dist/cjs/components/ImageComponent.d.ts +1 -1
  11. package/dist/cjs/components/SVGGlobalAttributesObject.d.ts +1 -1
  12. package/dist/cjs/components/Stroke.d.ts +1 -1
  13. package/dist/cjs/components/builders/types.d.ts +11 -0
  14. package/dist/cjs/rendering/Display.js +3 -1
  15. package/dist/cjs/rendering/renderers/DummyRenderer.d.ts +1 -0
  16. package/dist/cjs/rendering/renderers/DummyRenderer.js +3 -0
  17. package/dist/cjs/toolbar/AbstractToolbar.d.ts +18 -2
  18. package/dist/cjs/toolbar/AbstractToolbar.js +46 -30
  19. package/dist/cjs/toolbar/widgets/BaseWidget.js +1 -1
  20. package/dist/cjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
  21. package/dist/cjs/toolbar/widgets/ExitActionWidget.js +32 -0
  22. package/dist/cjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
  23. package/dist/cjs/toolbar/widgets/HandToolWidget.js +24 -13
  24. package/dist/cjs/toolbar/widgets/InsertImageWidget.js +1 -1
  25. package/dist/cjs/toolbar/widgets/keybindings.d.ts +1 -0
  26. package/dist/cjs/toolbar/widgets/keybindings.js +4 -1
  27. package/dist/cjs/toolbar/widgets/layout/types.d.ts +1 -1
  28. package/dist/cjs/tools/FindTool.js +1 -1
  29. package/dist/cjs/tools/Pen.js +13 -2
  30. package/dist/cjs/tools/SelectionTool/Selection.d.ts +4 -0
  31. package/dist/cjs/tools/SelectionTool/Selection.js +56 -12
  32. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
  33. package/dist/cjs/tools/SelectionTool/SelectionTool.js +35 -3
  34. package/dist/cjs/tools/ToolSwitcherShortcut.d.ts +0 -1
  35. package/dist/cjs/tools/ToolSwitcherShortcut.js +0 -1
  36. package/dist/cjs/tools/keybindings.d.ts +1 -0
  37. package/dist/cjs/tools/keybindings.js +3 -1
  38. package/dist/cjs/tools/localization.d.ts +2 -0
  39. package/dist/cjs/tools/localization.js +2 -0
  40. package/dist/cjs/util/listenForKeyboardEventsFrom.d.ts +5 -0
  41. package/dist/cjs/util/listenForKeyboardEventsFrom.js +5 -1
  42. package/dist/cjs/version.js +1 -1
  43. package/dist/mjs/Editor.d.ts +7 -0
  44. package/dist/mjs/Editor.mjs +18 -4
  45. package/dist/mjs/commands/invertCommand.mjs +5 -0
  46. package/dist/mjs/components/AbstractComponent.d.ts +8 -0
  47. package/dist/mjs/components/AbstractComponent.mjs +28 -8
  48. package/dist/mjs/components/BackgroundComponent.d.ts +1 -1
  49. package/dist/mjs/components/ImageComponent.d.ts +1 -1
  50. package/dist/mjs/components/SVGGlobalAttributesObject.d.ts +1 -1
  51. package/dist/mjs/components/Stroke.d.ts +1 -1
  52. package/dist/mjs/components/builders/types.d.ts +11 -0
  53. package/dist/mjs/rendering/Display.mjs +3 -1
  54. package/dist/mjs/rendering/renderers/DummyRenderer.d.ts +1 -0
  55. package/dist/mjs/rendering/renderers/DummyRenderer.mjs +3 -0
  56. package/dist/mjs/toolbar/AbstractToolbar.d.ts +18 -2
  57. package/dist/mjs/toolbar/AbstractToolbar.mjs +46 -30
  58. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +1 -1
  59. package/dist/mjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
  60. package/dist/mjs/toolbar/widgets/ExitActionWidget.mjs +27 -0
  61. package/dist/mjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
  62. package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +24 -13
  63. package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +1 -1
  64. package/dist/mjs/toolbar/widgets/keybindings.d.ts +1 -0
  65. package/dist/mjs/toolbar/widgets/keybindings.mjs +3 -0
  66. package/dist/mjs/toolbar/widgets/layout/types.d.ts +1 -1
  67. package/dist/mjs/tools/FindTool.mjs +1 -1
  68. package/dist/mjs/tools/Pen.mjs +13 -2
  69. package/dist/mjs/tools/SelectionTool/Selection.d.ts +4 -0
  70. package/dist/mjs/tools/SelectionTool/Selection.mjs +56 -12
  71. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
  72. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +36 -4
  73. package/dist/mjs/tools/ToolSwitcherShortcut.d.ts +0 -1
  74. package/dist/mjs/tools/ToolSwitcherShortcut.mjs +0 -1
  75. package/dist/mjs/tools/keybindings.d.ts +1 -0
  76. package/dist/mjs/tools/keybindings.mjs +2 -0
  77. package/dist/mjs/tools/localization.d.ts +2 -0
  78. package/dist/mjs/tools/localization.mjs +2 -0
  79. package/dist/mjs/util/listenForKeyboardEventsFrom.d.ts +5 -0
  80. package/dist/mjs/util/listenForKeyboardEventsFrom.mjs +5 -1
  81. package/dist/mjs/version.mjs +1 -1
  82. package/package.json +5 -5
  83. package/src/toolbar/AbstractToolbar.scss +3 -2
  84. package/src/toolbar/widgets/components/makeColorInput.scss +8 -0
@@ -15,3 +15,4 @@ export declare const zoomInKeyboardShortcutId = "jsdraw.tools.PanZoom.zoomIn";
15
15
  export declare const zoomOutKeyboardShortcutId = "jsdraw.tools.PanZoom.zoomOut";
16
16
  export declare const selectAllKeyboardShortcut = "jsdraw.tools.SelectionTool.selectAll";
17
17
  export declare const duplicateSelectionShortcut = "jsdraw.tools.SelectionTool.duplicateSelection";
18
+ export declare const sendToBackSelectionShortcut = "jsdraw.tools.SelectionTool.sendToBack";
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.duplicateSelectionShortcut = exports.selectAllKeyboardShortcut = exports.zoomOutKeyboardShortcutId = exports.zoomInKeyboardShortcutId = exports.rotateCounterClockwiseKeyboardShortcutId = exports.rotateClockwiseKeyboardShortcutId = exports.moveDownKeyboardShortcutId = exports.moveUpKeyboardShortcutId = exports.moveRightKeyboardShortcutId = exports.moveLeftKeyboardShortcutId = exports.toggleFindVisibleShortcutId = exports.lineLockKeyboardShortcutId = exports.snapToGridKeyboardShortcutId = exports.decreaseSizeKeyboardShortcutId = exports.increaseSizeKeyboardShortcutId = exports.redoKeyboardShortcutId = exports.undoKeyboardShortcutId = void 0;
6
+ exports.sendToBackSelectionShortcut = exports.duplicateSelectionShortcut = exports.selectAllKeyboardShortcut = exports.zoomOutKeyboardShortcutId = exports.zoomInKeyboardShortcutId = exports.rotateCounterClockwiseKeyboardShortcutId = exports.rotateClockwiseKeyboardShortcutId = exports.moveDownKeyboardShortcutId = exports.moveUpKeyboardShortcutId = exports.moveRightKeyboardShortcutId = exports.moveLeftKeyboardShortcutId = exports.toggleFindVisibleShortcutId = exports.lineLockKeyboardShortcutId = exports.snapToGridKeyboardShortcutId = exports.decreaseSizeKeyboardShortcutId = exports.increaseSizeKeyboardShortcutId = exports.redoKeyboardShortcutId = exports.undoKeyboardShortcutId = void 0;
7
7
  const KeyboardShortcutManager_1 = __importDefault(require("../shortcuts/KeyboardShortcutManager"));
8
8
  // This file contains user-overridable tool-realted keybindings.
9
9
  // Undo/redo
@@ -45,3 +45,5 @@ exports.selectAllKeyboardShortcut = 'jsdraw.tools.SelectionTool.selectAll';
45
45
  KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(exports.selectAllKeyboardShortcut, ['CtrlOrMeta+KeyA'], 'Select all');
46
46
  exports.duplicateSelectionShortcut = 'jsdraw.tools.SelectionTool.duplicateSelection';
47
47
  KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(exports.duplicateSelectionShortcut, ['CtrlOrMeta+KeyD'], 'Duplicate selection');
48
+ exports.sendToBackSelectionShortcut = 'jsdraw.tools.SelectionTool.sendToBack';
49
+ KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(exports.sendToBackSelectionShortcut, ['End'], 'Send to back');
@@ -9,6 +9,8 @@ export interface ToolLocalization {
9
9
  undoRedoTool: string;
10
10
  pipetteTool: string;
11
11
  rightClickDragPanTool: string;
12
+ autocorrectedTo: (description: string) => string;
13
+ autocorrectionCanceled: string;
12
14
  textTool: string;
13
15
  enterTextToInsert: string;
14
16
  changeTool: string;
@@ -12,6 +12,8 @@ exports.defaultToolLocalization = {
12
12
  rightClickDragPanTool: 'Right-click drag',
13
13
  pipetteTool: 'Pick color from screen',
14
14
  keyboardPanZoom: 'Keyboard pan/zoom shortcuts',
15
+ autocorrectedTo: (strokeDescription) => `Autocorrected to ${strokeDescription}`,
16
+ autocorrectionCanceled: 'Autocorrect cancelled',
15
17
  textTool: 'Text',
16
18
  enterTextToInsert: 'Text to insert',
17
19
  changeTool: 'Change tool',
@@ -2,6 +2,11 @@ interface Callbacks {
2
2
  filter(event: KeyboardEvent): boolean;
3
3
  handleKeyDown(event: KeyboardEvent): void;
4
4
  handleKeyUp(event: KeyboardEvent): void;
5
+ /**
6
+ * Should return `true` iff `source` is also registered as an event listener source.
7
+ * If `false` and focus leaves the original source, keyup events are fired.
8
+ */
9
+ getHandlesKeyEventsFrom(source: Node): boolean;
5
10
  }
6
11
  /**
7
12
  * Calls `callbacks` when different keys are known to be pressed.
@@ -67,7 +67,11 @@ const listenForKeyboardEventsFrom = (elem, callbacks) => {
67
67
  handleKeyEvent(htmlEvent);
68
68
  });
69
69
  elem.addEventListener('focusout', (focusEvent) => {
70
- const stillHasFocus = focusEvent.relatedTarget && elem.contains(focusEvent.relatedTarget);
70
+ let stillHasFocus = false;
71
+ if (focusEvent.relatedTarget) {
72
+ const relatedTarget = focusEvent.relatedTarget;
73
+ stillHasFocus = elem.contains(relatedTarget) || callbacks.getHandlesKeyEventsFrom(relatedTarget);
74
+ }
71
75
  if (!stillHasFocus) {
72
76
  for (const event of keysDown) {
73
77
  callbacks.handleKeyUp(new KeyboardEvent('keyup', {
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = {
4
- number: '1.10.0',
4
+ number: '1.11.1',
5
5
  };
@@ -255,6 +255,13 @@ export declare class Editor {
255
255
  private updateEditorSizeVariables;
256
256
  private pointers;
257
257
  private getPointerList;
258
+ /**
259
+ * A protected method that can override setPointerCapture in environments where it may fail
260
+ * (e.g. with synthetic events). @internal
261
+ */
262
+ protected setPointerCapture(target: HTMLElement, pointerId: number): void;
263
+ /** Can be overridden in a testing environment to handle synthetic events. @internal */
264
+ protected releasePointerCapture(target: HTMLElement, pointerId: number): void;
258
265
  /**
259
266
  * Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
260
267
  * as the content of the editor.
@@ -327,6 +327,17 @@ export class Editor {
327
327
  }
328
328
  return res;
329
329
  }
330
+ /**
331
+ * A protected method that can override setPointerCapture in environments where it may fail
332
+ * (e.g. with synthetic events). @internal
333
+ */
334
+ setPointerCapture(target, pointerId) {
335
+ target.setPointerCapture(pointerId);
336
+ }
337
+ /** Can be overridden in a testing environment to handle synthetic events. @internal */
338
+ releasePointerCapture(target, pointerId) {
339
+ target.releasePointerCapture(pointerId);
340
+ }
330
341
  /**
331
342
  * Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
332
343
  * as the content of the editor.
@@ -337,7 +348,7 @@ export class Editor {
337
348
  if (eventType === 'pointerdown') {
338
349
  const pointer = Pointer.ofEvent(evt, true, this.viewport, eventsRelativeTo);
339
350
  this.pointers[pointer.id] = pointer;
340
- eventTarget.setPointerCapture(pointer.id);
351
+ this.setPointerCapture(eventTarget, pointer.id);
341
352
  const event = {
342
353
  kind: InputEvtType.PointerDownEvt,
343
354
  current: pointer,
@@ -374,7 +385,7 @@ export class Editor {
374
385
  return false;
375
386
  }
376
387
  this.pointers[pointer.id] = pointer;
377
- eventTarget.releasePointerCapture(pointer.id);
388
+ this.releasePointerCapture(eventTarget, pointer.id);
378
389
  if (this.toolController.dispatchInputEvent({
379
390
  kind: InputEvtType.PointerUpEvt,
380
391
  current: pointer,
@@ -563,7 +574,7 @@ export class Editor {
563
574
  startPoint: currentPos,
564
575
  };
565
576
  // Capture the pointer so we receive future events even if the overlay is hidden.
566
- elem.setPointerCapture(event.pointerId);
577
+ this.setPointerCapture(elem, event.pointerId);
567
578
  // Don't send to the editor.
568
579
  sendToEditor = false;
569
580
  }
@@ -595,7 +606,7 @@ export class Editor {
595
606
  // pointercancel event.
596
607
  else if ((eventName === 'pointerup' || eventName === 'pointercancel')
597
608
  && gestureData[pointerId] && gestureData[pointerId].eventBuffer.length > 0) {
598
- elem.releasePointerCapture(event.pointerId);
609
+ this.releasePointerCapture(elem, event.pointerId);
599
610
  // Don't send to the editor.
600
611
  sendToEditor = false;
601
612
  delete gestureData[pointerId];
@@ -643,6 +654,9 @@ export class Editor {
643
654
  handleKeyUp: (htmlEvent) => {
644
655
  this.handleHTMLKeyUpEvent(htmlEvent);
645
656
  },
657
+ getHandlesKeyEventsFrom: (element) => {
658
+ return this.eventListenerTargets.includes(element);
659
+ },
646
660
  });
647
661
  // Allow drop.
648
662
  elem.ondragover = evt => {
@@ -6,6 +6,11 @@ const invertCommand = (command) => {
6
6
  if (command instanceof SerializableCommand) {
7
7
  // SerializableCommand that does the inverse of [command]
8
8
  return new class extends SerializableCommand {
9
+ constructor() {
10
+ super(...arguments);
11
+ // For debugging
12
+ this._command = command;
13
+ }
9
14
  serializeToJSON() {
10
15
  return command.serialize();
11
16
  }
@@ -116,6 +116,14 @@ export default abstract class AbstractComponent {
116
116
  protected abstract applyTransformation(affineTransfm: Mat33): void;
117
117
  transformBy(affineTransfm: Mat33): SerializableCommand;
118
118
  setZIndex(newZIndex: number): SerializableCommand;
119
+ /**
120
+ * Combines {@link transformBy} and {@link setZIndex} into a single command.
121
+ *
122
+ * @param newZIndex - The z-index this component should have after applying this command.
123
+ * @param originalZIndex - @internal The z-index the component should revert to after unapplying
124
+ * this command.
125
+ */
126
+ setZIndexAndTransformBy(affineTransfm: Mat33, newZIndex: number, originalZIndex?: number): SerializableCommand;
119
127
  isSelectable(): boolean;
120
128
  isBackground(): boolean;
121
129
  getProportionalRenderingTime(): number;
@@ -138,13 +138,25 @@ class AbstractComponent {
138
138
  }
139
139
  // Returns a command that, when applied, transforms this by [affineTransfm] and
140
140
  // updates the editor.
141
- // This also increases the element's z-index so that it is on top.
141
+ //
142
+ // The transformed component is also moved to the top (use {@link setZIndexAndTransformBy} to
143
+ // avoid this behavior).
142
144
  transformBy(affineTransfm) {
143
145
  return new AbstractComponent.TransformElementCommand(affineTransfm, this.getId(), this);
144
146
  }
145
147
  // Returns a command that updates this component's z-index.
146
148
  setZIndex(newZIndex) {
147
- return new AbstractComponent.TransformElementCommand(Mat33.identity, this.getId(), this, newZIndex, this.getZIndex());
149
+ return new AbstractComponent.TransformElementCommand(Mat33.identity, this.getId(), this, newZIndex);
150
+ }
151
+ /**
152
+ * Combines {@link transformBy} and {@link setZIndex} into a single command.
153
+ *
154
+ * @param newZIndex - The z-index this component should have after applying this command.
155
+ * @param originalZIndex - @internal The z-index the component should revert to after unapplying
156
+ * this command.
157
+ */
158
+ setZIndexAndTransformBy(affineTransfm, newZIndex, originalZIndex) {
159
+ return new AbstractComponent.TransformElementCommand(affineTransfm, this.getId(), this, newZIndex, originalZIndex);
148
160
  }
149
161
  // @returns true iff this component can be selected (e.g. by the selection tool.)
150
162
  isSelectable() {
@@ -215,8 +227,12 @@ class AbstractComponent {
215
227
  throw new Error(`Element with data ${json} cannot be deserialized.`);
216
228
  }
217
229
  const instance = this.deserializationCallbacks[json.name](json.data);
218
- instance.zIndex = json.zIndex;
219
230
  instance.id = json.id;
231
+ if (isFinite(json.zIndex)) {
232
+ instance.zIndex = json.zIndex;
233
+ // Ensure that new components will be added on top.
234
+ AbstractComponent.zIndexCounter = Math.max(AbstractComponent.zIndexCounter, instance.zIndex + 1);
235
+ }
220
236
  // TODO: What should we do with json.loadSaveData?
221
237
  // If we attach it to [instance], we create a potential security risk — loadSaveData
222
238
  // is often used to store unrecognised attributes so they can be preserved on output.
@@ -225,6 +241,7 @@ class AbstractComponent {
225
241
  }
226
242
  }
227
243
  // Topmost z-index
244
+ // TODO: Should be a property of the EditorImage.
228
245
  AbstractComponent.zIndexCounter = 0;
229
246
  AbstractComponent.deserializationCallbacks = {};
230
247
  AbstractComponent.transformElementCommandId = 'transform-element';
@@ -252,7 +269,7 @@ AbstractComponent.TransformElementCommand = (_a = class extends UnresolvedSerial
252
269
  super.resolveComponent(image);
253
270
  this.origZIndex ??= this.component.getZIndex();
254
271
  }
255
- updateTransform(editor, newTransfm) {
272
+ updateTransform(editor, newTransfm, targetZIndex) {
256
273
  if (!this.component) {
257
274
  throw new Error('this.component is undefined or null!');
258
275
  }
@@ -264,7 +281,12 @@ AbstractComponent.TransformElementCommand = (_a = class extends UnresolvedSerial
264
281
  hadParent = true;
265
282
  }
266
283
  this.component.applyTransformation(newTransfm);
284
+ this.component.zIndex = targetZIndex;
267
285
  this.component.lastChangedTime = (new Date()).getTime();
286
+ // Ensure that new components are automatically drawn above the current component.
287
+ if (targetZIndex >= AbstractComponent.zIndexCounter) {
288
+ AbstractComponent.zIndexCounter = targetZIndex + 1;
289
+ }
268
290
  // Add the element back to the document.
269
291
  if (hadParent) {
270
292
  EditorImage.addElement(this.component).apply(editor);
@@ -272,14 +294,12 @@ AbstractComponent.TransformElementCommand = (_a = class extends UnresolvedSerial
272
294
  }
273
295
  apply(editor) {
274
296
  this.resolveComponent(editor.image);
275
- this.component.zIndex = this.targetZIndex;
276
- this.updateTransform(editor, this.affineTransfm);
297
+ this.updateTransform(editor, this.affineTransfm, this.targetZIndex);
277
298
  editor.queueRerender();
278
299
  }
279
300
  unapply(editor) {
280
301
  this.resolveComponent(editor.image);
281
- this.component.zIndex = this.origZIndex;
282
- this.updateTransform(editor, this.affineTransfm.inverse());
302
+ this.updateTransform(editor, this.affineTransfm.inverse(), this.origZIndex);
283
303
  editor.queueRerender();
284
304
  }
285
305
  description(_editor, localizationTable) {
@@ -64,5 +64,5 @@ export default class BackgroundComponent extends AbstractComponent implements Re
64
64
  protected applyTransformation(_affineTransfm: Mat33): void;
65
65
  description(localizationTable: ImageComponentLocalization): string;
66
66
  protected createClone(): AbstractComponent;
67
- static deserializeFromJSON(json: any): BackgroundComponent;
67
+ static deserializeFromJSON(this: void, json: any): BackgroundComponent;
68
68
  }
@@ -37,5 +37,5 @@ export default class ImageComponent extends AbstractComponent {
37
37
  height: number;
38
38
  transform: Mat33Array;
39
39
  };
40
- static deserializeFromJSON(data: any): ImageComponent;
40
+ static deserializeFromJSON(this: void, data: any): ImageComponent;
41
41
  }
@@ -15,6 +15,6 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
15
15
  protected createClone(): SVGGlobalAttributesObject;
16
16
  description(localization: ImageComponentLocalization): string;
17
17
  protected serializeToJSON(): string | null;
18
- static deserializeFromString(_data: string): AbstractComponent;
18
+ static deserializeFromString(this: void, _data: string): AbstractComponent;
19
19
  }
20
20
  export {};
@@ -88,5 +88,5 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
88
88
  path: string;
89
89
  }[];
90
90
  /** @internal */
91
- static deserializeFromJSON(json: any): Stroke;
91
+ static deserializeFromJSON(this: void, json: any): Stroke;
92
92
  }
@@ -7,6 +7,17 @@ export interface ComponentBuilder {
7
7
  getBBox(): Rect2;
8
8
  build(): AbstractComponent;
9
9
  preview(renderer: AbstractRenderer): void;
10
+ /**
11
+ * Called when the pen is stationary (or the user otherwise
12
+ * activates autocomplete). This might attempt to fit the user's
13
+ * drawing to a particular shape.
14
+ *
15
+ * The shape returned by this function may be ignored if it has
16
+ * an empty bounding box.
17
+ *
18
+ * Although this returns a Promise, it should return *as fast as
19
+ * possible*.
20
+ */
10
21
  autocorrectShape?: () => Promise<AbstractComponent | null>;
11
22
  addPoint(point: StrokeDataPoint): void;
12
23
  }
@@ -70,7 +70,9 @@ export default class Display {
70
70
  },
71
71
  blockResolution: cacheBlockResolution,
72
72
  cacheSize: 600 * 600 * 4 * 90,
73
- maxScale: 1.3,
73
+ // On higher resolution displays, don't scale cache blocks as much to decrease blurriness.
74
+ // TODO: Decrease the minimum cache scale as well.
75
+ maxScale: Math.max(1, 1.3 / window.devicePixelRatio),
74
76
  // Require about 20 strokes with 4 parts each to cache an image in one of the
75
77
  // parts of the cache grid.
76
78
  minProportionalRenderTimePerCache: 20 * 4,
@@ -29,4 +29,5 @@ export default class DummyRenderer extends AbstractRenderer {
29
29
  isTooSmallToRender(_rect: Rect2): boolean;
30
30
  canRenderFromWithoutDataLoss(other: AbstractRenderer): boolean;
31
31
  renderFromOtherOfSameType(transform: Mat33, other: AbstractRenderer): void;
32
+ toString(): string;
32
33
  }
@@ -103,4 +103,7 @@ export default class DummyRenderer extends AbstractRenderer {
103
103
  return transform.transformVec2(point);
104
104
  }));
105
105
  }
106
+ toString() {
107
+ return '[DummyRenderer]';
108
+ }
106
109
  }
@@ -3,6 +3,7 @@ import { ToolbarLocalization } from './localization';
3
3
  import { ActionButtonIcon } from './types';
4
4
  import BaseWidget, { ToolbarWidgetTag } from './widgets/BaseWidget';
5
5
  import { DispatcherEventListener } from '../EventDispatcher';
6
+ import { BaseTool } from '../lib';
6
7
  export interface SpacerOptions {
7
8
  grow: number;
8
9
  minSize: string;
@@ -131,7 +132,7 @@ export default abstract class AbstractToolbar {
131
132
  /**
132
133
  * Adds an "Exit" button that, when clicked, calls `exitCallback`.
133
134
  *
134
- * **Note**: This is roughly equivalent to
135
+ * **Note**: This is *roughly* equivalent to
135
136
  * ```ts
136
137
  * toolbar.addTaggedActionButton([ ToolbarWidgetTag.Exit ], {
137
138
  * label: this.editor.localization.exit,
@@ -154,9 +155,24 @@ export default abstract class AbstractToolbar {
154
155
  */
155
156
  addUndoRedoButtons(undoFirst?: boolean): void;
156
157
  /**
157
- * Adds toolbar widgets based on the enabled tools.
158
+ * Adds widgets for pen/eraser/selection/text/pan-zoom primary tools.
159
+ *
160
+ * If `filter` returns `false` for a tool, no widget is added for that tool.
161
+ * See {@link addDefaultToolWidgets}
162
+ */
163
+ addWidgetsForPrimaryTools(filter?: (tool: BaseTool) => boolean): void;
164
+ /**
165
+ * Adds toolbar widgets based on the enabled tools, and additional tool-like
166
+ * buttons (e.g. {@link DocumentPropertiesWidget} and {@link InsertImageWidget}).
158
167
  */
159
168
  addDefaultToolWidgets(): void;
169
+ /**
170
+ * Adds widgets that don't correspond to tools, but do allow the user to control
171
+ * the editor in some way.
172
+ *
173
+ * By default, this includes {@link DocumentPropertiesWidget} and {@link InsertImageWidget}.
174
+ */
175
+ addDefaultEditorControlWidgets(): void;
160
176
  addDefaultActionButtons(): void;
161
177
  /**
162
178
  * Adds both the default tool widgets and action buttons.
@@ -30,6 +30,7 @@ import DocumentPropertiesWidget from './widgets/DocumentPropertiesWidget.mjs';
30
30
  import { Color4 } from '@js-draw/math';
31
31
  import { toolbarCSSPrefix } from './constants.mjs';
32
32
  import SaveActionWidget from './widgets/SaveActionWidget.mjs';
33
+ import ExitActionWidget from './widgets/ExitActionWidget.mjs';
33
34
  class AbstractToolbar {
34
35
  /** @internal */
35
36
  constructor(editor, localizationTable = defaultToolbarLocalization) {
@@ -292,14 +293,13 @@ class AbstractToolbar {
292
293
  */
293
294
  addSaveButton(saveCallback, labelOverride = {}) {
294
295
  const widget = new SaveActionWidget(this.editor, this.localizationTable, saveCallback, labelOverride);
295
- widget.setTags([ToolbarWidgetTag.Save]);
296
296
  this.addWidget(widget);
297
297
  return widget;
298
298
  }
299
299
  /**
300
300
  * Adds an "Exit" button that, when clicked, calls `exitCallback`.
301
301
  *
302
- * **Note**: This is roughly equivalent to
302
+ * **Note**: This is *roughly* equivalent to
303
303
  * ```ts
304
304
  * toolbar.addTaggedActionButton([ ToolbarWidgetTag.Exit ], {
305
305
  * label: this.editor.localization.exit,
@@ -316,15 +316,9 @@ class AbstractToolbar {
316
316
  * @final
317
317
  */
318
318
  addExitButton(exitCallback, labelOverride = {}) {
319
- return this.addTaggedActionButton([ToolbarWidgetTag.Exit], {
320
- label: this.editor.localization.exit,
321
- icon: this.editor.icons.makeCloseIcon(),
322
- ...labelOverride,
323
- }, () => {
324
- exitCallback();
325
- }, {
326
- autoDisableInReadOnlyEditors: false,
327
- });
319
+ const widget = new ExitActionWidget(this.editor, this.localizationTable, exitCallback, labelOverride);
320
+ this.addWidget(widget);
321
+ return widget;
328
322
  }
329
323
  /**
330
324
  * Adds undo and redo buttons that trigger the editor's built-in undo and redo
@@ -372,27 +366,49 @@ class AbstractToolbar {
372
366
  });
373
367
  }
374
368
  /**
375
- * Adds toolbar widgets based on the enabled tools.
369
+ * Adds widgets for pen/eraser/selection/text/pan-zoom primary tools.
370
+ *
371
+ * If `filter` returns `false` for a tool, no widget is added for that tool.
372
+ * See {@link addDefaultToolWidgets}
376
373
  */
377
- addDefaultToolWidgets() {
378
- const toolController = this.editor.toolController;
379
- for (const tool of toolController.getMatchingTools(PenTool)) {
380
- const widget = new PenToolWidget(this.editor, tool, this.localizationTable);
381
- this.addWidget(widget);
382
- }
383
- for (const tool of toolController.getMatchingTools(EraserTool)) {
384
- this.addWidget(new EraserWidget(this.editor, tool, this.localizationTable));
385
- }
386
- for (const tool of toolController.getMatchingTools(SelectionTool)) {
387
- this.addWidget(new SelectionToolWidget(this.editor, tool, this.localizationTable));
388
- }
389
- for (const tool of toolController.getMatchingTools(TextTool)) {
390
- this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
391
- }
392
- const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
393
- if (panZoomTool) {
394
- this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
374
+ addWidgetsForPrimaryTools(filter) {
375
+ for (const tool of this.editor.toolController.getPrimaryTools()) {
376
+ if (filter && !filter?.(tool)) {
377
+ continue;
378
+ }
379
+ if (tool instanceof PenTool) {
380
+ const widget = new PenToolWidget(this.editor, tool, this.localizationTable);
381
+ this.addWidget(widget);
382
+ }
383
+ else if (tool instanceof EraserTool) {
384
+ this.addWidget(new EraserWidget(this.editor, tool, this.localizationTable));
385
+ }
386
+ else if (tool instanceof SelectionTool) {
387
+ this.addWidget(new SelectionToolWidget(this.editor, tool, this.localizationTable));
388
+ }
389
+ else if (tool instanceof TextTool) {
390
+ this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
391
+ }
392
+ else if (tool instanceof PanZoomTool) {
393
+ this.addWidget(new HandToolWidget(this.editor, tool, this.localizationTable));
394
+ }
395
395
  }
396
+ }
397
+ /**
398
+ * Adds toolbar widgets based on the enabled tools, and additional tool-like
399
+ * buttons (e.g. {@link DocumentPropertiesWidget} and {@link InsertImageWidget}).
400
+ */
401
+ addDefaultToolWidgets() {
402
+ this.addWidgetsForPrimaryTools();
403
+ this.addDefaultEditorControlWidgets();
404
+ }
405
+ /**
406
+ * Adds widgets that don't correspond to tools, but do allow the user to control
407
+ * the editor in some way.
408
+ *
409
+ * By default, this includes {@link DocumentPropertiesWidget} and {@link InsertImageWidget}.
410
+ */
411
+ addDefaultEditorControlWidgets() {
396
412
  this.addWidget(new DocumentPropertiesWidget(this.editor, this.localizationTable));
397
413
  this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
398
414
  }
@@ -47,7 +47,7 @@ class BaseWidget {
47
47
  this.layoutManager = defaultLayoutManager;
48
48
  this.icon = null;
49
49
  this.container = document.createElement('div');
50
- this.container.classList.add(`${toolbarCSSPrefix}toolContainer`, `${toolbarCSSPrefix}toolButtonContainer`);
50
+ this.container.classList.add(`${toolbarCSSPrefix}toolContainer`, `${toolbarCSSPrefix}toolButtonContainer`, `${toolbarCSSPrefix}internalWidgetId--${id.replace(/[^a-zA-Z0-9_]/g, '-')}`);
51
51
  this.dropdownContent = document.createElement('div');
52
52
  __classPrivateFieldSet(this, _BaseWidget_hasDropdown, false, "f");
53
53
  this.button = document.createElement('div');
@@ -0,0 +1,12 @@
1
+ import { KeyPressEvent } from '../../inputEvents';
2
+ import Editor from '../../Editor';
3
+ import { ToolbarLocalization } from '../localization';
4
+ import ActionButtonWidget from './ActionButtonWidget';
5
+ import { ActionButtonIcon } from '../types';
6
+ declare class ExitActionWidget extends ActionButtonWidget {
7
+ constructor(editor: Editor, localization: ToolbarLocalization, saveCallback: () => void, labelOverride?: Partial<ActionButtonIcon>);
8
+ protected shouldAutoDisableInReadOnlyEditor(): boolean;
9
+ protected onKeyPress(event: KeyPressEvent): boolean;
10
+ mustBeInToplevelMenu(): boolean;
11
+ }
12
+ export default ExitActionWidget;
@@ -0,0 +1,27 @@
1
+ import ActionButtonWidget from './ActionButtonWidget.mjs';
2
+ import { ToolbarWidgetTag } from './BaseWidget.mjs';
3
+ import { exitKeyboardShortcut } from './keybindings.mjs';
4
+ class ExitActionWidget extends ActionButtonWidget {
5
+ constructor(editor, localization, saveCallback, labelOverride = {}) {
6
+ super(editor, 'exit-button',
7
+ // Creates an icon
8
+ () => {
9
+ return labelOverride.icon ?? editor.icons.makeCloseIcon();
10
+ }, labelOverride.label ?? localization.exit, saveCallback);
11
+ this.setTags([ToolbarWidgetTag.Exit]);
12
+ }
13
+ shouldAutoDisableInReadOnlyEditor() {
14
+ return false;
15
+ }
16
+ onKeyPress(event) {
17
+ if (this.editor.shortcuts.matchesShortcut(exitKeyboardShortcut, event)) {
18
+ this.clickAction();
19
+ return true;
20
+ }
21
+ return super.onKeyPress(event);
22
+ }
23
+ mustBeInToplevelMenu() {
24
+ return true;
25
+ }
26
+ }
27
+ export default ExitActionWidget;
@@ -4,11 +4,12 @@ import { ToolbarLocalization } from '../localization';
4
4
  import BaseToolWidget from './BaseToolWidget';
5
5
  import { SavedToolbuttonState } from './BaseWidget';
6
6
  export default class HandToolWidget extends BaseToolWidget {
7
- protected overridePanZoomTool: PanZoom;
8
7
  private allowTogglingBaseTool;
9
- constructor(editor: Editor, overridePanZoomTool: PanZoom, localizationTable: ToolbarLocalization);
10
- protected shouldAutoDisableInReadOnlyEditor(): boolean;
8
+ protected overridePanZoomTool: PanZoom;
9
+ constructor(editor: Editor, tool: PanZoom, localizationTable: ToolbarLocalization);
11
10
  private static getPrimaryHandTool;
11
+ private static getOverrideHandTool;
12
+ protected shouldAutoDisableInReadOnlyEditor(): boolean;
12
13
  protected getTitle(): string;
13
14
  protected createIcon(): Element;
14
15
  protected handleClick(): void;
@@ -96,34 +96,45 @@ class HandModeWidget extends BaseWidget {
96
96
  }
97
97
  export default class HandToolWidget extends BaseToolWidget {
98
98
  constructor(editor,
99
- // Pan zoom tool that overrides all other tools (enabling this tool for a device
100
- // causes that device to pan/zoom instead of interact with the primary tools)
101
- overridePanZoomTool, localizationTable) {
102
- const primaryHandTool = HandToolWidget.getPrimaryHandTool(editor.toolController);
103
- const tool = primaryHandTool ?? overridePanZoomTool;
104
- super(editor, tool, 'hand-tool-widget', localizationTable);
105
- this.overridePanZoomTool = overridePanZoomTool;
99
+ // Can either be the primary pan/zoom tool (in the primary tools list) or
100
+ // the override pan/zoom tool.
101
+ // If the override pan/zoom tool, the primary will be gotten from the editor's
102
+ // tool controller.
103
+ // If the primary, the override will be gotten from the editor's tool controller.
104
+ tool, localizationTable) {
105
+ const isGivenToolPrimary = editor.toolController.getPrimaryTools().includes(tool);
106
+ const primaryTool = (isGivenToolPrimary ? tool : HandToolWidget.getPrimaryHandTool(editor.toolController))
107
+ ?? tool;
108
+ super(editor, primaryTool, 'hand-tool-widget', localizationTable);
109
+ this.overridePanZoomTool =
110
+ (isGivenToolPrimary ? HandToolWidget.getOverrideHandTool(editor.toolController) : tool)
111
+ ?? tool;
106
112
  // Only allow toggling a hand tool if we're using the primary hand tool and not the override
107
113
  // hand tool for this button.
108
- this.allowTogglingBaseTool = primaryHandTool !== null;
114
+ this.allowTogglingBaseTool = primaryTool !== null;
109
115
  // Allow showing/hiding the dropdown, even if `overridePanZoomTool` isn't enabled.
110
116
  if (!this.allowTogglingBaseTool) {
111
117
  this.container.classList.add('dropdownShowable');
112
118
  }
113
119
  // Controls for the overriding hand tool.
114
- const touchPanningWidget = new HandModeWidget(editor, overridePanZoomTool, PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning, localizationTable);
115
- const rotationLockWidget = new HandModeWidget(editor, overridePanZoomTool, PanZoomMode.RotationLocked, () => this.editor.icons.makeRotationLockIcon(), localizationTable.lockRotation, localizationTable);
120
+ const touchPanningWidget = new HandModeWidget(editor, this.overridePanZoomTool, PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning, localizationTable);
121
+ const rotationLockWidget = new HandModeWidget(editor, this.overridePanZoomTool, PanZoomMode.RotationLocked, () => this.editor.icons.makeRotationLockIcon(), localizationTable.lockRotation, localizationTable);
116
122
  this.addSubWidget(touchPanningWidget);
117
123
  this.addSubWidget(rotationLockWidget);
118
124
  }
119
- shouldAutoDisableInReadOnlyEditor() {
120
- return false;
121
- }
122
125
  static getPrimaryHandTool(toolController) {
123
126
  const primaryPanZoomToolList = toolController.getPrimaryTools().filter(tool => tool instanceof PanZoom);
124
127
  const primaryPanZoomTool = primaryPanZoomToolList[0];
125
128
  return primaryPanZoomTool;
126
129
  }
130
+ static getOverrideHandTool(toolController) {
131
+ const panZoomToolList = toolController.getMatchingTools(PanZoom);
132
+ const panZoomTool = panZoomToolList[0];
133
+ return panZoomTool;
134
+ }
135
+ shouldAutoDisableInReadOnlyEditor() {
136
+ return false;
137
+ }
127
138
  getTitle() {
128
139
  return this.localizationTable.handTool;
129
140
  }