js-draw 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/bundle.js +2 -2
  2. package/dist/cjs/commands/invertCommand.js +5 -0
  3. package/dist/cjs/components/AbstractComponent.d.ts +8 -0
  4. package/dist/cjs/components/AbstractComponent.js +28 -8
  5. package/dist/cjs/components/builders/types.d.ts +11 -0
  6. package/dist/cjs/toolbar/AbstractToolbar.d.ts +18 -2
  7. package/dist/cjs/toolbar/AbstractToolbar.js +46 -30
  8. package/dist/cjs/toolbar/widgets/BaseWidget.js +1 -1
  9. package/dist/cjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
  10. package/dist/cjs/toolbar/widgets/ExitActionWidget.js +32 -0
  11. package/dist/cjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
  12. package/dist/cjs/toolbar/widgets/HandToolWidget.js +24 -13
  13. package/dist/cjs/toolbar/widgets/InsertImageWidget.js +1 -1
  14. package/dist/cjs/toolbar/widgets/keybindings.d.ts +1 -0
  15. package/dist/cjs/toolbar/widgets/keybindings.js +4 -1
  16. package/dist/cjs/toolbar/widgets/layout/types.d.ts +1 -1
  17. package/dist/cjs/tools/Pen.js +5 -0
  18. package/dist/cjs/tools/SelectionTool/Selection.d.ts +4 -0
  19. package/dist/cjs/tools/SelectionTool/Selection.js +56 -12
  20. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
  21. package/dist/cjs/tools/SelectionTool/SelectionTool.js +19 -1
  22. package/dist/cjs/tools/ToolSwitcherShortcut.d.ts +0 -1
  23. package/dist/cjs/tools/ToolSwitcherShortcut.js +0 -1
  24. package/dist/cjs/tools/keybindings.d.ts +1 -0
  25. package/dist/cjs/tools/keybindings.js +3 -1
  26. package/dist/cjs/version.js +1 -1
  27. package/dist/mjs/commands/invertCommand.mjs +5 -0
  28. package/dist/mjs/components/AbstractComponent.d.ts +8 -0
  29. package/dist/mjs/components/AbstractComponent.mjs +28 -8
  30. package/dist/mjs/components/builders/types.d.ts +11 -0
  31. package/dist/mjs/toolbar/AbstractToolbar.d.ts +18 -2
  32. package/dist/mjs/toolbar/AbstractToolbar.mjs +46 -30
  33. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +1 -1
  34. package/dist/mjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
  35. package/dist/mjs/toolbar/widgets/ExitActionWidget.mjs +27 -0
  36. package/dist/mjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
  37. package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +24 -13
  38. package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +1 -1
  39. package/dist/mjs/toolbar/widgets/keybindings.d.ts +1 -0
  40. package/dist/mjs/toolbar/widgets/keybindings.mjs +3 -0
  41. package/dist/mjs/toolbar/widgets/layout/types.d.ts +1 -1
  42. package/dist/mjs/tools/Pen.mjs +5 -0
  43. package/dist/mjs/tools/SelectionTool/Selection.d.ts +4 -0
  44. package/dist/mjs/tools/SelectionTool/Selection.mjs +56 -12
  45. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
  46. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +20 -2
  47. package/dist/mjs/tools/ToolSwitcherShortcut.d.ts +0 -1
  48. package/dist/mjs/tools/ToolSwitcherShortcut.mjs +0 -1
  49. package/dist/mjs/tools/keybindings.d.ts +1 -0
  50. package/dist/mjs/tools/keybindings.mjs +2 -0
  51. package/dist/mjs/version.mjs +1 -1
  52. package/package.json +2 -2
@@ -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;
@@ -303,6 +307,8 @@ class SelectionTool extends BaseTool_1.default {
303
307
  const oldTransform = this.selectionBox.getTransform();
304
308
  this.selectionBox.setTransform(oldTransform.rightMul(transform));
305
309
  this.selectionBox.scrollTo();
310
+ // The transformation needs to be finalized at some point (on key up)
311
+ this.hasUnfinalizedTransformFromKeyPress = true;
306
312
  }
307
313
  if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
308
314
  this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
@@ -328,12 +334,24 @@ class SelectionTool extends BaseTool_1.default {
328
334
  });
329
335
  return true;
330
336
  }
337
+ if (this.selectionBox && shortcucts.matchesShortcut(keybindings_1.sendToBackSelectionShortcut, evt)) {
338
+ const sendToBackCommand = this.selectionBox.sendToBack();
339
+ if (sendToBackCommand) {
340
+ this.editor.dispatch(sendToBackCommand);
341
+ }
342
+ return true;
343
+ }
331
344
  if (evt.key === 'Shift') {
332
345
  this.shiftKeyPressed = false;
333
346
  return true;
334
347
  }
348
+ // If we don't need to finalize the transform
349
+ if (!this.hasUnfinalizedTransformFromKeyPress) {
350
+ return true;
351
+ }
335
352
  if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
336
353
  this.selectionBox.finalizeTransform();
354
+ this.hasUnfinalizedTransformFromKeyPress = false;
337
355
  return true;
338
356
  }
339
357
  return false;
@@ -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) {
@@ -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');
@@ -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.0',
5
5
  };
@@ -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) {
@@ -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
  }
@@ -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;