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
@@ -169,7 +169,7 @@ class InsertImageWidget extends BaseWidget {
169
169
  this.image?.reset();
170
170
  };
171
171
  this.statusView.replaceChildren(sizeText);
172
- const largeImageThreshold = 0.25; // MiB
172
+ const largeImageThreshold = 0.12; // MiB
173
173
  if (sizeInMiB > largeImageThreshold) {
174
174
  this.statusView.appendChild(decreaseSizeButton);
175
175
  }
@@ -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";
@@ -11,3 +11,6 @@ for (let i = 0; i < selectStrokeTypeKeyboardShortcutIds.length; i++) {
11
11
  // Save
12
12
  export const saveKeyboardShortcut = 'jsdraw.toolbar.SaveActionWidget.save';
13
13
  KeyboardShortcutManager.registerDefaultKeyboardShortcut(saveKeyboardShortcut, ['ctrlOrMeta+KeyS'], 'Save');
14
+ // Exit
15
+ export const exitKeyboardShortcut = 'jsdraw.toolbar.ExitActionWidget.exit';
16
+ KeyboardShortcutManager.registerDefaultKeyboardShortcut(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
  *
@@ -34,7 +34,7 @@ export default class FindTool extends BaseTool {
34
34
  }
35
35
  if (matchIdx < matches.length) {
36
36
  const undoable = false;
37
- this.editor.dispatch(this.editor.viewport.zoomTo(matches[matchIdx], true, true), undoable);
37
+ void this.editor.dispatch(this.editor.viewport.zoomTo(matches[matchIdx], true, true), undoable);
38
38
  this.editor.announceForAccessibility(this.editor.localization.focusedFoundText(matchIdx + 1, matches.length));
39
39
  }
40
40
  }
@@ -97,8 +97,8 @@ export default class Pen extends BaseTool {
97
97
  this.currentDeviceType = current.device;
98
98
  if (this.shapeAutocompletionEnabled) {
99
99
  const stationaryDetectionConfig = {
100
- maxSpeed: 5,
101
- maxRadius: 10,
100
+ maxSpeed: 8.5,
101
+ maxRadius: 11,
102
102
  minTimeSeconds: 0.5, // s
103
103
  };
104
104
  this.stationaryDetector = new StationaryPenDetector(current, stationaryDetectionConfig, pointer => this.autocorrectShape(pointer));
@@ -141,6 +141,7 @@ export default class Pen extends BaseTool {
141
141
  if (this.autocorrectedShape) {
142
142
  this.removedAutocorrectedShapeTime = performance.now();
143
143
  this.autocorrectedShape = null;
144
+ this.editor.announceForAccessibility(this.editor.localization.autocorrectionCanceled);
144
145
  }
145
146
  }
146
147
  }
@@ -187,6 +188,13 @@ export default class Pen extends BaseTool {
187
188
  if (!this.builder || !correctedShape) {
188
189
  return;
189
190
  }
191
+ // Don't complete to empty shapes.
192
+ const bboxArea = correctedShape.getBBox().area;
193
+ if (bboxArea === 0 || !isFinite(bboxArea)) {
194
+ return;
195
+ }
196
+ const shapeDescription = correctedShape.description(this.editor.localization);
197
+ this.editor.announceForAccessibility(this.editor.localization.autocorrectedTo(shapeDescription));
190
198
  this.autocorrectedShape = correctedShape;
191
199
  this.lastAutocorrectedShape = correctedShape;
192
200
  this.previewStroke();
@@ -201,6 +209,9 @@ export default class Pen extends BaseTool {
201
209
  const stroke = this.autocorrectedShape ?? this.builder.build();
202
210
  this.previewStroke();
203
211
  if (stroke.getBBox().area > 0) {
212
+ if (stroke === this.autocorrectedShape) {
213
+ this.editor.announceForAccessibility(this.editor.localization.autocorrectedTo(stroke.description(this.editor.localization)));
214
+ }
204
215
  const canFlatten = true;
205
216
  const action = EditorImage.addElement(stroke, canFlatten);
206
217
  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;
@@ -13,6 +13,7 @@ import Duplicate from '../../commands/Duplicate.mjs';
13
13
  import { DragTransformer, ResizeTransformer, RotateTransformer } from './TransformMode.mjs';
14
14
  import { ResizeMode } from './types.mjs';
15
15
  import EditorImage from '../../image/EditorImage.mjs';
16
+ import uniteCommands from '../../commands/uniteCommands.mjs';
16
17
  const updateChunkSize = 100;
17
18
  const maxPreviewElemCount = 500;
18
19
  // @internal
@@ -23,6 +24,7 @@ class Selection {
23
24
  // @see getTightBoundingBox
24
25
  this.selectionTightBoundingBox = null;
25
26
  this.transform = Mat33.identity;
27
+ // invariant: sorted by increasing z-index
26
28
  this.selectedElems = [];
27
29
  this.hasParent = true;
28
30
  // Maps IDs to whether we removed the component from the image
@@ -133,6 +135,16 @@ class Selection {
133
135
  this.previewTransformCmds();
134
136
  }
135
137
  }
138
+ getDeltaZIndexToMoveSelectionToTop() {
139
+ if (this.selectedElems.length === 0) {
140
+ return 0;
141
+ }
142
+ const selectedBottommostZIndex = this.selectedElems[0].getZIndex();
143
+ const visibleObjects = this.editor.image.getElementsIntersectingRegion(this.region);
144
+ const topMostVisibleZIndex = visibleObjects[visibleObjects.length - 1]?.getZIndex() ?? selectedBottommostZIndex;
145
+ const deltaZIndex = (topMostVisibleZIndex + 1) - selectedBottommostZIndex;
146
+ return deltaZIndex;
147
+ }
136
148
  // Applies the current transformation to the selection
137
149
  finalizeTransform() {
138
150
  const fullTransform = this.transform;
@@ -141,17 +153,35 @@ class Selection {
141
153
  this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
142
154
  this.transform = Mat33.identity;
143
155
  this.scrollTo();
156
+ let transformPromise = undefined;
144
157
  // Make the commands undo-able.
145
158
  // Don't check for non-empty transforms because this breaks changing the
146
159
  // z-index of the just-transformed commands.
147
- //
148
- // TODO: Check whether the selectedElems are already all toplevel.
149
- const transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform));
160
+ if (this.selectedElems.length > 0) {
161
+ const deltaZIndex = this.getDeltaZIndexToMoveSelectionToTop();
162
+ transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform, deltaZIndex));
163
+ }
150
164
  // Clear renderings of any in-progress transformations
151
165
  const wetInkRenderer = this.editor.display.getWetInkRenderer();
152
166
  wetInkRenderer.clear();
153
167
  return transformPromise;
154
168
  }
169
+ /** Sends all selected elements to the bottom of the visible image. */
170
+ sendToBack() {
171
+ const visibleObjects = this.editor.image.getElementsIntersectingRegion(this.editor.viewport.visibleRect);
172
+ // VisibleObjects and selectedElems should both be sorted by z-index
173
+ const lowestVisibleZIndex = visibleObjects[0]?.getZIndex() ?? 0;
174
+ const highestSelectedZIndex = this.selectedElems[this.selectedElems.length - 1]?.getZIndex() ?? 0;
175
+ const targetHighestZIndex = lowestVisibleZIndex - 1;
176
+ const deltaZIndex = targetHighestZIndex - highestSelectedZIndex;
177
+ if (deltaZIndex !== 0) {
178
+ const commands = this.selectedElems.map(elem => {
179
+ return elem.setZIndex(elem.getZIndex() + deltaZIndex);
180
+ });
181
+ return uniteCommands(commands, updateChunkSize);
182
+ }
183
+ return null;
184
+ }
155
185
  // Preview the effects of the current transformation on the selection
156
186
  previewTransformCmds() {
157
187
  if (this.selectedElems.length === 0) {
@@ -165,7 +195,7 @@ class Selection {
165
195
  const wetInkRenderer = this.editor.display.getWetInkRenderer();
166
196
  wetInkRenderer.clear();
167
197
  wetInkRenderer.pushTransform(this.transform);
168
- const viewportVisibleRect = this.editor.viewport.visibleRect;
198
+ const viewportVisibleRect = this.editor.viewport.visibleRect.union(this.region);
169
199
  const visibleRect = viewportVisibleRect.transformedBoundingBox(this.transform.inverse());
170
200
  for (const elem of this.selectedElems) {
171
201
  elem.render(wetInkRenderer, visibleRect);
@@ -411,7 +441,8 @@ class Selection {
411
441
  if (wasTransforming) {
412
442
  // Don't update the selection's focus when redoing/undoing
413
443
  const selectionToUpdate = null;
414
- tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
444
+ const deltaZIndex = this.getDeltaZIndexToMoveSelectionToTop();
445
+ tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform, deltaZIndex);
415
446
  // Transform to ensure that the duplicates are in the correct location
416
447
  await tmpApplyCommand.apply(this.editor);
417
448
  // Show items again
@@ -452,6 +483,8 @@ class Selection {
452
483
  this.originalRegion = bbox;
453
484
  this.selectionTightBoundingBox = bbox;
454
485
  this.selectedElems = objects.filter(object => object.isSelectable());
486
+ // Enforce increasing z-index invariant
487
+ this.selectedElems.sort((a, b) => a.getZIndex() - b.getZIndex());
455
488
  this.padRegion();
456
489
  this.updateUI();
457
490
  }
@@ -465,7 +498,8 @@ _a = Selection;
465
498
  // The selection box is lost when serializing/deserializing. No need to store box rotation
466
499
  const fullTransform = new Mat33(...json.transform);
467
500
  const elemIds = (json.elems ?? []);
468
- return new _a.ApplyTransformationCommand(null, elemIds, fullTransform);
501
+ const deltaZIndex = parseInt(json.deltaZIndex ?? 0);
502
+ return new _a.ApplyTransformationCommand(null, elemIds, fullTransform, deltaZIndex);
469
503
  });
470
504
  })();
471
505
  Selection.ApplyTransformationCommand = class extends SerializableCommand {
@@ -473,10 +507,11 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
473
507
  // If a `string[]`, selectedElems is a list of element IDs.
474
508
  selectedElems,
475
509
  // Full transformation used to transform elements.
476
- fullTransform) {
510
+ fullTransform, deltaZIndex) {
477
511
  super('selection-tool-transform');
478
512
  this.selection = selection;
479
513
  this.fullTransform = fullTransform;
514
+ this.deltaZIndex = deltaZIndex;
480
515
  const isIDList = (arr) => {
481
516
  return typeof arr[0] === 'string';
482
517
  };
@@ -487,11 +522,11 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
487
522
  else {
488
523
  this.selectedElemIds = selectedElems.map(elem => elem.getId());
489
524
  this.transformCommands = selectedElems.map(elem => {
490
- return elem.transformBy(this.fullTransform);
525
+ return elem.setZIndexAndTransformBy(this.fullTransform, elem.getZIndex() + deltaZIndex);
491
526
  });
492
527
  }
493
528
  }
494
- resolveToElems(editor) {
529
+ resolveToElems(editor, isUndoing) {
495
530
  if (this.transformCommands) {
496
531
  return;
497
532
  }
@@ -500,11 +535,19 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
500
535
  if (!elem) {
501
536
  throw new Error(`Unable to find element with ID, ${id}.`);
502
537
  }
503
- return elem.transformBy(this.fullTransform);
538
+ let originalZIndex = elem.getZIndex();
539
+ let targetZIndex = elem.getZIndex() + this.deltaZIndex;
540
+ // If the command has already been applied, the element should currently
541
+ // have the target z-index.
542
+ if (isUndoing) {
543
+ targetZIndex = elem.getZIndex();
544
+ originalZIndex = elem.getZIndex() - this.deltaZIndex;
545
+ }
546
+ return elem.setZIndexAndTransformBy(this.fullTransform, targetZIndex, originalZIndex);
504
547
  });
505
548
  }
506
549
  async apply(editor) {
507
- this.resolveToElems(editor);
550
+ this.resolveToElems(editor, false);
508
551
  this.selection?.setTransform(this.fullTransform, false);
509
552
  this.selection?.updateUI();
510
553
  await editor.asyncApplyCommands(this.transformCommands, updateChunkSize);
@@ -513,7 +556,7 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
513
556
  this.selection?.updateUI();
514
557
  }
515
558
  async unapply(editor) {
516
- this.resolveToElems(editor);
559
+ this.resolveToElems(editor, true);
517
560
  this.selection?.setTransform(this.fullTransform.inverse(), false);
518
561
  this.selection?.updateUI();
519
562
  await editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize, true);
@@ -525,6 +568,7 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
525
568
  return {
526
569
  elems: this.selectedElemIds,
527
570
  transform: this.fullTransform.toArray(),
571
+ deltaZIndex: this.deltaZIndex,
528
572
  };
529
573
  }
530
574
  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;
@@ -5,7 +5,7 @@ import BaseTool from '../BaseTool.mjs';
5
5
  import SVGRenderer from '../../rendering/renderers/SVGRenderer.mjs';
6
6
  import Selection from './Selection.mjs';
7
7
  import TextComponent from '../../components/TextComponent.mjs';
8
- import { duplicateSelectionShortcut, selectAllKeyboardShortcut, snapToGridKeyboardShortcutId } from '../keybindings.mjs';
8
+ import { duplicateSelectionShortcut, selectAllKeyboardShortcut, sendToBackSelectionShortcut, snapToGridKeyboardShortcutId } from '../keybindings.mjs';
9
9
  import ToPointerAutoscroller from './ToPointerAutoscroller.mjs';
10
10
  export const cssPrefix = 'selection-tool-';
11
11
  // Allows users to select/transform portions of the `EditorImage`.
@@ -21,6 +21,9 @@ class SelectionTool extends BaseTool {
21
21
  this.lastPointer = null;
22
22
  this.selectionBoxHandlingEvt = false;
23
23
  this.lastSelectedObjects = [];
24
+ // Whether the last keypress corresponded to an action that didn't transform the
25
+ // selection (and thus does not need to be finalized on onKeyUp).
26
+ this.hasUnfinalizedTransformFromKeyPress = false;
24
27
  this.autoscroller = new ToPointerAutoscroller(editor.viewport, (scrollBy) => {
25
28
  editor.dispatch(Viewport.transformBy(Mat33.translation(scrollBy)), false);
26
29
  // Update the selection box/content to match the new viewport.
@@ -213,7 +216,8 @@ class SelectionTool extends BaseTool {
213
216
  this.snapToGrid = true;
214
217
  return true;
215
218
  }
216
- if (this.selectionBox && shortcucts.matchesShortcut(duplicateSelectionShortcut, event)) {
219
+ if (this.selectionBox && (shortcucts.matchesShortcut(duplicateSelectionShortcut, event)
220
+ || shortcucts.matchesShortcut(sendToBackSelectionShortcut, event))) {
217
221
  // Handle duplication on key up — we don't want to accidentally duplicate
218
222
  // many times.
219
223
  return true;
@@ -227,9 +231,11 @@ class SelectionTool extends BaseTool {
227
231
  // Pass it to another tool, if apliccable.
228
232
  return false;
229
233
  }
230
- else if (event.key === 'Shift') {
234
+ else if (event.shiftKey || event.key === 'Shift') {
231
235
  this.shiftKeyPressed = true;
232
- return true;
236
+ if (event.key === 'Shift') {
237
+ return true;
238
+ }
233
239
  }
234
240
  let rotationSteps = 0;
235
241
  let xTranslateSteps = 0;
@@ -297,6 +303,8 @@ class SelectionTool extends BaseTool {
297
303
  const oldTransform = this.selectionBox.getTransform();
298
304
  this.selectionBox.setTransform(oldTransform.rightMul(transform));
299
305
  this.selectionBox.scrollTo();
306
+ // The transformation needs to be finalized at some point (on key up)
307
+ this.hasUnfinalizedTransformFromKeyPress = true;
300
308
  }
301
309
  if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
302
310
  this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
@@ -322,12 +330,32 @@ class SelectionTool extends BaseTool {
322
330
  });
323
331
  return true;
324
332
  }
333
+ if (this.selectionBox && shortcucts.matchesShortcut(sendToBackSelectionShortcut, evt)) {
334
+ const sendToBackCommand = this.selectionBox.sendToBack();
335
+ if (sendToBackCommand) {
336
+ this.editor.dispatch(sendToBackCommand);
337
+ }
338
+ return true;
339
+ }
340
+ // Here, we check if shiftKey === false because, as of this writing,
341
+ // evt.shiftKey is an optional property. Being falsey could just mean
342
+ // that it wasn't set.
343
+ if (evt.shiftKey === false) {
344
+ this.shiftKeyPressed = false;
345
+ // Don't return immediately -- event may be otherwise handled
346
+ }
347
+ // Also check for key === 'Shift' (for the case where shiftKey is undefined)
325
348
  if (evt.key === 'Shift') {
326
349
  this.shiftKeyPressed = false;
327
350
  return true;
328
351
  }
352
+ // If we don't need to finalize the transform
353
+ if (!this.hasUnfinalizedTransformFromKeyPress) {
354
+ return true;
355
+ }
329
356
  if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
330
357
  this.selectionBox.finalizeTransform();
358
+ this.hasUnfinalizedTransformFromKeyPress = false;
331
359
  return true;
332
360
  }
333
361
  return false;
@@ -361,7 +389,11 @@ class SelectionTool extends BaseTool {
361
389
  return true;
362
390
  }
363
391
  setEnabled(enabled) {
392
+ const wasEnabled = this.isEnabled();
364
393
  super.setEnabled(enabled);
394
+ if (wasEnabled === enabled) {
395
+ return;
396
+ }
365
397
  // Clear the selection
366
398
  this.selectionBox?.cancelSelection();
367
399
  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;
@@ -5,7 +5,6 @@ import BaseTool from './BaseTool.mjs';
5
5
  *
6
6
  * This is in the default set of {@link ToolController} tools.
7
7
  *
8
- * @deprecated This may be replaced in the future.
9
8
  */
10
9
  export default class ToolSwitcherShortcut extends BaseTool {
11
10
  constructor(editor) {
@@ -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";
@@ -39,3 +39,5 @@ export const selectAllKeyboardShortcut = 'jsdraw.tools.SelectionTool.selectAll';
39
39
  KeyboardShortcutManager.registerDefaultKeyboardShortcut(selectAllKeyboardShortcut, ['CtrlOrMeta+KeyA'], 'Select all');
40
40
  export const duplicateSelectionShortcut = 'jsdraw.tools.SelectionTool.duplicateSelection';
41
41
  KeyboardShortcutManager.registerDefaultKeyboardShortcut(duplicateSelectionShortcut, ['CtrlOrMeta+KeyD'], 'Duplicate selection');
42
+ export const sendToBackSelectionShortcut = 'jsdraw.tools.SelectionTool.sendToBack';
43
+ KeyboardShortcutManager.registerDefaultKeyboardShortcut(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;
@@ -9,6 +9,8 @@ export const defaultToolLocalization = {
9
9
  rightClickDragPanTool: 'Right-click drag',
10
10
  pipetteTool: 'Pick color from screen',
11
11
  keyboardPanZoom: 'Keyboard pan/zoom shortcuts',
12
+ autocorrectedTo: (strokeDescription) => `Autocorrected to ${strokeDescription}`,
13
+ autocorrectionCanceled: 'Autocorrect cancelled',
12
14
  textTool: 'Text',
13
15
  enterTextToInsert: 'Text to insert',
14
16
  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.
@@ -65,7 +65,11 @@ const listenForKeyboardEventsFrom = (elem, callbacks) => {
65
65
  handleKeyEvent(htmlEvent);
66
66
  });
67
67
  elem.addEventListener('focusout', (focusEvent) => {
68
- const stillHasFocus = focusEvent.relatedTarget && elem.contains(focusEvent.relatedTarget);
68
+ let stillHasFocus = false;
69
+ if (focusEvent.relatedTarget) {
70
+ const relatedTarget = focusEvent.relatedTarget;
71
+ stillHasFocus = elem.contains(relatedTarget) || callbacks.getHandlesKeyEventsFrom(relatedTarget);
72
+ }
69
73
  if (!stillHasFocus) {
70
74
  for (const event of keysDown) {
71
75
  callbacks.handleKeyUp(new KeyboardEvent('keyup', {
@@ -1,3 +1,3 @@
1
1
  export default {
2
- number: '1.10.0',
2
+ number: '1.11.1',
3
3
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "1.10.0",
3
+ "version": "1.11.1",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -64,11 +64,11 @@
64
64
  "postpack": "ts-node tools/copyREADME.ts revert"
65
65
  },
66
66
  "dependencies": {
67
- "@js-draw/math": "^1.10.0",
68
- "@melloware/coloris": "0.21.0"
67
+ "@js-draw/math": "^1.11.1",
68
+ "@melloware/coloris": "0.22.0"
69
69
  },
70
70
  "devDependencies": {
71
- "@js-draw/build-tool": "^1.7.0",
71
+ "@js-draw/build-tool": "^1.11.1",
72
72
  "@types/jest": "29.5.5",
73
73
  "@types/jsdom": "21.1.3"
74
74
  },
@@ -86,5 +86,5 @@
86
86
  "freehand",
87
87
  "svg"
88
88
  ],
89
- "gitHead": "ccf1d0634e902c731fcd794df11cd001c3a30585"
89
+ "gitHead": "695cfe01116839842668233a14fa858ad4ae0bac"
90
90
  }
@@ -45,7 +45,8 @@
45
45
  }
46
46
 
47
47
  .toolbar-button.disabled {
48
- filter: opacity(0.5) sepia(0.2);
48
+ filter: sepia(0.2);
49
+ opacity: 0.45;
49
50
  cursor: unset;
50
51
  }
51
52
 
@@ -103,7 +104,7 @@
103
104
 
104
105
  .toolbar-root button:disabled {
105
106
  cursor: inherit;
106
- filter: opacity(0.5);
107
+ opacity: 0.5;
107
108
  }
108
109
 
109
110
  .toolbar-root .toolbar-icon {
@@ -29,6 +29,14 @@
29
29
  display: inline-flex;
30
30
  flex-direction: row;
31
31
 
32
+ .coloris_input {
33
+ // Ensure that the region that can be clicked to open the input is roughly
34
+ // the full height of the container.
35
+ // Because the color picker is always shown below or above the input, 5px is
36
+ // subtracted to make the picker better align with the input's container.
37
+ height: calc(100% - 6px);
38
+ }
39
+
32
40
  &.picker-open .clr-field {
33
41
  // Work around what seems to be a Coloris bug -- clicking on the input button
34
42
  // keeps the color picker open (while clicking anywhere else closes the picker).