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
@@ -35,6 +35,7 @@ const DocumentPropertiesWidget_1 = __importDefault(require("./widgets/DocumentPr
35
35
  const math_1 = require("@js-draw/math");
36
36
  const constants_1 = require("./constants");
37
37
  const SaveActionWidget_1 = __importDefault(require("./widgets/SaveActionWidget"));
38
+ const ExitActionWidget_1 = __importDefault(require("./widgets/ExitActionWidget"));
38
39
  class AbstractToolbar {
39
40
  /** @internal */
40
41
  constructor(editor, localizationTable = localization_1.defaultToolbarLocalization) {
@@ -297,14 +298,13 @@ class AbstractToolbar {
297
298
  */
298
299
  addSaveButton(saveCallback, labelOverride = {}) {
299
300
  const widget = new SaveActionWidget_1.default(this.editor, this.localizationTable, saveCallback, labelOverride);
300
- widget.setTags([BaseWidget_1.ToolbarWidgetTag.Save]);
301
301
  this.addWidget(widget);
302
302
  return widget;
303
303
  }
304
304
  /**
305
305
  * Adds an "Exit" button that, when clicked, calls `exitCallback`.
306
306
  *
307
- * **Note**: This is roughly equivalent to
307
+ * **Note**: This is *roughly* equivalent to
308
308
  * ```ts
309
309
  * toolbar.addTaggedActionButton([ ToolbarWidgetTag.Exit ], {
310
310
  * label: this.editor.localization.exit,
@@ -321,15 +321,9 @@ class AbstractToolbar {
321
321
  * @final
322
322
  */
323
323
  addExitButton(exitCallback, labelOverride = {}) {
324
- return this.addTaggedActionButton([BaseWidget_1.ToolbarWidgetTag.Exit], {
325
- label: this.editor.localization.exit,
326
- icon: this.editor.icons.makeCloseIcon(),
327
- ...labelOverride,
328
- }, () => {
329
- exitCallback();
330
- }, {
331
- autoDisableInReadOnlyEditors: false,
332
- });
324
+ const widget = new ExitActionWidget_1.default(this.editor, this.localizationTable, exitCallback, labelOverride);
325
+ this.addWidget(widget);
326
+ return widget;
333
327
  }
334
328
  /**
335
329
  * Adds undo and redo buttons that trigger the editor's built-in undo and redo
@@ -377,27 +371,49 @@ class AbstractToolbar {
377
371
  });
378
372
  }
379
373
  /**
380
- * Adds toolbar widgets based on the enabled tools.
374
+ * Adds widgets for pen/eraser/selection/text/pan-zoom primary tools.
375
+ *
376
+ * If `filter` returns `false` for a tool, no widget is added for that tool.
377
+ * See {@link addDefaultToolWidgets}
381
378
  */
382
- addDefaultToolWidgets() {
383
- const toolController = this.editor.toolController;
384
- for (const tool of toolController.getMatchingTools(Pen_1.default)) {
385
- const widget = new PenToolWidget_1.default(this.editor, tool, this.localizationTable);
386
- this.addWidget(widget);
387
- }
388
- for (const tool of toolController.getMatchingTools(Eraser_1.default)) {
389
- this.addWidget(new EraserToolWidget_1.default(this.editor, tool, this.localizationTable));
390
- }
391
- for (const tool of toolController.getMatchingTools(SelectionTool_1.default)) {
392
- this.addWidget(new SelectionToolWidget_1.default(this.editor, tool, this.localizationTable));
393
- }
394
- for (const tool of toolController.getMatchingTools(TextTool_1.default)) {
395
- this.addWidget(new TextToolWidget_1.default(this.editor, tool, this.localizationTable));
396
- }
397
- const panZoomTool = toolController.getMatchingTools(PanZoom_1.default)[0];
398
- if (panZoomTool) {
399
- this.addWidget(new HandToolWidget_1.default(this.editor, panZoomTool, this.localizationTable));
379
+ addWidgetsForPrimaryTools(filter) {
380
+ for (const tool of this.editor.toolController.getPrimaryTools()) {
381
+ if (filter && !filter?.(tool)) {
382
+ continue;
383
+ }
384
+ if (tool instanceof Pen_1.default) {
385
+ const widget = new PenToolWidget_1.default(this.editor, tool, this.localizationTable);
386
+ this.addWidget(widget);
387
+ }
388
+ else if (tool instanceof Eraser_1.default) {
389
+ this.addWidget(new EraserToolWidget_1.default(this.editor, tool, this.localizationTable));
390
+ }
391
+ else if (tool instanceof SelectionTool_1.default) {
392
+ this.addWidget(new SelectionToolWidget_1.default(this.editor, tool, this.localizationTable));
393
+ }
394
+ else if (tool instanceof TextTool_1.default) {
395
+ this.addWidget(new TextToolWidget_1.default(this.editor, tool, this.localizationTable));
396
+ }
397
+ else if (tool instanceof PanZoom_1.default) {
398
+ this.addWidget(new HandToolWidget_1.default(this.editor, tool, this.localizationTable));
399
+ }
400
400
  }
401
+ }
402
+ /**
403
+ * Adds toolbar widgets based on the enabled tools, and additional tool-like
404
+ * buttons (e.g. {@link DocumentPropertiesWidget} and {@link InsertImageWidget}).
405
+ */
406
+ addDefaultToolWidgets() {
407
+ this.addWidgetsForPrimaryTools();
408
+ this.addDefaultEditorControlWidgets();
409
+ }
410
+ /**
411
+ * Adds widgets that don't correspond to tools, but do allow the user to control
412
+ * the editor in some way.
413
+ *
414
+ * By default, this includes {@link DocumentPropertiesWidget} and {@link InsertImageWidget}.
415
+ */
416
+ addDefaultEditorControlWidgets() {
401
417
  this.addWidget(new DocumentPropertiesWidget_1.default(this.editor, this.localizationTable));
402
418
  this.addWidget(new InsertImageWidget_1.default(this.editor, this.localizationTable));
403
419
  }
@@ -53,7 +53,7 @@ class BaseWidget {
53
53
  this.layoutManager = defaultLayoutManager;
54
54
  this.icon = null;
55
55
  this.container = document.createElement('div');
56
- this.container.classList.add(`${constants_1.toolbarCSSPrefix}toolContainer`, `${constants_1.toolbarCSSPrefix}toolButtonContainer`);
56
+ this.container.classList.add(`${constants_1.toolbarCSSPrefix}toolContainer`, `${constants_1.toolbarCSSPrefix}toolButtonContainer`, `${constants_1.toolbarCSSPrefix}internalWidgetId--${id.replace(/[^a-zA-Z0-9_]/g, '-')}`);
57
57
  this.dropdownContent = document.createElement('div');
58
58
  __classPrivateFieldSet(this, _BaseWidget_hasDropdown, false, "f");
59
59
  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,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const ActionButtonWidget_1 = __importDefault(require("./ActionButtonWidget"));
7
+ const BaseWidget_1 = require("./BaseWidget");
8
+ const keybindings_1 = require("./keybindings");
9
+ class ExitActionWidget extends ActionButtonWidget_1.default {
10
+ constructor(editor, localization, saveCallback, labelOverride = {}) {
11
+ super(editor, 'exit-button',
12
+ // Creates an icon
13
+ () => {
14
+ return labelOverride.icon ?? editor.icons.makeCloseIcon();
15
+ }, labelOverride.label ?? localization.exit, saveCallback);
16
+ this.setTags([BaseWidget_1.ToolbarWidgetTag.Exit]);
17
+ }
18
+ shouldAutoDisableInReadOnlyEditor() {
19
+ return false;
20
+ }
21
+ onKeyPress(event) {
22
+ if (this.editor.shortcuts.matchesShortcut(keybindings_1.exitKeyboardShortcut, event)) {
23
+ this.clickAction();
24
+ return true;
25
+ }
26
+ return super.onKeyPress(event);
27
+ }
28
+ mustBeInToplevelMenu() {
29
+ return true;
30
+ }
31
+ }
32
+ exports.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;
@@ -124,34 +124,45 @@ class HandModeWidget extends BaseWidget_1.default {
124
124
  }
125
125
  class HandToolWidget extends BaseToolWidget_1.default {
126
126
  constructor(editor,
127
- // Pan zoom tool that overrides all other tools (enabling this tool for a device
128
- // causes that device to pan/zoom instead of interact with the primary tools)
129
- overridePanZoomTool, localizationTable) {
130
- const primaryHandTool = HandToolWidget.getPrimaryHandTool(editor.toolController);
131
- const tool = primaryHandTool ?? overridePanZoomTool;
132
- super(editor, tool, 'hand-tool-widget', localizationTable);
133
- this.overridePanZoomTool = overridePanZoomTool;
127
+ // Can either be the primary pan/zoom tool (in the primary tools list) or
128
+ // the override pan/zoom tool.
129
+ // If the override pan/zoom tool, the primary will be gotten from the editor's
130
+ // tool controller.
131
+ // If the primary, the override will be gotten from the editor's tool controller.
132
+ tool, localizationTable) {
133
+ const isGivenToolPrimary = editor.toolController.getPrimaryTools().includes(tool);
134
+ const primaryTool = (isGivenToolPrimary ? tool : HandToolWidget.getPrimaryHandTool(editor.toolController))
135
+ ?? tool;
136
+ super(editor, primaryTool, 'hand-tool-widget', localizationTable);
137
+ this.overridePanZoomTool =
138
+ (isGivenToolPrimary ? HandToolWidget.getOverrideHandTool(editor.toolController) : tool)
139
+ ?? tool;
134
140
  // Only allow toggling a hand tool if we're using the primary hand tool and not the override
135
141
  // hand tool for this button.
136
- this.allowTogglingBaseTool = primaryHandTool !== null;
142
+ this.allowTogglingBaseTool = primaryTool !== null;
137
143
  // Allow showing/hiding the dropdown, even if `overridePanZoomTool` isn't enabled.
138
144
  if (!this.allowTogglingBaseTool) {
139
145
  this.container.classList.add('dropdownShowable');
140
146
  }
141
147
  // Controls for the overriding hand tool.
142
- const touchPanningWidget = new HandModeWidget(editor, overridePanZoomTool, PanZoom_1.PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning, localizationTable);
143
- const rotationLockWidget = new HandModeWidget(editor, overridePanZoomTool, PanZoom_1.PanZoomMode.RotationLocked, () => this.editor.icons.makeRotationLockIcon(), localizationTable.lockRotation, localizationTable);
148
+ const touchPanningWidget = new HandModeWidget(editor, this.overridePanZoomTool, PanZoom_1.PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning, localizationTable);
149
+ const rotationLockWidget = new HandModeWidget(editor, this.overridePanZoomTool, PanZoom_1.PanZoomMode.RotationLocked, () => this.editor.icons.makeRotationLockIcon(), localizationTable.lockRotation, localizationTable);
144
150
  this.addSubWidget(touchPanningWidget);
145
151
  this.addSubWidget(rotationLockWidget);
146
152
  }
147
- shouldAutoDisableInReadOnlyEditor() {
148
- return false;
149
- }
150
153
  static getPrimaryHandTool(toolController) {
151
154
  const primaryPanZoomToolList = toolController.getPrimaryTools().filter(tool => tool instanceof PanZoom_1.default);
152
155
  const primaryPanZoomTool = primaryPanZoomToolList[0];
153
156
  return primaryPanZoomTool;
154
157
  }
158
+ static getOverrideHandTool(toolController) {
159
+ const panZoomToolList = toolController.getMatchingTools(PanZoom_1.default);
160
+ const panZoomTool = panZoomToolList[0];
161
+ return panZoomTool;
162
+ }
163
+ shouldAutoDisableInReadOnlyEditor() {
164
+ return false;
165
+ }
155
166
  getTitle() {
156
167
  return this.localizationTable.handTool;
157
168
  }
@@ -174,7 +174,7 @@ class InsertImageWidget extends BaseWidget_1.default {
174
174
  this.image?.reset();
175
175
  };
176
176
  this.statusView.replaceChildren(sizeText);
177
- const largeImageThreshold = 0.25; // MiB
177
+ const largeImageThreshold = 0.12; // MiB
178
178
  if (sizeInMiB > largeImageThreshold) {
179
179
  this.statusView.appendChild(decreaseSizeButton);
180
180
  }
@@ -1,3 +1,4 @@
1
1
  export declare const resizeImageToSelectionKeyboardShortcut = "jsdraw.toolbar.SelectionTool.resizeImageToSelection";
2
2
  export declare const selectStrokeTypeKeyboardShortcutIds: string[];
3
3
  export declare const saveKeyboardShortcut = "jsdraw.toolbar.SaveActionWidget.save";
4
+ export declare const exitKeyboardShortcut = "jsdraw.toolbar.ExitActionWidget.exit";
@@ -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.saveKeyboardShortcut = exports.selectStrokeTypeKeyboardShortcutIds = exports.resizeImageToSelectionKeyboardShortcut = void 0;
6
+ exports.exitKeyboardShortcut = exports.saveKeyboardShortcut = exports.selectStrokeTypeKeyboardShortcutIds = exports.resizeImageToSelectionKeyboardShortcut = void 0;
7
7
  const KeyboardShortcutManager_1 = __importDefault(require("../../shortcuts/KeyboardShortcutManager"));
8
8
  // Selection
9
9
  exports.resizeImageToSelectionKeyboardShortcut = 'jsdraw.toolbar.SelectionTool.resizeImageToSelection';
@@ -17,3 +17,6 @@ for (let i = 0; i < exports.selectStrokeTypeKeyboardShortcutIds.length; i++) {
17
17
  // Save
18
18
  exports.saveKeyboardShortcut = 'jsdraw.toolbar.SaveActionWidget.save';
19
19
  KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(exports.saveKeyboardShortcut, ['ctrlOrMeta+KeyS'], 'Save');
20
+ // Exit
21
+ exports.exitKeyboardShortcut = 'jsdraw.toolbar.ExitActionWidget.exit';
22
+ KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(exports.exitKeyboardShortcut, ['Alt+KeyQ'], 'Exit');
@@ -1,4 +1,4 @@
1
- import ReactiveValue from 'js-draw/src/util/ReactiveValue';
1
+ import ReactiveValue from '../../../util/ReactiveValue';
2
2
  /**
3
3
  * A class that manages whether/what content is shown for a widget.
4
4
  *
@@ -39,7 +39,7 @@ class FindTool extends BaseTool_1.default {
39
39
  }
40
40
  if (matchIdx < matches.length) {
41
41
  const undoable = false;
42
- this.editor.dispatch(this.editor.viewport.zoomTo(matches[matchIdx], true, true), undoable);
42
+ void this.editor.dispatch(this.editor.viewport.zoomTo(matches[matchIdx], true, true), undoable);
43
43
  this.editor.announceForAccessibility(this.editor.localization.focusedFoundText(matchIdx + 1, matches.length));
44
44
  }
45
45
  }
@@ -102,8 +102,8 @@ class Pen extends BaseTool_1.default {
102
102
  this.currentDeviceType = current.device;
103
103
  if (this.shapeAutocompletionEnabled) {
104
104
  const stationaryDetectionConfig = {
105
- maxSpeed: 5,
106
- maxRadius: 10,
105
+ maxSpeed: 8.5,
106
+ maxRadius: 11,
107
107
  minTimeSeconds: 0.5, // s
108
108
  };
109
109
  this.stationaryDetector = new StationaryPenDetector_1.default(current, stationaryDetectionConfig, pointer => this.autocorrectShape(pointer));
@@ -146,6 +146,7 @@ class Pen extends BaseTool_1.default {
146
146
  if (this.autocorrectedShape) {
147
147
  this.removedAutocorrectedShapeTime = performance.now();
148
148
  this.autocorrectedShape = null;
149
+ this.editor.announceForAccessibility(this.editor.localization.autocorrectionCanceled);
149
150
  }
150
151
  }
151
152
  }
@@ -192,6 +193,13 @@ class Pen extends BaseTool_1.default {
192
193
  if (!this.builder || !correctedShape) {
193
194
  return;
194
195
  }
196
+ // Don't complete to empty shapes.
197
+ const bboxArea = correctedShape.getBBox().area;
198
+ if (bboxArea === 0 || !isFinite(bboxArea)) {
199
+ return;
200
+ }
201
+ const shapeDescription = correctedShape.description(this.editor.localization);
202
+ this.editor.announceForAccessibility(this.editor.localization.autocorrectedTo(shapeDescription));
195
203
  this.autocorrectedShape = correctedShape;
196
204
  this.lastAutocorrectedShape = correctedShape;
197
205
  this.previewStroke();
@@ -206,6 +214,9 @@ class Pen extends BaseTool_1.default {
206
214
  const stroke = this.autocorrectedShape ?? this.builder.build();
207
215
  this.previewStroke();
208
216
  if (stroke.getBBox().area > 0) {
217
+ if (stroke === this.autocorrectedShape) {
218
+ this.editor.announceForAccessibility(this.editor.localization.autocorrectedTo(stroke.description(this.editor.localization)));
219
+ }
209
220
  const canFlatten = true;
210
221
  const action = EditorImage_1.default.addElement(stroke, canFlatten);
211
222
  this.editor.dispatch(action);
@@ -2,6 +2,7 @@
2
2
  * @internal
3
3
  * @packageDocumentation
4
4
  */
5
+ import SerializableCommand from '../../commands/SerializableCommand';
5
6
  import Editor from '../../Editor';
6
7
  import { Mat33, Rect2, Point2 } from '@js-draw/math';
7
8
  import Pointer from '../../Pointer';
@@ -36,7 +37,10 @@ export default class Selection {
36
37
  getScreenRegion(): Rect2;
37
38
  get screenRegionRotation(): number;
38
39
  setTransform(transform: Mat33, preview?: boolean): void;
40
+ private getDeltaZIndexToMoveSelectionToTop;
39
41
  finalizeTransform(): void | Promise<void>;
42
+ /** Sends all selected elements to the bottom of the visible image. */
43
+ sendToBack(): SerializableCommand | null;
40
44
  private static ApplyTransformationCommand;
41
45
  private previewTransformCmds;
42
46
  resolveToObjects(): boolean;
@@ -41,6 +41,7 @@ const Duplicate_1 = __importDefault(require("../../commands/Duplicate"));
41
41
  const TransformMode_1 = require("./TransformMode");
42
42
  const types_1 = require("./types");
43
43
  const EditorImage_1 = __importDefault(require("../../image/EditorImage"));
44
+ const uniteCommands_1 = __importDefault(require("../../commands/uniteCommands"));
44
45
  const updateChunkSize = 100;
45
46
  const maxPreviewElemCount = 500;
46
47
  // @internal
@@ -51,6 +52,7 @@ class Selection {
51
52
  // @see getTightBoundingBox
52
53
  this.selectionTightBoundingBox = null;
53
54
  this.transform = math_1.Mat33.identity;
55
+ // invariant: sorted by increasing z-index
54
56
  this.selectedElems = [];
55
57
  this.hasParent = true;
56
58
  // Maps IDs to whether we removed the component from the image
@@ -161,6 +163,16 @@ class Selection {
161
163
  this.previewTransformCmds();
162
164
  }
163
165
  }
166
+ getDeltaZIndexToMoveSelectionToTop() {
167
+ if (this.selectedElems.length === 0) {
168
+ return 0;
169
+ }
170
+ const selectedBottommostZIndex = this.selectedElems[0].getZIndex();
171
+ const visibleObjects = this.editor.image.getElementsIntersectingRegion(this.region);
172
+ const topMostVisibleZIndex = visibleObjects[visibleObjects.length - 1]?.getZIndex() ?? selectedBottommostZIndex;
173
+ const deltaZIndex = (topMostVisibleZIndex + 1) - selectedBottommostZIndex;
174
+ return deltaZIndex;
175
+ }
164
176
  // Applies the current transformation to the selection
165
177
  finalizeTransform() {
166
178
  const fullTransform = this.transform;
@@ -169,17 +181,35 @@ class Selection {
169
181
  this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
170
182
  this.transform = math_1.Mat33.identity;
171
183
  this.scrollTo();
184
+ let transformPromise = undefined;
172
185
  // Make the commands undo-able.
173
186
  // Don't check for non-empty transforms because this breaks changing the
174
187
  // z-index of the just-transformed commands.
175
- //
176
- // TODO: Check whether the selectedElems are already all toplevel.
177
- const transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform));
188
+ if (this.selectedElems.length > 0) {
189
+ const deltaZIndex = this.getDeltaZIndexToMoveSelectionToTop();
190
+ transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform, deltaZIndex));
191
+ }
178
192
  // Clear renderings of any in-progress transformations
179
193
  const wetInkRenderer = this.editor.display.getWetInkRenderer();
180
194
  wetInkRenderer.clear();
181
195
  return transformPromise;
182
196
  }
197
+ /** Sends all selected elements to the bottom of the visible image. */
198
+ sendToBack() {
199
+ const visibleObjects = this.editor.image.getElementsIntersectingRegion(this.editor.viewport.visibleRect);
200
+ // VisibleObjects and selectedElems should both be sorted by z-index
201
+ const lowestVisibleZIndex = visibleObjects[0]?.getZIndex() ?? 0;
202
+ const highestSelectedZIndex = this.selectedElems[this.selectedElems.length - 1]?.getZIndex() ?? 0;
203
+ const targetHighestZIndex = lowestVisibleZIndex - 1;
204
+ const deltaZIndex = targetHighestZIndex - highestSelectedZIndex;
205
+ if (deltaZIndex !== 0) {
206
+ const commands = this.selectedElems.map(elem => {
207
+ return elem.setZIndex(elem.getZIndex() + deltaZIndex);
208
+ });
209
+ return (0, uniteCommands_1.default)(commands, updateChunkSize);
210
+ }
211
+ return null;
212
+ }
183
213
  // Preview the effects of the current transformation on the selection
184
214
  previewTransformCmds() {
185
215
  if (this.selectedElems.length === 0) {
@@ -193,7 +223,7 @@ class Selection {
193
223
  const wetInkRenderer = this.editor.display.getWetInkRenderer();
194
224
  wetInkRenderer.clear();
195
225
  wetInkRenderer.pushTransform(this.transform);
196
- const viewportVisibleRect = this.editor.viewport.visibleRect;
226
+ const viewportVisibleRect = this.editor.viewport.visibleRect.union(this.region);
197
227
  const visibleRect = viewportVisibleRect.transformedBoundingBox(this.transform.inverse());
198
228
  for (const elem of this.selectedElems) {
199
229
  elem.render(wetInkRenderer, visibleRect);
@@ -439,7 +469,8 @@ class Selection {
439
469
  if (wasTransforming) {
440
470
  // Don't update the selection's focus when redoing/undoing
441
471
  const selectionToUpdate = null;
442
- tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
472
+ const deltaZIndex = this.getDeltaZIndexToMoveSelectionToTop();
473
+ tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform, deltaZIndex);
443
474
  // Transform to ensure that the duplicates are in the correct location
444
475
  await tmpApplyCommand.apply(this.editor);
445
476
  // Show items again
@@ -480,6 +511,8 @@ class Selection {
480
511
  this.originalRegion = bbox;
481
512
  this.selectionTightBoundingBox = bbox;
482
513
  this.selectedElems = objects.filter(object => object.isSelectable());
514
+ // Enforce increasing z-index invariant
515
+ this.selectedElems.sort((a, b) => a.getZIndex() - b.getZIndex());
483
516
  this.padRegion();
484
517
  this.updateUI();
485
518
  }
@@ -493,7 +526,8 @@ _a = Selection;
493
526
  // The selection box is lost when serializing/deserializing. No need to store box rotation
494
527
  const fullTransform = new math_1.Mat33(...json.transform);
495
528
  const elemIds = (json.elems ?? []);
496
- return new _a.ApplyTransformationCommand(null, elemIds, fullTransform);
529
+ const deltaZIndex = parseInt(json.deltaZIndex ?? 0);
530
+ return new _a.ApplyTransformationCommand(null, elemIds, fullTransform, deltaZIndex);
497
531
  });
498
532
  })();
499
533
  Selection.ApplyTransformationCommand = class extends SerializableCommand_1.default {
@@ -501,10 +535,11 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand_1.defau
501
535
  // If a `string[]`, selectedElems is a list of element IDs.
502
536
  selectedElems,
503
537
  // Full transformation used to transform elements.
504
- fullTransform) {
538
+ fullTransform, deltaZIndex) {
505
539
  super('selection-tool-transform');
506
540
  this.selection = selection;
507
541
  this.fullTransform = fullTransform;
542
+ this.deltaZIndex = deltaZIndex;
508
543
  const isIDList = (arr) => {
509
544
  return typeof arr[0] === 'string';
510
545
  };
@@ -515,11 +550,11 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand_1.defau
515
550
  else {
516
551
  this.selectedElemIds = selectedElems.map(elem => elem.getId());
517
552
  this.transformCommands = selectedElems.map(elem => {
518
- return elem.transformBy(this.fullTransform);
553
+ return elem.setZIndexAndTransformBy(this.fullTransform, elem.getZIndex() + deltaZIndex);
519
554
  });
520
555
  }
521
556
  }
522
- resolveToElems(editor) {
557
+ resolveToElems(editor, isUndoing) {
523
558
  if (this.transformCommands) {
524
559
  return;
525
560
  }
@@ -528,11 +563,19 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand_1.defau
528
563
  if (!elem) {
529
564
  throw new Error(`Unable to find element with ID, ${id}.`);
530
565
  }
531
- return elem.transformBy(this.fullTransform);
566
+ let originalZIndex = elem.getZIndex();
567
+ let targetZIndex = elem.getZIndex() + this.deltaZIndex;
568
+ // If the command has already been applied, the element should currently
569
+ // have the target z-index.
570
+ if (isUndoing) {
571
+ targetZIndex = elem.getZIndex();
572
+ originalZIndex = elem.getZIndex() - this.deltaZIndex;
573
+ }
574
+ return elem.setZIndexAndTransformBy(this.fullTransform, targetZIndex, originalZIndex);
532
575
  });
533
576
  }
534
577
  async apply(editor) {
535
- this.resolveToElems(editor);
578
+ this.resolveToElems(editor, false);
536
579
  this.selection?.setTransform(this.fullTransform, false);
537
580
  this.selection?.updateUI();
538
581
  await editor.asyncApplyCommands(this.transformCommands, updateChunkSize);
@@ -541,7 +584,7 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand_1.defau
541
584
  this.selection?.updateUI();
542
585
  }
543
586
  async unapply(editor) {
544
- this.resolveToElems(editor);
587
+ this.resolveToElems(editor, true);
545
588
  this.selection?.setTransform(this.fullTransform.inverse(), false);
546
589
  this.selection?.updateUI();
547
590
  await editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize, true);
@@ -553,6 +596,7 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand_1.defau
553
596
  return {
554
597
  elems: this.selectedElemIds,
555
598
  transform: this.fullTransform.toArray(),
599
+ deltaZIndex: this.deltaZIndex,
556
600
  };
557
601
  }
558
602
  description(_editor, localizationTable) {
@@ -28,6 +28,7 @@ export default class SelectionTool extends BaseTool {
28
28
  private onSelectionUpdated;
29
29
  private zoomToSelection;
30
30
  private static handleableKeys;
31
+ private hasUnfinalizedTransformFromKeyPress;
31
32
  onKeyPress(event: KeyPressEvent): boolean;
32
33
  onKeyUp(evt: KeyUpEvent): boolean;
33
34
  onCopy(event: CopyEvent): boolean;
@@ -27,6 +27,9 @@ class SelectionTool extends BaseTool_1.default {
27
27
  this.lastPointer = null;
28
28
  this.selectionBoxHandlingEvt = false;
29
29
  this.lastSelectedObjects = [];
30
+ // Whether the last keypress corresponded to an action that didn't transform the
31
+ // selection (and thus does not need to be finalized on onKeyUp).
32
+ this.hasUnfinalizedTransformFromKeyPress = false;
30
33
  this.autoscroller = new ToPointerAutoscroller_1.default(editor.viewport, (scrollBy) => {
31
34
  editor.dispatch(Viewport_1.default.transformBy(math_1.Mat33.translation(scrollBy)), false);
32
35
  // Update the selection box/content to match the new viewport.
@@ -219,7 +222,8 @@ class SelectionTool extends BaseTool_1.default {
219
222
  this.snapToGrid = true;
220
223
  return true;
221
224
  }
222
- if (this.selectionBox && shortcucts.matchesShortcut(keybindings_1.duplicateSelectionShortcut, event)) {
225
+ if (this.selectionBox && (shortcucts.matchesShortcut(keybindings_1.duplicateSelectionShortcut, event)
226
+ || shortcucts.matchesShortcut(keybindings_1.sendToBackSelectionShortcut, event))) {
223
227
  // Handle duplication on key up — we don't want to accidentally duplicate
224
228
  // many times.
225
229
  return true;
@@ -233,9 +237,11 @@ class SelectionTool extends BaseTool_1.default {
233
237
  // Pass it to another tool, if apliccable.
234
238
  return false;
235
239
  }
236
- else if (event.key === 'Shift') {
240
+ else if (event.shiftKey || event.key === 'Shift') {
237
241
  this.shiftKeyPressed = true;
238
- return true;
242
+ if (event.key === 'Shift') {
243
+ return true;
244
+ }
239
245
  }
240
246
  let rotationSteps = 0;
241
247
  let xTranslateSteps = 0;
@@ -303,6 +309,8 @@ class SelectionTool extends BaseTool_1.default {
303
309
  const oldTransform = this.selectionBox.getTransform();
304
310
  this.selectionBox.setTransform(oldTransform.rightMul(transform));
305
311
  this.selectionBox.scrollTo();
312
+ // The transformation needs to be finalized at some point (on key up)
313
+ this.hasUnfinalizedTransformFromKeyPress = true;
306
314
  }
307
315
  if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
308
316
  this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
@@ -328,12 +336,32 @@ class SelectionTool extends BaseTool_1.default {
328
336
  });
329
337
  return true;
330
338
  }
339
+ if (this.selectionBox && shortcucts.matchesShortcut(keybindings_1.sendToBackSelectionShortcut, evt)) {
340
+ const sendToBackCommand = this.selectionBox.sendToBack();
341
+ if (sendToBackCommand) {
342
+ this.editor.dispatch(sendToBackCommand);
343
+ }
344
+ return true;
345
+ }
346
+ // Here, we check if shiftKey === false because, as of this writing,
347
+ // evt.shiftKey is an optional property. Being falsey could just mean
348
+ // that it wasn't set.
349
+ if (evt.shiftKey === false) {
350
+ this.shiftKeyPressed = false;
351
+ // Don't return immediately -- event may be otherwise handled
352
+ }
353
+ // Also check for key === 'Shift' (for the case where shiftKey is undefined)
331
354
  if (evt.key === 'Shift') {
332
355
  this.shiftKeyPressed = false;
333
356
  return true;
334
357
  }
358
+ // If we don't need to finalize the transform
359
+ if (!this.hasUnfinalizedTransformFromKeyPress) {
360
+ return true;
361
+ }
335
362
  if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
336
363
  this.selectionBox.finalizeTransform();
364
+ this.hasUnfinalizedTransformFromKeyPress = false;
337
365
  return true;
338
366
  }
339
367
  return false;
@@ -367,7 +395,11 @@ class SelectionTool extends BaseTool_1.default {
367
395
  return true;
368
396
  }
369
397
  setEnabled(enabled) {
398
+ const wasEnabled = this.isEnabled();
370
399
  super.setEnabled(enabled);
400
+ if (wasEnabled === enabled) {
401
+ return;
402
+ }
371
403
  // Clear the selection
372
404
  this.selectionBox?.cancelSelection();
373
405
  this.onSelectionUpdated();
@@ -7,7 +7,6 @@ import BaseTool from './BaseTool';
7
7
  *
8
8
  * This is in the default set of {@link ToolController} tools.
9
9
  *
10
- * @deprecated This may be replaced in the future.
11
10
  */
12
11
  export default class ToolSwitcherShortcut extends BaseTool {
13
12
  private editor;
@@ -10,7 +10,6 @@ const BaseTool_1 = __importDefault(require("./BaseTool"));
10
10
  *
11
11
  * This is in the default set of {@link ToolController} tools.
12
12
  *
13
- * @deprecated This may be replaced in the future.
14
13
  */
15
14
  class ToolSwitcherShortcut extends BaseTool_1.default {
16
15
  constructor(editor) {