js-draw 1.4.1 → 1.6.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 (63) hide show
  1. package/README.md +10 -1
  2. package/dist/Editor.css +13 -12
  3. package/dist/bundle.js +2 -2
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +8 -0
  6. package/dist/cjs/Editor.js +32 -9
  7. package/dist/cjs/SVGLoader.d.ts +6 -0
  8. package/dist/cjs/SVGLoader.js +100 -56
  9. package/dist/cjs/commands/Erase.d.ts +7 -1
  10. package/dist/cjs/commands/Erase.js +13 -4
  11. package/dist/cjs/commands/invertCommand.js +1 -1
  12. package/dist/cjs/components/Stroke.d.ts +1 -1
  13. package/dist/cjs/components/Stroke.js +1 -1
  14. package/dist/cjs/image/EditorImage.js +1 -1
  15. package/dist/cjs/toolbar/AbstractToolbar.d.ts +13 -4
  16. package/dist/cjs/toolbar/AbstractToolbar.js +18 -5
  17. package/dist/cjs/toolbar/AbstractToolbar.test.d.ts +1 -0
  18. package/dist/cjs/toolbar/EdgeToolbar.js +2 -2
  19. package/dist/cjs/toolbar/widgets/BaseToolWidget.js +4 -13
  20. package/dist/cjs/toolbar/widgets/BaseToolWidget.test.d.ts +1 -0
  21. package/dist/cjs/toolbar/widgets/BaseWidget.d.ts +5 -1
  22. package/dist/cjs/toolbar/widgets/BaseWidget.js +45 -28
  23. package/dist/cjs/toolbar/widgets/BaseWidget.test.d.ts +1 -0
  24. package/dist/cjs/toolbar/widgets/SaveActionWidget.d.ts +2 -1
  25. package/dist/cjs/toolbar/widgets/SaveActionWidget.js +6 -2
  26. package/dist/cjs/tools/BaseTool.js +1 -0
  27. package/dist/cjs/tools/SelectionTool/Selection.js +4 -2
  28. package/dist/cjs/tools/ToolController.d.ts +23 -0
  29. package/dist/cjs/tools/ToolController.js +65 -4
  30. package/dist/cjs/tools/ToolController.test.d.ts +1 -0
  31. package/dist/cjs/version.js +1 -1
  32. package/dist/mjs/Editor.d.ts +8 -0
  33. package/dist/mjs/Editor.mjs +32 -9
  34. package/dist/mjs/SVGLoader.d.ts +6 -0
  35. package/dist/mjs/SVGLoader.mjs +99 -55
  36. package/dist/mjs/commands/Erase.d.ts +7 -1
  37. package/dist/mjs/commands/Erase.mjs +13 -4
  38. package/dist/mjs/commands/invertCommand.mjs +1 -1
  39. package/dist/mjs/components/Stroke.d.ts +1 -1
  40. package/dist/mjs/components/Stroke.mjs +1 -1
  41. package/dist/mjs/image/EditorImage.mjs +1 -1
  42. package/dist/mjs/toolbar/AbstractToolbar.d.ts +13 -4
  43. package/dist/mjs/toolbar/AbstractToolbar.mjs +18 -5
  44. package/dist/mjs/toolbar/AbstractToolbar.test.d.ts +1 -0
  45. package/dist/mjs/toolbar/EdgeToolbar.mjs +2 -2
  46. package/dist/mjs/toolbar/widgets/BaseToolWidget.mjs +4 -13
  47. package/dist/mjs/toolbar/widgets/BaseToolWidget.test.d.ts +1 -0
  48. package/dist/mjs/toolbar/widgets/BaseWidget.d.ts +5 -1
  49. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +45 -28
  50. package/dist/mjs/toolbar/widgets/BaseWidget.test.d.ts +1 -0
  51. package/dist/mjs/toolbar/widgets/SaveActionWidget.d.ts +2 -1
  52. package/dist/mjs/toolbar/widgets/SaveActionWidget.mjs +6 -2
  53. package/dist/mjs/tools/BaseTool.mjs +1 -0
  54. package/dist/mjs/tools/SelectionTool/Selection.mjs +4 -2
  55. package/dist/mjs/tools/ToolController.d.ts +23 -0
  56. package/dist/mjs/tools/ToolController.mjs +65 -4
  57. package/dist/mjs/tools/ToolController.test.d.ts +1 -0
  58. package/dist/mjs/version.mjs +1 -1
  59. package/docs/img/readme-images/logo.svg +1 -0
  60. package/package.json +3 -3
  61. package/src/Editor.scss +4 -2
  62. package/src/toolbar/EdgeToolbar.scss +7 -4
  63. package/src/tools/SelectionTool/SelectionTool.scss +2 -2
@@ -37,7 +37,7 @@ class Stroke extends AbstractComponent_1.default {
37
37
  *
38
38
  * const stroke = new Stroke([
39
39
  * // Fill with red
40
- * path.toRenderable({ fill: Color4.red })
40
+ * pathToRenderable({ fill: Color4.red })
41
41
  * ]);
42
42
  * ```
43
43
  */
@@ -58,7 +58,7 @@ class EditorImage {
58
58
  this.settingExportRect = false;
59
59
  this.root = new RootImageNode();
60
60
  this.background = new RootImageNode();
61
- this.componentsById = {};
61
+ this.componentsById = Object.create(null);
62
62
  this.notifier = new EventDispatcher_1.default();
63
63
  this.importExportViewport = new Viewport_1.default(() => {
64
64
  this.onExportViewportChanged();
@@ -124,24 +124,30 @@ export default abstract class AbstractToolbar {
124
124
  * toolbar.addDefaults();
125
125
  * toolbar.addSaveButton(() => alert('save clicked!'));
126
126
  * ```
127
+ *
128
+ * `labelOverride` can optionally be used to change the `label` or `icon` of the button.
127
129
  */
128
- addSaveButton(saveCallback: () => void): BaseWidget;
130
+ addSaveButton(saveCallback: () => void, labelOverride?: Partial<ActionButtonIcon>): BaseWidget;
129
131
  /**
130
132
  * Adds an "Exit" button that, when clicked, calls `exitCallback`.
131
133
  *
132
- * **Note**: This is equivalent to
134
+ * **Note**: This is roughly equivalent to
133
135
  * ```ts
134
136
  * toolbar.addTaggedActionButton([ ToolbarWidgetTag.Exit ], {
135
137
  * label: this.editor.localization.exit,
136
138
  * icon: this.editor.icons.makeCloseIcon(),
139
+ *
140
+ * // labelOverride can be used to override label or icon.
141
+ * ...labelOverride,
137
142
  * }, () => {
138
143
  * exitCallback();
139
144
  * });
140
145
  * ```
146
+ * with some additional configuration.
141
147
  *
142
148
  * @final
143
149
  */
144
- addExitButton(exitCallback: () => void): BaseWidget;
150
+ addExitButton(exitCallback: () => void, labelOverride?: Partial<ActionButtonIcon>): BaseWidget;
145
151
  /**
146
152
  * Adds undo and redo buttons that trigger the editor's built-in undo and redo
147
153
  * functionality.
@@ -156,7 +162,10 @@ export default abstract class AbstractToolbar {
156
162
  * Adds both the default tool widgets and action buttons.
157
163
  */
158
164
  abstract addDefaults(): void;
159
- /** Remove this toolbar from its container and clean up listeners. */
165
+ /**
166
+ * Remove this toolbar from its container and clean up listeners.
167
+ * This should only be called **once** for a given toolbar.
168
+ */
160
169
  remove(): void;
161
170
  /**
162
171
  * Removes `listener` when {@link remove} is called.
@@ -292,9 +292,11 @@ class AbstractToolbar {
292
292
  * toolbar.addDefaults();
293
293
  * toolbar.addSaveButton(() => alert('save clicked!'));
294
294
  * ```
295
+ *
296
+ * `labelOverride` can optionally be used to change the `label` or `icon` of the button.
295
297
  */
296
- addSaveButton(saveCallback) {
297
- const widget = new SaveActionWidget_1.default(this.editor, this.localizationTable, saveCallback);
298
+ addSaveButton(saveCallback, labelOverride = {}) {
299
+ const widget = new SaveActionWidget_1.default(this.editor, this.localizationTable, saveCallback, labelOverride);
298
300
  widget.setTags([BaseWidget_1.ToolbarWidgetTag.Save]);
299
301
  this.addWidget(widget);
300
302
  return widget;
@@ -302,22 +304,27 @@ class AbstractToolbar {
302
304
  /**
303
305
  * Adds an "Exit" button that, when clicked, calls `exitCallback`.
304
306
  *
305
- * **Note**: This is equivalent to
307
+ * **Note**: This is roughly equivalent to
306
308
  * ```ts
307
309
  * toolbar.addTaggedActionButton([ ToolbarWidgetTag.Exit ], {
308
310
  * label: this.editor.localization.exit,
309
311
  * icon: this.editor.icons.makeCloseIcon(),
312
+ *
313
+ * // labelOverride can be used to override label or icon.
314
+ * ...labelOverride,
310
315
  * }, () => {
311
316
  * exitCallback();
312
317
  * });
313
318
  * ```
319
+ * with some additional configuration.
314
320
  *
315
321
  * @final
316
322
  */
317
- addExitButton(exitCallback) {
323
+ addExitButton(exitCallback, labelOverride = {}) {
318
324
  return this.addTaggedActionButton([BaseWidget_1.ToolbarWidgetTag.Exit], {
319
325
  label: this.editor.localization.exit,
320
326
  icon: this.editor.icons.makeCloseIcon(),
327
+ ...labelOverride,
321
328
  }, () => {
322
329
  exitCallback();
323
330
  }, {
@@ -397,7 +404,10 @@ class AbstractToolbar {
397
404
  addDefaultActionButtons() {
398
405
  this.addUndoRedoButtons();
399
406
  }
400
- /** Remove this toolbar from its container and clean up listeners. */
407
+ /**
408
+ * Remove this toolbar from its container and clean up listeners.
409
+ * This should only be called **once** for a given toolbar.
410
+ */
401
411
  remove() {
402
412
  this.closeColorPickerOverlay?.remove();
403
413
  for (const listener of __classPrivateFieldGet(this, _AbstractToolbar_listeners, "f")) {
@@ -405,6 +415,9 @@ class AbstractToolbar {
405
415
  }
406
416
  __classPrivateFieldSet(this, _AbstractToolbar_listeners, [], "f");
407
417
  this.onRemove();
418
+ for (const widget of __classPrivateFieldGet(this, _AbstractToolbar_widgetList, "f")) {
419
+ widget.remove();
420
+ }
408
421
  }
409
422
  /**
410
423
  * Removes `listener` when {@link remove} is called.
@@ -0,0 +1 @@
1
+ export {};
@@ -235,11 +235,11 @@ class EdgeToolbar extends AbstractToolbar_1.default {
235
235
  widget.removeCSSClassFromContainer('label-right');
236
236
  if (tags.includes(BaseWidget_1.ToolbarWidgetTag.Save)) {
237
237
  widget.addCSSClassToContainer('label-inline');
238
- widget.addCSSClassToContainer('label-right');
238
+ widget.addCSSClassToContainer('label-left');
239
239
  }
240
240
  if (tags.includes(BaseWidget_1.ToolbarWidgetTag.Exit)) {
241
241
  widget.addCSSClassToContainer('label-inline');
242
- widget.addCSSClassToContainer('label-left');
242
+ widget.addCSSClassToContainer('label-right');
243
243
  }
244
244
  }
245
245
  addWidgetInternal(widget) {
@@ -3,7 +3,6 @@ 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
- const types_1 = require("../../types");
7
6
  const BaseWidget_1 = __importDefault(require("./BaseWidget"));
8
7
  const constants_1 = require("../constants");
9
8
  const isToolWidgetFocused = () => {
@@ -14,11 +13,8 @@ class BaseToolWidget extends BaseWidget_1.default {
14
13
  constructor(editor, targetTool, id, localizationTable) {
15
14
  super(editor, id, localizationTable);
16
15
  this.targetTool = targetTool;
17
- editor.notifier.on(types_1.EditorEventType.ToolEnabled, toolEvt => {
18
- if (toolEvt.kind !== types_1.EditorEventType.ToolEnabled) {
19
- throw new Error('Incorrect event type! (Expected ToolEnabled)');
20
- }
21
- if (toolEvt.tool === targetTool) {
16
+ this.targetTool.enabledValue().onUpdateAndNow(enabled => {
17
+ if (enabled) {
22
18
  this.setSelected(true);
23
19
  // Transfer focus to the current button, only if another toolbar button is
24
20
  // focused.
@@ -28,12 +24,7 @@ class BaseToolWidget extends BaseWidget_1.default {
28
24
  this.focus();
29
25
  }
30
26
  }
31
- });
32
- editor.notifier.on(types_1.EditorEventType.ToolDisabled, toolEvt => {
33
- if (toolEvt.kind !== types_1.EditorEventType.ToolDisabled) {
34
- throw new Error('Incorrect event type! (Expected ToolDisabled)');
35
- }
36
- if (toolEvt.tool === targetTool) {
27
+ else {
37
28
  this.setSelected(false);
38
29
  this.setDropdownVisible(false);
39
30
  }
@@ -57,7 +48,7 @@ class BaseToolWidget extends BaseWidget_1.default {
57
48
  }
58
49
  }
59
50
  onKeyPress(event) {
60
- if (this.isSelected() && event.key === ' ' && this.hasDropdown) {
51
+ if (this.isSelected() && event.code === 'Space' && this.hasDropdown) {
61
52
  this.handleClick();
62
53
  return true;
63
54
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -81,13 +81,17 @@ export default abstract class BaseWidget {
81
81
  * @internal
82
82
  */
83
83
  addTo(parent: HTMLElement): HTMLElement;
84
+ /**
85
+ * Remove this. This allows the widget to be added to a toolbar again
86
+ * in the future using {@link addTo}.
87
+ */
88
+ remove(): void;
84
89
  focus(): void;
85
90
  /**
86
91
  * @internal
87
92
  */
88
93
  addCSSClassToContainer(className: string): void;
89
94
  removeCSSClassFromContainer(className: string): void;
90
- remove(): void;
91
95
  protected updateIcon(): void;
92
96
  setDisabled(disabled: boolean): void;
93
97
  setSelected(selected: boolean): void;
@@ -13,7 +13,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
13
13
  var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  return (mod && mod.__esModule) ? mod : { "default": mod };
15
15
  };
16
- var _BaseWidget_hasDropdown, _BaseWidget_disabledDueToReadOnlyEditor, _BaseWidget_tags, _BaseWidget_readOnlyListener;
16
+ var _BaseWidget_instances, _BaseWidget_hasDropdown, _BaseWidget_disabledDueToReadOnlyEditor, _BaseWidget_tags, _BaseWidget_removeEditorListeners, _BaseWidget_addEditorListeners;
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.ToolbarWidgetTag = void 0;
19
19
  const ToolbarShortcutHandler_1 = __importDefault(require("../../tools/ToolbarShortcutHandler"));
@@ -32,6 +32,7 @@ var ToolbarWidgetTag;
32
32
  })(ToolbarWidgetTag || (exports.ToolbarWidgetTag = ToolbarWidgetTag = {}));
33
33
  class BaseWidget {
34
34
  constructor(editor, id, localizationTable) {
35
+ _BaseWidget_instances.add(this);
35
36
  this.editor = editor;
36
37
  this.id = id;
37
38
  this.dropdown = null;
@@ -44,8 +45,7 @@ class BaseWidget {
44
45
  // Maps subWidget IDs to subWidgets.
45
46
  this.subWidgets = {};
46
47
  this.toplevel = true;
47
- // Listens for changes in whether the editor is read-only
48
- _BaseWidget_readOnlyListener.set(this, null);
48
+ _BaseWidget_removeEditorListeners.set(this, null);
49
49
  this.localizationTable = localizationTable ?? editor.localization;
50
50
  // Default layout manager
51
51
  const defaultLayoutManager = new DropdownLayoutManager_1.default((text) => this.editor.announceForAccessibility(text), this.localizationTable);
@@ -66,12 +66,6 @@ class BaseWidget {
66
66
  this.button.oncontextmenu = event => {
67
67
  event.preventDefault();
68
68
  };
69
- const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler_1.default);
70
- // If the onKeyPress function has been extended and the editor is configured to send keypress events to
71
- // toolbar widgets,
72
- if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== BaseWidget.prototype.onKeyPress) {
73
- toolbarShortcutHandlers[0].registerListener(event => this.onKeyPress(event));
74
- }
75
69
  }
76
70
  /**
77
71
  * Should return a constant true or false value. If true (the default),
@@ -274,22 +268,18 @@ class BaseWidget {
274
268
  if (this.container.parentElement) {
275
269
  this.container.remove();
276
270
  }
277
- __classPrivateFieldSet(this, _BaseWidget_readOnlyListener, this.editor.isReadOnlyReactiveValue().onUpdateAndNow(readOnly => {
278
- if (readOnly && this.shouldAutoDisableInReadOnlyEditor() && !this.disabled) {
279
- this.setDisabled(true);
280
- __classPrivateFieldSet(this, _BaseWidget_disabledDueToReadOnlyEditor, true, "f");
281
- if (__classPrivateFieldGet(this, _BaseWidget_hasDropdown, "f")) {
282
- this.dropdown?.requestHide();
283
- }
284
- }
285
- else if (!readOnly && __classPrivateFieldGet(this, _BaseWidget_disabledDueToReadOnlyEditor, "f")) {
286
- __classPrivateFieldSet(this, _BaseWidget_disabledDueToReadOnlyEditor, false, "f");
287
- this.setDisabled(false);
288
- }
289
- }), "f");
271
+ __classPrivateFieldGet(this, _BaseWidget_instances, "m", _BaseWidget_addEditorListeners).call(this);
290
272
  parent.appendChild(this.container);
291
273
  return this.container;
292
274
  }
275
+ /**
276
+ * Remove this. This allows the widget to be added to a toolbar again
277
+ * in the future using {@link addTo}.
278
+ */
279
+ remove() {
280
+ this.container.remove();
281
+ __classPrivateFieldGet(this, _BaseWidget_removeEditorListeners, "f")?.call(this);
282
+ }
293
283
  focus() {
294
284
  this.button.focus();
295
285
  }
@@ -302,11 +292,6 @@ class BaseWidget {
302
292
  removeCSSClassFromContainer(className) {
303
293
  this.container.classList.remove(className);
304
294
  }
305
- remove() {
306
- this.container.remove();
307
- __classPrivateFieldGet(this, _BaseWidget_readOnlyListener, "f")?.remove();
308
- __classPrivateFieldSet(this, _BaseWidget_readOnlyListener, null, "f");
309
- }
310
295
  updateIcon() {
311
296
  let newIcon = this.createIcon();
312
297
  if (!newIcon) {
@@ -443,5 +428,37 @@ class BaseWidget {
443
428
  }
444
429
  }
445
430
  }
446
- _BaseWidget_hasDropdown = new WeakMap(), _BaseWidget_disabledDueToReadOnlyEditor = new WeakMap(), _BaseWidget_tags = new WeakMap(), _BaseWidget_readOnlyListener = new WeakMap();
431
+ _BaseWidget_hasDropdown = new WeakMap(), _BaseWidget_disabledDueToReadOnlyEditor = new WeakMap(), _BaseWidget_tags = new WeakMap(), _BaseWidget_removeEditorListeners = new WeakMap(), _BaseWidget_instances = new WeakSet(), _BaseWidget_addEditorListeners = function _BaseWidget_addEditorListeners() {
432
+ __classPrivateFieldGet(this, _BaseWidget_removeEditorListeners, "f")?.call(this);
433
+ const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler_1.default);
434
+ let removeKeyPressListener = null;
435
+ // If the onKeyPress function has been extended and the editor is configured to send keypress events to
436
+ // toolbar widgets,
437
+ if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== BaseWidget.prototype.onKeyPress) {
438
+ const keyPressListener = (event) => this.onKeyPress(event);
439
+ const handler = toolbarShortcutHandlers[0];
440
+ handler.registerListener(keyPressListener);
441
+ removeKeyPressListener = () => {
442
+ handler.removeListener(keyPressListener);
443
+ };
444
+ }
445
+ const readOnlyListener = this.editor.isReadOnlyReactiveValue().onUpdateAndNow(readOnly => {
446
+ if (readOnly && this.shouldAutoDisableInReadOnlyEditor() && !this.disabled) {
447
+ this.setDisabled(true);
448
+ __classPrivateFieldSet(this, _BaseWidget_disabledDueToReadOnlyEditor, true, "f");
449
+ if (__classPrivateFieldGet(this, _BaseWidget_hasDropdown, "f")) {
450
+ this.dropdown?.requestHide();
451
+ }
452
+ }
453
+ else if (!readOnly && __classPrivateFieldGet(this, _BaseWidget_disabledDueToReadOnlyEditor, "f")) {
454
+ __classPrivateFieldSet(this, _BaseWidget_disabledDueToReadOnlyEditor, false, "f");
455
+ this.setDisabled(false);
456
+ }
457
+ });
458
+ __classPrivateFieldSet(this, _BaseWidget_removeEditorListeners, () => {
459
+ readOnlyListener.remove();
460
+ removeKeyPressListener?.();
461
+ __classPrivateFieldSet(this, _BaseWidget_removeEditorListeners, null, "f");
462
+ }, "f");
463
+ };
447
464
  exports.default = BaseWidget;
@@ -0,0 +1 @@
1
+ export {};
@@ -2,8 +2,9 @@ import { KeyPressEvent } from '../../inputEvents';
2
2
  import Editor from '../../Editor';
3
3
  import { ToolbarLocalization } from '../localization';
4
4
  import ActionButtonWidget from './ActionButtonWidget';
5
+ import { ActionButtonIcon } from '../types';
5
6
  declare class SaveActionWidget extends ActionButtonWidget {
6
- constructor(editor: Editor, localization: ToolbarLocalization, saveCallback: () => void);
7
+ constructor(editor: Editor, localization: ToolbarLocalization, saveCallback: () => void, labelOverride?: Partial<ActionButtonIcon>);
7
8
  protected shouldAutoDisableInReadOnlyEditor(): boolean;
8
9
  protected onKeyPress(event: KeyPressEvent): boolean;
9
10
  mustBeInToplevelMenu(): boolean;
@@ -7,8 +7,12 @@ const ActionButtonWidget_1 = __importDefault(require("./ActionButtonWidget"));
7
7
  const BaseWidget_1 = require("./BaseWidget");
8
8
  const keybindings_1 = require("./keybindings");
9
9
  class SaveActionWidget extends ActionButtonWidget_1.default {
10
- constructor(editor, localization, saveCallback) {
11
- super(editor, 'save-button', editor.icons.makeSaveIcon, localization.save, saveCallback);
10
+ constructor(editor, localization, saveCallback, labelOverride = {}) {
11
+ super(editor, 'save-button',
12
+ // Creates an icon
13
+ () => {
14
+ return labelOverride.icon ?? editor.icons.makeSaveIcon();
15
+ }, labelOverride.label ?? localization.save, saveCallback);
12
16
  this.setTags([BaseWidget_1.ToolbarWidgetTag.Save]);
13
17
  }
14
18
  shouldAutoDisableInReadOnlyEditor() {
@@ -167,6 +167,7 @@ class BaseTool {
167
167
  onDestroy() {
168
168
  __classPrivateFieldGet(this, _BaseTool_readOnlyEditorChangeListener, "f")?.remove();
169
169
  __classPrivateFieldSet(this, _BaseTool_readOnlyEditorChangeListener, null, "f");
170
+ __classPrivateFieldSet(this, _BaseTool_group, null, "f");
170
171
  }
171
172
  }
172
173
  _BaseTool_enabled = new WeakMap(), _BaseTool_group = new WeakMap(), _BaseTool_inputMapper = new WeakMap(), _BaseTool_readOnlyEditorChangeListener = new WeakMap();
@@ -157,8 +157,10 @@ class Selection {
157
157
  // Reset for the next drag
158
158
  this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
159
159
  this.transform = math_1.Mat33.identity;
160
- // Make the commands undo-able
161
- await this.editor.dispatch(new Selection.ApplyTransformationCommand(this, selectedElems, fullTransform));
160
+ // Make the commands undo-able, but only if the transform is non-empty.
161
+ if (!fullTransform.eq(math_1.Mat33.identity)) {
162
+ await this.editor.dispatch(new Selection.ApplyTransformationCommand(this, selectedElems, fullTransform));
163
+ }
162
164
  // Clear renderings of any in-progress transformations
163
165
  const wetInkRenderer = this.editor.display.getWetInkRenderer();
164
166
  wetInkRenderer.clear();
@@ -13,9 +13,32 @@ export default class ToolController implements InputEventListener {
13
13
  /** @internal */
14
14
  constructor(editor: Editor, localization: ToolLocalization);
15
15
  setTools(tools: BaseTool[], primaryToolGroup?: ToolEnabledGroup): void;
16
+ /**
17
+ * Add a tool that acts like one of the primary tools (only one primary tool can be enabled at a time).
18
+ *
19
+ * If the tool is already added to this, the tool is converted to a primary tool.
20
+ *
21
+ * This should be called before creating the app's toolbar.
22
+ */
16
23
  addPrimaryTool(tool: BaseTool): void;
17
24
  getPrimaryTools(): BaseTool[];
18
25
  addTool(tool: BaseTool): void;
26
+ /**
27
+ * Removes **and destroys** all tools in `tools` from this.
28
+ */
29
+ removeAndDestroyTools(tools: BaseTool[]): void;
30
+ private insertTools;
31
+ /**
32
+ * Removes a tool from this' tool list and replaces it with `replaceWith`.
33
+ *
34
+ * If any of `toolsToInsert` have already been added to this, the tools are
35
+ * moved.
36
+ *
37
+ * This should be called before creating the editor's toolbar.
38
+ */
39
+ insertToolsAfter(insertAfter: BaseTool, toolsToInsert: BaseTool[]): void;
40
+ /** @see {@link insertToolsAfter} */
41
+ insertToolsBefore(insertBefore: BaseTool, toolsToInsert: BaseTool[]): void;
19
42
  private onEventInternal;
20
43
  /** Alias for {@link dispatchInputEvent}. */
21
44
  onEvent(event: InputEvt): boolean;
@@ -115,14 +115,21 @@ class ToolController {
115
115
  this.tools = tools;
116
116
  this.primaryToolGroup = primaryToolGroup ?? new ToolEnabledGroup_1.default();
117
117
  }
118
- // Add a tool that acts like one of the primary tools (only one primary tool can be enabled at a time).
119
- // This should be called before creating the app's toolbar.
118
+ /**
119
+ * Add a tool that acts like one of the primary tools (only one primary tool can be enabled at a time).
120
+ *
121
+ * If the tool is already added to this, the tool is converted to a primary tool.
122
+ *
123
+ * This should be called before creating the app's toolbar.
124
+ */
120
125
  addPrimaryTool(tool) {
121
126
  tool.setToolGroup(this.primaryToolGroup);
122
127
  if (tool.isEnabled()) {
123
128
  this.primaryToolGroup.notifyEnabled(tool);
124
129
  }
125
- this.addTool(tool);
130
+ if (!this.tools.includes(tool)) {
131
+ this.addTool(tool);
132
+ }
126
133
  }
127
134
  getPrimaryTools() {
128
135
  return this.tools.filter(tool => {
@@ -131,8 +138,62 @@ class ToolController {
131
138
  }
132
139
  // Add a tool to the end of this' tool list (the added tool receives events after tools already added to this).
133
140
  // This should be called before creating the app's toolbar.
141
+ //
142
+ // A tool should only be added once.
134
143
  addTool(tool) {
135
- this.tools.push(tool);
144
+ // Only add if not already present.
145
+ if (!this.tools.includes(tool)) {
146
+ this.tools.push(tool);
147
+ }
148
+ }
149
+ /**
150
+ * Removes **and destroys** all tools in `tools` from this.
151
+ */
152
+ removeAndDestroyTools(tools) {
153
+ const newTools = [];
154
+ for (const tool of this.tools) {
155
+ if (tools.includes(tool)) {
156
+ if (this.activeTool === tool) {
157
+ this.activeTool = null;
158
+ }
159
+ tool.onDestroy();
160
+ }
161
+ else {
162
+ newTools.push(tool);
163
+ }
164
+ }
165
+ this.tools = newTools;
166
+ }
167
+ insertTools(insertNear, toolsToInsert, mode) {
168
+ this.tools = this.tools.filter(tool => !toolsToInsert.includes(tool));
169
+ const newTools = [];
170
+ for (const tool of this.tools) {
171
+ if (mode === 'after') {
172
+ newTools.push(tool);
173
+ }
174
+ if (tool === insertNear) {
175
+ newTools.push(...toolsToInsert);
176
+ }
177
+ if (mode === 'before') {
178
+ newTools.push(tool);
179
+ }
180
+ }
181
+ this.tools = newTools;
182
+ }
183
+ /**
184
+ * Removes a tool from this' tool list and replaces it with `replaceWith`.
185
+ *
186
+ * If any of `toolsToInsert` have already been added to this, the tools are
187
+ * moved.
188
+ *
189
+ * This should be called before creating the editor's toolbar.
190
+ */
191
+ insertToolsAfter(insertAfter, toolsToInsert) {
192
+ this.insertTools(insertAfter, toolsToInsert, 'after');
193
+ }
194
+ /** @see {@link insertToolsAfter} */
195
+ insertToolsBefore(insertBefore, toolsToInsert) {
196
+ this.insertTools(insertBefore, toolsToInsert, 'before');
136
197
  }
137
198
  // @internal use `dispatchEvent` rather than calling `onEvent` directly.
138
199
  onEventInternal(event) {
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = {
4
- number: '1.4.1',
4
+ number: '1.6.0',
5
5
  };
@@ -64,6 +64,14 @@ export interface EditorSettings {
64
64
  * Additional messages to show in the "about" dialog.
65
65
  */
66
66
  notices: AboutDialogEntry[];
67
+ /**
68
+ * Information about the app/website js-draw is running within
69
+ * to show at the beginning of the about dialog.
70
+ */
71
+ appInfo: {
72
+ name: string;
73
+ version?: string;
74
+ } | null;
67
75
  }
68
76
  /**
69
77
  * The main entrypoint for the full editor.
@@ -106,6 +106,7 @@ export class Editor {
106
106
  keyboardShortcutOverrides: settings.keyboardShortcutOverrides ?? {},
107
107
  iconProvider: settings.iconProvider ?? new IconProvider(),
108
108
  notices: [],
109
+ appInfo: settings.appInfo ? { ...settings.appInfo } : null,
109
110
  };
110
111
  // Validate settings
111
112
  if (this.settings.minZoom > this.settings.maxZoom) {
@@ -644,12 +645,16 @@ export class Editor {
644
645
  htmlEvent.preventDefault();
645
646
  }
646
647
  });
647
- elem.addEventListener('blur', () => {
648
- for (const event of keysDown) {
649
- this.toolController.dispatchInputEvent({
650
- ...event,
651
- kind: InputEvtType.KeyUpEvent,
652
- });
648
+ elem.addEventListener('focusout', (event) => {
649
+ const stillHasFocus = event.relatedTarget && elem.contains(event.relatedTarget);
650
+ if (!stillHasFocus) {
651
+ for (const event of keysDown) {
652
+ this.toolController.dispatchInputEvent({
653
+ ...event,
654
+ kind: InputEvtType.KeyUpEvent,
655
+ });
656
+ }
657
+ keysDown = [];
653
658
  }
654
659
  });
655
660
  // Allow drop.
@@ -1116,16 +1121,34 @@ export class Editor {
1116
1121
  showAboutDialog() {
1117
1122
  const iconLicenseText = this.icons.licenseInfo();
1118
1123
  const notices = [];
1124
+ if (this.settings.appInfo) {
1125
+ const versionLines = [];
1126
+ if (this.settings.appInfo.version) {
1127
+ versionLines.push(`v${this.settings.appInfo.version}`, '');
1128
+ }
1129
+ notices.push({
1130
+ heading: `${this.settings.appInfo.name}`,
1131
+ text: [
1132
+ ...versionLines,
1133
+ `Image editor library: js-draw v${version.number}.`,
1134
+ ].join('\n'),
1135
+ });
1136
+ }
1137
+ else {
1138
+ notices.push({
1139
+ heading: 'js-draw',
1140
+ text: `v${version.number}`,
1141
+ });
1142
+ }
1119
1143
  notices.push({
1120
- heading: 'js-draw',
1144
+ heading: 'Developer information',
1121
1145
  text: [
1122
- `v${version.number}`,
1123
- '',
1124
1146
  'Image debug information (from when this dialog was opened):',
1125
1147
  ` ${this.viewport.getScaleFactor()}x zoom, ${180 / Math.PI * this.viewport.getRotationAngle()} rotation`,
1126
1148
  ` ${this.image.estimateNumElements()} components`,
1127
1149
  ` ${this.getImportExportRect().w}x${this.getImportExportRect().h} size`,
1128
1150
  ].join('\n'),
1151
+ minimized: true,
1129
1152
  });
1130
1153
  notices.push({
1131
1154
  heading: 'Libraries',
@@ -11,9 +11,14 @@ export type SVGLoaderUnknownStyleAttribute = {
11
11
  value: string;
12
12
  priority?: string;
13
13
  };
14
+ export declare enum SVGLoaderLoadMethod {
15
+ IFrame = "iframe",
16
+ DOMParser = "domparser"
17
+ }
14
18
  export interface SVGLoaderOptions {
15
19
  sanitize?: boolean;
16
20
  disableUnknownObjectWarnings?: boolean;
21
+ loadMethod?: 'iframe' | 'domparser';
17
22
  }
18
23
  export default class SVGLoader implements ImageLoader {
19
24
  private source;
@@ -32,6 +37,7 @@ export default class SVGLoader implements ImageLoader {
32
37
  private attachUnrecognisedAttrs;
33
38
  private addPath;
34
39
  private addBackground;
40
+ private getComputedStyle;
35
41
  private getTransform;
36
42
  private makeText;
37
43
  private addText;