js-draw 1.9.1 → 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 (145) hide show
  1. package/dist/Editor.css +48 -1
  2. package/dist/bundle.js +2 -2
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.d.ts +41 -0
  5. package/dist/cjs/Editor.js +9 -0
  6. package/dist/cjs/Pointer.js +1 -1
  7. package/dist/cjs/commands/Erase.d.ts +22 -2
  8. package/dist/cjs/commands/Erase.js +22 -2
  9. package/dist/cjs/commands/invertCommand.js +5 -0
  10. package/dist/cjs/commands/uniteCommands.d.ts +36 -0
  11. package/dist/cjs/commands/uniteCommands.js +36 -0
  12. package/dist/cjs/components/AbstractComponent.d.ts +8 -0
  13. package/dist/cjs/components/AbstractComponent.js +28 -8
  14. package/dist/cjs/components/ImageComponent.d.ts +12 -0
  15. package/dist/cjs/components/ImageComponent.js +16 -9
  16. package/dist/cjs/components/Stroke.d.ts +16 -2
  17. package/dist/cjs/components/Stroke.js +17 -1
  18. package/dist/cjs/components/builders/ArrowBuilder.js +3 -3
  19. package/dist/cjs/components/builders/CircleBuilder.js +3 -3
  20. package/dist/cjs/components/builders/FreehandLineBuilder.js +3 -3
  21. package/dist/cjs/components/builders/LineBuilder.js +3 -3
  22. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +3 -3
  23. package/dist/cjs/components/builders/RectangleBuilder.js +5 -6
  24. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
  25. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +168 -0
  26. package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
  27. package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.js +46 -0
  28. package/dist/cjs/components/builders/types.d.ts +12 -0
  29. package/dist/cjs/image/EditorImage.d.ts +32 -1
  30. package/dist/cjs/image/EditorImage.js +32 -1
  31. package/dist/cjs/rendering/RenderablePathSpec.d.ts +5 -1
  32. package/dist/cjs/rendering/RenderablePathSpec.js +4 -0
  33. package/dist/cjs/toolbar/AbstractToolbar.d.ts +18 -2
  34. package/dist/cjs/toolbar/AbstractToolbar.js +46 -30
  35. package/dist/cjs/toolbar/IconProvider.d.ts +2 -0
  36. package/dist/cjs/toolbar/IconProvider.js +17 -0
  37. package/dist/cjs/toolbar/localization.d.ts +3 -0
  38. package/dist/cjs/toolbar/localization.js +4 -1
  39. package/dist/cjs/toolbar/widgets/BaseWidget.js +1 -1
  40. package/dist/cjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
  41. package/dist/cjs/toolbar/widgets/ExitActionWidget.js +32 -0
  42. package/dist/cjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
  43. package/dist/cjs/toolbar/widgets/HandToolWidget.js +24 -13
  44. package/dist/cjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
  45. package/dist/cjs/toolbar/widgets/InsertImageWidget.js +102 -22
  46. package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
  47. package/dist/cjs/toolbar/widgets/PenToolWidget.js +50 -20
  48. package/dist/cjs/toolbar/widgets/keybindings.d.ts +1 -0
  49. package/dist/cjs/toolbar/widgets/keybindings.js +4 -1
  50. package/dist/cjs/toolbar/widgets/layout/types.d.ts +1 -1
  51. package/dist/cjs/tools/Pen.d.ts +9 -0
  52. package/dist/cjs/tools/Pen.js +82 -3
  53. package/dist/cjs/tools/SelectionTool/Selection.d.ts +4 -0
  54. package/dist/cjs/tools/SelectionTool/Selection.js +56 -12
  55. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
  56. package/dist/cjs/tools/SelectionTool/SelectionTool.js +19 -1
  57. package/dist/cjs/tools/TextTool.js +5 -1
  58. package/dist/cjs/tools/ToolSwitcherShortcut.d.ts +0 -1
  59. package/dist/cjs/tools/ToolSwitcherShortcut.js +0 -1
  60. package/dist/cjs/tools/keybindings.d.ts +1 -0
  61. package/dist/cjs/tools/keybindings.js +3 -1
  62. package/dist/cjs/tools/util/StationaryPenDetector.d.ts +22 -0
  63. package/dist/cjs/tools/util/StationaryPenDetector.js +95 -0
  64. package/dist/cjs/util/ReactiveValue.d.ts +2 -0
  65. package/dist/cjs/util/ReactiveValue.js +2 -0
  66. package/dist/cjs/util/lib.d.ts +1 -0
  67. package/dist/cjs/util/lib.js +4 -1
  68. package/dist/cjs/util/waitForImageLoaded.d.ts +2 -0
  69. package/dist/cjs/util/waitForImageLoaded.js +12 -0
  70. package/dist/cjs/version.js +1 -1
  71. package/dist/mjs/Editor.d.ts +41 -0
  72. package/dist/mjs/Editor.mjs +9 -0
  73. package/dist/mjs/Pointer.mjs +1 -1
  74. package/dist/mjs/commands/Erase.d.ts +22 -2
  75. package/dist/mjs/commands/Erase.mjs +22 -2
  76. package/dist/mjs/commands/invertCommand.mjs +5 -0
  77. package/dist/mjs/commands/uniteCommands.d.ts +36 -0
  78. package/dist/mjs/commands/uniteCommands.mjs +36 -0
  79. package/dist/mjs/components/AbstractComponent.d.ts +8 -0
  80. package/dist/mjs/components/AbstractComponent.mjs +28 -8
  81. package/dist/mjs/components/ImageComponent.d.ts +12 -0
  82. package/dist/mjs/components/ImageComponent.mjs +16 -9
  83. package/dist/mjs/components/Stroke.d.ts +16 -2
  84. package/dist/mjs/components/Stroke.mjs +17 -1
  85. package/dist/mjs/components/builders/ArrowBuilder.mjs +3 -2
  86. package/dist/mjs/components/builders/CircleBuilder.mjs +3 -2
  87. package/dist/mjs/components/builders/FreehandLineBuilder.mjs +3 -2
  88. package/dist/mjs/components/builders/LineBuilder.mjs +3 -2
  89. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +3 -2
  90. package/dist/mjs/components/builders/RectangleBuilder.mjs +5 -4
  91. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
  92. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +166 -0
  93. package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
  94. package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.mjs +44 -0
  95. package/dist/mjs/components/builders/types.d.ts +12 -0
  96. package/dist/mjs/image/EditorImage.d.ts +32 -1
  97. package/dist/mjs/image/EditorImage.mjs +32 -1
  98. package/dist/mjs/rendering/RenderablePathSpec.d.ts +5 -1
  99. package/dist/mjs/rendering/RenderablePathSpec.mjs +4 -0
  100. package/dist/mjs/toolbar/AbstractToolbar.d.ts +18 -2
  101. package/dist/mjs/toolbar/AbstractToolbar.mjs +46 -30
  102. package/dist/mjs/toolbar/IconProvider.d.ts +2 -0
  103. package/dist/mjs/toolbar/IconProvider.mjs +17 -0
  104. package/dist/mjs/toolbar/localization.d.ts +3 -0
  105. package/dist/mjs/toolbar/localization.mjs +4 -1
  106. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +1 -1
  107. package/dist/mjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
  108. package/dist/mjs/toolbar/widgets/ExitActionWidget.mjs +27 -0
  109. package/dist/mjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
  110. package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +24 -13
  111. package/dist/mjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
  112. package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +102 -22
  113. package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
  114. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +50 -20
  115. package/dist/mjs/toolbar/widgets/keybindings.d.ts +1 -0
  116. package/dist/mjs/toolbar/widgets/keybindings.mjs +3 -0
  117. package/dist/mjs/toolbar/widgets/layout/types.d.ts +1 -1
  118. package/dist/mjs/tools/Pen.d.ts +9 -0
  119. package/dist/mjs/tools/Pen.mjs +82 -3
  120. package/dist/mjs/tools/SelectionTool/Selection.d.ts +4 -0
  121. package/dist/mjs/tools/SelectionTool/Selection.mjs +56 -12
  122. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
  123. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +20 -2
  124. package/dist/mjs/tools/TextTool.mjs +5 -1
  125. package/dist/mjs/tools/ToolSwitcherShortcut.d.ts +0 -1
  126. package/dist/mjs/tools/ToolSwitcherShortcut.mjs +0 -1
  127. package/dist/mjs/tools/keybindings.d.ts +1 -0
  128. package/dist/mjs/tools/keybindings.mjs +2 -0
  129. package/dist/mjs/tools/util/StationaryPenDetector.d.ts +22 -0
  130. package/dist/mjs/tools/util/StationaryPenDetector.mjs +92 -0
  131. package/dist/mjs/util/ReactiveValue.d.ts +2 -0
  132. package/dist/mjs/util/ReactiveValue.mjs +2 -0
  133. package/dist/mjs/util/lib.d.ts +1 -0
  134. package/dist/mjs/util/lib.mjs +1 -0
  135. package/dist/mjs/util/waitForImageLoaded.d.ts +2 -0
  136. package/dist/mjs/util/waitForImageLoaded.mjs +10 -0
  137. package/dist/mjs/version.mjs +1 -1
  138. package/package.json +3 -3
  139. package/src/Editor.scss +7 -0
  140. package/src/toolbar/AbstractToolbar.scss +20 -0
  141. package/src/toolbar/toolbar.scss +1 -1
  142. package/src/toolbar/widgets/InsertImageWidget.scss +6 -1
  143. package/src/toolbar/widgets/PenToolWidget.scss +33 -0
  144. package/src/tools/SelectionTool/SelectionTool.scss +6 -0
  145. package/src/toolbar/widgets/PenToolWidget.css +0 -2
@@ -35,6 +35,7 @@ const DocumentPropertiesWidget_1 = __importDefault(require("./widgets/DocumentPr
35
35
  const math_1 = require("@js-draw/math");
36
36
  const constants_1 = require("./constants");
37
37
  const SaveActionWidget_1 = __importDefault(require("./widgets/SaveActionWidget"));
38
+ const ExitActionWidget_1 = __importDefault(require("./widgets/ExitActionWidget"));
38
39
  class AbstractToolbar {
39
40
  /** @internal */
40
41
  constructor(editor, localizationTable = localization_1.defaultToolbarLocalization) {
@@ -297,14 +298,13 @@ class AbstractToolbar {
297
298
  */
298
299
  addSaveButton(saveCallback, labelOverride = {}) {
299
300
  const widget = new SaveActionWidget_1.default(this.editor, this.localizationTable, saveCallback, labelOverride);
300
- widget.setTags([BaseWidget_1.ToolbarWidgetTag.Save]);
301
301
  this.addWidget(widget);
302
302
  return widget;
303
303
  }
304
304
  /**
305
305
  * Adds an "Exit" button that, when clicked, calls `exitCallback`.
306
306
  *
307
- * **Note**: This is roughly equivalent to
307
+ * **Note**: This is *roughly* equivalent to
308
308
  * ```ts
309
309
  * toolbar.addTaggedActionButton([ ToolbarWidgetTag.Exit ], {
310
310
  * label: this.editor.localization.exit,
@@ -321,15 +321,9 @@ class AbstractToolbar {
321
321
  * @final
322
322
  */
323
323
  addExitButton(exitCallback, labelOverride = {}) {
324
- return this.addTaggedActionButton([BaseWidget_1.ToolbarWidgetTag.Exit], {
325
- label: this.editor.localization.exit,
326
- icon: this.editor.icons.makeCloseIcon(),
327
- ...labelOverride,
328
- }, () => {
329
- exitCallback();
330
- }, {
331
- autoDisableInReadOnlyEditors: false,
332
- });
324
+ const widget = new ExitActionWidget_1.default(this.editor, this.localizationTable, exitCallback, labelOverride);
325
+ this.addWidget(widget);
326
+ return widget;
333
327
  }
334
328
  /**
335
329
  * Adds undo and redo buttons that trigger the editor's built-in undo and redo
@@ -377,27 +371,49 @@ class AbstractToolbar {
377
371
  });
378
372
  }
379
373
  /**
380
- * Adds toolbar widgets based on the enabled tools.
374
+ * Adds widgets for pen/eraser/selection/text/pan-zoom primary tools.
375
+ *
376
+ * If `filter` returns `false` for a tool, no widget is added for that tool.
377
+ * See {@link addDefaultToolWidgets}
381
378
  */
382
- addDefaultToolWidgets() {
383
- const toolController = this.editor.toolController;
384
- for (const tool of toolController.getMatchingTools(Pen_1.default)) {
385
- const widget = new PenToolWidget_1.default(this.editor, tool, this.localizationTable);
386
- this.addWidget(widget);
387
- }
388
- for (const tool of toolController.getMatchingTools(Eraser_1.default)) {
389
- this.addWidget(new EraserToolWidget_1.default(this.editor, tool, this.localizationTable));
390
- }
391
- for (const tool of toolController.getMatchingTools(SelectionTool_1.default)) {
392
- this.addWidget(new SelectionToolWidget_1.default(this.editor, tool, this.localizationTable));
393
- }
394
- for (const tool of toolController.getMatchingTools(TextTool_1.default)) {
395
- this.addWidget(new TextToolWidget_1.default(this.editor, tool, this.localizationTable));
396
- }
397
- const panZoomTool = toolController.getMatchingTools(PanZoom_1.default)[0];
398
- if (panZoomTool) {
399
- this.addWidget(new HandToolWidget_1.default(this.editor, panZoomTool, this.localizationTable));
379
+ addWidgetsForPrimaryTools(filter) {
380
+ for (const tool of this.editor.toolController.getPrimaryTools()) {
381
+ if (filter && !filter?.(tool)) {
382
+ continue;
383
+ }
384
+ if (tool instanceof Pen_1.default) {
385
+ const widget = new PenToolWidget_1.default(this.editor, tool, this.localizationTable);
386
+ this.addWidget(widget);
387
+ }
388
+ else if (tool instanceof Eraser_1.default) {
389
+ this.addWidget(new EraserToolWidget_1.default(this.editor, tool, this.localizationTable));
390
+ }
391
+ else if (tool instanceof SelectionTool_1.default) {
392
+ this.addWidget(new SelectionToolWidget_1.default(this.editor, tool, this.localizationTable));
393
+ }
394
+ else if (tool instanceof TextTool_1.default) {
395
+ this.addWidget(new TextToolWidget_1.default(this.editor, tool, this.localizationTable));
396
+ }
397
+ else if (tool instanceof PanZoom_1.default) {
398
+ this.addWidget(new HandToolWidget_1.default(this.editor, tool, this.localizationTable));
399
+ }
400
400
  }
401
+ }
402
+ /**
403
+ * Adds toolbar widgets based on the enabled tools, and additional tool-like
404
+ * buttons (e.g. {@link DocumentPropertiesWidget} and {@link InsertImageWidget}).
405
+ */
406
+ addDefaultToolWidgets() {
407
+ this.addWidgetsForPrimaryTools();
408
+ this.addDefaultEditorControlWidgets();
409
+ }
410
+ /**
411
+ * Adds widgets that don't correspond to tools, but do allow the user to control
412
+ * the editor in some way.
413
+ *
414
+ * By default, this includes {@link DocumentPropertiesWidget} and {@link InsertImageWidget}.
415
+ */
416
+ addDefaultEditorControlWidgets() {
401
417
  this.addWidget(new DocumentPropertiesWidget_1.default(this.editor, this.localizationTable));
402
418
  this.addWidget(new InsertImageWidget_1.default(this.editor, this.localizationTable));
403
419
  }
@@ -51,6 +51,8 @@ export default class IconProvider {
51
51
  makePenIcon(penStyle: PenStyle): IconElemType;
52
52
  makeIconFromFactory(penStyle: PenStyle): IconElemType;
53
53
  makePipetteIcon(color?: Color4): IconElemType;
54
+ makeShapeAutocorrectIcon(): IconElemType;
55
+ makeStrokeSmoothingIcon(): IconElemType;
54
56
  /** Unused. @deprecated */
55
57
  makeFormatSelectionIcon(): IconElemType;
56
58
  makeResizeImageToSelectionIcon(): IconElemType;
@@ -649,6 +649,23 @@ class IconProvider {
649
649
  icon.setAttribute('viewBox', '5 -40 140 140');
650
650
  return icon;
651
651
  }
652
+ makeShapeAutocorrectIcon() {
653
+ const fill = 'none';
654
+ const strokeColor = 'var(--icon-color)';
655
+ return this.makeIconFromPath(`
656
+ m 79.129476,33.847107 9.967823,-0.03218 v 55 h -55 l 0.03218,-9.96782
657
+ M 71.1,40.8 a 30,30 0 0 1 -30,30 30,30 0 0 1 -30,-30 30,30 0 0 1 30,-30 30,30 0 0 1 30,30 L 71.1,40.8
658
+ M 34.1,58.8 v -25 h 25 v 0
659
+ `, fill, strokeColor, '7px');
660
+ }
661
+ makeStrokeSmoothingIcon() {
662
+ const fill = 'none';
663
+ const strokeColor = 'var(--icon-color)';
664
+ return this.makeIconFromPath(`
665
+ m 31,83.2 c -50,0 30,-65 -20,-65
666
+ M 75,17.3 40,59.7 38.2,77.6 55.5,72.4 90.5,30 Z
667
+ `, fill, strokeColor, '7px');
668
+ }
652
669
  /** Unused. @deprecated */
653
670
  makeFormatSelectionIcon() {
654
671
  return this.makeIconFromPath(`
@@ -10,6 +10,8 @@ export interface ToolbarLocalization {
10
10
  arrowPen: string;
11
11
  image: string;
12
12
  inputAltText: string;
13
+ decreaseImageSize: string;
14
+ resetImage: string;
13
15
  chooseFile: string;
14
16
  dragAndDropHereOrBrowse: string;
15
17
  cancel: string;
@@ -48,6 +50,7 @@ export interface ToolbarLocalization {
48
50
  toggleOverflow: string;
49
51
  about: string;
50
52
  inputStabilization: string;
53
+ strokeAutocorrect: string;
51
54
  errorImageHasZeroSize: string;
52
55
  closeSidebar: (toolName: string) => string;
53
56
  dropdownShown: (toolName: string) => string;
@@ -10,6 +10,8 @@ exports.defaultToolbarLocalization = {
10
10
  image: 'Image',
11
11
  reformatSelection: 'Format selection',
12
12
  inputAltText: 'Alt text',
13
+ decreaseImageSize: 'Decrease size',
14
+ resetImage: 'Reset',
13
15
  chooseFile: 'Choose file',
14
16
  dragAndDropHereOrBrowse: 'Drag and drop here\nor\n{{browse}}',
15
17
  submit: 'Submit',
@@ -40,7 +42,8 @@ exports.defaultToolbarLocalization = {
40
42
  enableAutoresizeOption: 'Auto-resize',
41
43
  toggleOverflow: 'More',
42
44
  about: 'About',
43
- inputStabilization: 'Input stabilization',
45
+ inputStabilization: 'Stabilization',
46
+ strokeAutocorrect: 'Autocorrect',
44
47
  touchPanning: 'Touchscreen panning',
45
48
  roundedTipPen: 'Round',
46
49
  flatTipPen: 'Flat',
@@ -53,7 +53,7 @@ class BaseWidget {
53
53
  this.layoutManager = defaultLayoutManager;
54
54
  this.icon = null;
55
55
  this.container = document.createElement('div');
56
- this.container.classList.add(`${constants_1.toolbarCSSPrefix}toolContainer`, `${constants_1.toolbarCSSPrefix}toolButtonContainer`);
56
+ this.container.classList.add(`${constants_1.toolbarCSSPrefix}toolContainer`, `${constants_1.toolbarCSSPrefix}toolButtonContainer`, `${constants_1.toolbarCSSPrefix}internalWidgetId--${id.replace(/[^a-zA-Z0-9_]/g, '-')}`);
57
57
  this.dropdownContent = document.createElement('div');
58
58
  __classPrivateFieldSet(this, _BaseWidget_hasDropdown, false, "f");
59
59
  this.button = document.createElement('div');
@@ -0,0 +1,12 @@
1
+ import { KeyPressEvent } from '../../inputEvents';
2
+ import Editor from '../../Editor';
3
+ import { ToolbarLocalization } from '../localization';
4
+ import ActionButtonWidget from './ActionButtonWidget';
5
+ import { ActionButtonIcon } from '../types';
6
+ declare class ExitActionWidget extends ActionButtonWidget {
7
+ constructor(editor: Editor, localization: ToolbarLocalization, saveCallback: () => void, labelOverride?: Partial<ActionButtonIcon>);
8
+ protected shouldAutoDisableInReadOnlyEditor(): boolean;
9
+ protected onKeyPress(event: KeyPressEvent): boolean;
10
+ mustBeInToplevelMenu(): boolean;
11
+ }
12
+ export default ExitActionWidget;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const ActionButtonWidget_1 = __importDefault(require("./ActionButtonWidget"));
7
+ const BaseWidget_1 = require("./BaseWidget");
8
+ const keybindings_1 = require("./keybindings");
9
+ class ExitActionWidget extends ActionButtonWidget_1.default {
10
+ constructor(editor, localization, saveCallback, labelOverride = {}) {
11
+ super(editor, 'exit-button',
12
+ // Creates an icon
13
+ () => {
14
+ return labelOverride.icon ?? editor.icons.makeCloseIcon();
15
+ }, labelOverride.label ?? localization.exit, saveCallback);
16
+ this.setTags([BaseWidget_1.ToolbarWidgetTag.Exit]);
17
+ }
18
+ shouldAutoDisableInReadOnlyEditor() {
19
+ return false;
20
+ }
21
+ onKeyPress(event) {
22
+ if (this.editor.shortcuts.matchesShortcut(keybindings_1.exitKeyboardShortcut, event)) {
23
+ this.clickAction();
24
+ return true;
25
+ }
26
+ return super.onKeyPress(event);
27
+ }
28
+ mustBeInToplevelMenu() {
29
+ return true;
30
+ }
31
+ }
32
+ exports.default = ExitActionWidget;
@@ -4,11 +4,12 @@ import { ToolbarLocalization } from '../localization';
4
4
  import BaseToolWidget from './BaseToolWidget';
5
5
  import { SavedToolbuttonState } from './BaseWidget';
6
6
  export default class HandToolWidget extends BaseToolWidget {
7
- protected overridePanZoomTool: PanZoom;
8
7
  private allowTogglingBaseTool;
9
- constructor(editor: Editor, overridePanZoomTool: PanZoom, localizationTable: ToolbarLocalization);
10
- protected shouldAutoDisableInReadOnlyEditor(): boolean;
8
+ protected overridePanZoomTool: PanZoom;
9
+ constructor(editor: Editor, tool: PanZoom, localizationTable: ToolbarLocalization);
11
10
  private static getPrimaryHandTool;
11
+ private static getOverrideHandTool;
12
+ protected shouldAutoDisableInReadOnlyEditor(): boolean;
12
13
  protected getTitle(): string;
13
14
  protected createIcon(): Element;
14
15
  protected handleClick(): void;
@@ -124,34 +124,45 @@ class HandModeWidget extends BaseWidget_1.default {
124
124
  }
125
125
  class HandToolWidget extends BaseToolWidget_1.default {
126
126
  constructor(editor,
127
- // Pan zoom tool that overrides all other tools (enabling this tool for a device
128
- // causes that device to pan/zoom instead of interact with the primary tools)
129
- overridePanZoomTool, localizationTable) {
130
- const primaryHandTool = HandToolWidget.getPrimaryHandTool(editor.toolController);
131
- const tool = primaryHandTool ?? overridePanZoomTool;
132
- super(editor, tool, 'hand-tool-widget', localizationTable);
133
- this.overridePanZoomTool = overridePanZoomTool;
127
+ // Can either be the primary pan/zoom tool (in the primary tools list) or
128
+ // the override pan/zoom tool.
129
+ // If the override pan/zoom tool, the primary will be gotten from the editor's
130
+ // tool controller.
131
+ // If the primary, the override will be gotten from the editor's tool controller.
132
+ tool, localizationTable) {
133
+ const isGivenToolPrimary = editor.toolController.getPrimaryTools().includes(tool);
134
+ const primaryTool = (isGivenToolPrimary ? tool : HandToolWidget.getPrimaryHandTool(editor.toolController))
135
+ ?? tool;
136
+ super(editor, primaryTool, 'hand-tool-widget', localizationTable);
137
+ this.overridePanZoomTool =
138
+ (isGivenToolPrimary ? HandToolWidget.getOverrideHandTool(editor.toolController) : tool)
139
+ ?? tool;
134
140
  // Only allow toggling a hand tool if we're using the primary hand tool and not the override
135
141
  // hand tool for this button.
136
- this.allowTogglingBaseTool = primaryHandTool !== null;
142
+ this.allowTogglingBaseTool = primaryTool !== null;
137
143
  // Allow showing/hiding the dropdown, even if `overridePanZoomTool` isn't enabled.
138
144
  if (!this.allowTogglingBaseTool) {
139
145
  this.container.classList.add('dropdownShowable');
140
146
  }
141
147
  // Controls for the overriding hand tool.
142
- const touchPanningWidget = new HandModeWidget(editor, overridePanZoomTool, PanZoom_1.PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning, localizationTable);
143
- const rotationLockWidget = new HandModeWidget(editor, overridePanZoomTool, PanZoom_1.PanZoomMode.RotationLocked, () => this.editor.icons.makeRotationLockIcon(), localizationTable.lockRotation, localizationTable);
148
+ const touchPanningWidget = new HandModeWidget(editor, this.overridePanZoomTool, PanZoom_1.PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning, localizationTable);
149
+ const rotationLockWidget = new HandModeWidget(editor, this.overridePanZoomTool, PanZoom_1.PanZoomMode.RotationLocked, () => this.editor.icons.makeRotationLockIcon(), localizationTable.lockRotation, localizationTable);
144
150
  this.addSubWidget(touchPanningWidget);
145
151
  this.addSubWidget(rotationLockWidget);
146
152
  }
147
- shouldAutoDisableInReadOnlyEditor() {
148
- return false;
149
- }
150
153
  static getPrimaryHandTool(toolController) {
151
154
  const primaryPanZoomToolList = toolController.getPrimaryTools().filter(tool => tool instanceof PanZoom_1.default);
152
155
  const primaryPanZoomTool = primaryPanZoomToolList[0];
153
156
  return primaryPanZoomTool;
154
157
  }
158
+ static getOverrideHandTool(toolController) {
159
+ const panZoomToolList = toolController.getMatchingTools(PanZoom_1.default);
160
+ const panZoomTool = panZoomToolList[0];
161
+ return panZoomTool;
162
+ }
163
+ shouldAutoDisableInReadOnlyEditor() {
164
+ return false;
165
+ }
155
166
  getTitle() {
156
167
  return this.localizationTable.handTool;
157
168
  }
@@ -3,10 +3,10 @@ import { ToolbarLocalization } from '../localization';
3
3
  import BaseWidget from './BaseWidget';
4
4
  export default class InsertImageWidget extends BaseWidget {
5
5
  private imagePreview;
6
+ private image;
6
7
  private selectedFiles;
7
8
  private imageAltTextInput;
8
9
  private statusView;
9
- private imageBase64URL;
10
10
  private submitButton;
11
11
  constructor(editor: Editor, localization?: ToolbarLocalization);
12
12
  protected getTitle(): string;
@@ -15,6 +15,7 @@ export default class InsertImageWidget extends BaseWidget {
15
15
  protected handleClick(): void;
16
16
  private static nextInputId;
17
17
  protected fillDropdown(dropdown: HTMLElement): boolean;
18
+ private onImageDataUpdate;
18
19
  private hideDialog;
19
20
  private updateImageSizeDisplay;
20
21
  private updateInputs;
@@ -14,10 +14,48 @@ const BaseWidget_1 = __importDefault(require("./BaseWidget"));
14
14
  const types_1 = require("../../types");
15
15
  const constants_1 = require("../constants");
16
16
  const makeFileInput_1 = __importDefault(require("./components/makeFileInput"));
17
+ class ImageWrapper {
18
+ constructor(imageBase64Url, preview, onUrlUpdate) {
19
+ this.imageBase64Url = imageBase64Url;
20
+ this.preview = preview;
21
+ this.onUrlUpdate = onUrlUpdate;
22
+ this.originalSrc = imageBase64Url;
23
+ preview.src = imageBase64Url;
24
+ }
25
+ updateImageData(base64DataUrl) {
26
+ this.preview.src = base64DataUrl;
27
+ this.imageBase64Url = base64DataUrl;
28
+ this.onUrlUpdate();
29
+ }
30
+ decreaseSize(resizeFactor = 3 / 4) {
31
+ const canvas = document.createElement('canvas');
32
+ canvas.width = this.preview.naturalWidth * resizeFactor;
33
+ canvas.height = this.preview.naturalHeight * resizeFactor;
34
+ const ctx = canvas.getContext('2d');
35
+ ctx?.drawImage(this.preview, 0, 0, canvas.width, canvas.height);
36
+ // JPEG can be much smaller than PNG for the same image size. Prefer it if
37
+ // the image is already a JPEG.
38
+ const format = this.originalSrc?.startsWith('data:image/jpeg;') ? 'image/jpeg' : 'image/png';
39
+ this.updateImageData(canvas.toDataURL(format));
40
+ }
41
+ reset() {
42
+ this.updateImageData(this.originalSrc);
43
+ }
44
+ isChanged() {
45
+ return this.imageBase64Url !== this.originalSrc;
46
+ }
47
+ getBase64Url() {
48
+ return this.imageBase64Url;
49
+ }
50
+ static fromSrcAndPreview(initialBase64Src, preview, onUrlUpdate) {
51
+ return new ImageWrapper(initialBase64Src, preview, onUrlUpdate);
52
+ }
53
+ }
17
54
  class InsertImageWidget extends BaseWidget_1.default {
18
55
  constructor(editor, localization) {
19
56
  localization ??= editor.localization;
20
57
  super(editor, 'insert-image-widget', localization);
58
+ this.image = null;
21
59
  // Make the dropdown showable
22
60
  this.container.classList.add('dropdownShowable');
23
61
  editor.notifier.on(types_1.EditorEventType.SelectionUpdated, event => {
@@ -51,6 +89,7 @@ class InsertImageWidget extends BaseWidget_1.default {
51
89
  this.statusView = document.createElement('div');
52
90
  const actionButtonRow = document.createElement('div');
53
91
  actionButtonRow.classList.add('action-button-row');
92
+ this.statusView.classList.add('insert-image-image-status-view');
54
93
  this.submitButton = document.createElement('button');
55
94
  this.selectedFiles = selectedFiles;
56
95
  this.imageAltTextInput = document.createElement('input');
@@ -65,9 +104,8 @@ class InsertImageWidget extends BaseWidget_1.default {
65
104
  this.submitButton.innerText = this.localizationTable.submit;
66
105
  this.selectedFiles.onUpdateAndNow(async (files) => {
67
106
  if (files.length === 0) {
68
- this.imagePreview.style.display = 'none';
69
- this.submitButton.disabled = true;
70
- this.submitButton.style.display = 'none';
107
+ this.image = null;
108
+ this.onImageDataUpdate();
71
109
  return;
72
110
  }
73
111
  this.imagePreview.style.display = 'block';
@@ -79,18 +117,13 @@ class InsertImageWidget extends BaseWidget_1.default {
79
117
  catch (e) {
80
118
  this.statusView.innerText = this.localizationTable.imageLoadError(e);
81
119
  }
82
- this.imageBase64URL = data;
83
120
  if (data) {
84
- this.imagePreview.src = data;
85
- this.submitButton.disabled = false;
86
- this.submitButton.style.display = '';
87
- this.updateImageSizeDisplay();
121
+ this.image = ImageWrapper.fromSrcAndPreview(data, this.imagePreview, () => this.onImageDataUpdate());
88
122
  }
89
123
  else {
90
- this.submitButton.disabled = true;
91
- this.submitButton.style.display = 'none';
92
- this.statusView.innerText = '';
124
+ this.image = null;
93
125
  }
126
+ this.onImageDataUpdate();
94
127
  });
95
128
  altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
96
129
  actionButtonRow.replaceChildren(this.submitButton);
@@ -98,11 +131,27 @@ class InsertImageWidget extends BaseWidget_1.default {
98
131
  dropdown.replaceChildren(container);
99
132
  return true;
100
133
  }
134
+ onImageDataUpdate() {
135
+ const base64Data = this.image?.getBase64Url();
136
+ if (base64Data) {
137
+ this.submitButton.disabled = false;
138
+ this.submitButton.style.display = '';
139
+ this.imagePreview.style.display = '';
140
+ this.updateImageSizeDisplay();
141
+ }
142
+ else {
143
+ this.submitButton.disabled = true;
144
+ this.submitButton.style.display = 'none';
145
+ this.statusView.innerText = '';
146
+ this.imagePreview.style.display = 'none';
147
+ this.submitButton.disabled = true;
148
+ }
149
+ }
101
150
  hideDialog() {
102
151
  this.setDropdownVisible(false);
103
152
  }
104
153
  updateImageSizeDisplay() {
105
- const imageData = this.imageBase64URL ?? '';
154
+ const imageData = this.image?.getBase64Url() ?? '';
106
155
  const sizeInKiB = imageData.length / 1024;
107
156
  const sizeInMiB = sizeInKiB / 1024;
108
157
  let units = 'KiB';
@@ -111,7 +160,27 @@ class InsertImageWidget extends BaseWidget_1.default {
111
160
  size = sizeInMiB;
112
161
  units = 'MiB';
113
162
  }
114
- this.statusView.innerText = this.localizationTable.imageSize(Math.round(size), units);
163
+ const sizeText = document.createElement('span');
164
+ sizeText.innerText = this.localizationTable.imageSize(Math.round(size), units);
165
+ // Add a button to allow decreasing the size of large images.
166
+ const decreaseSizeButton = document.createElement('button');
167
+ decreaseSizeButton.innerText = this.localizationTable.decreaseImageSize;
168
+ decreaseSizeButton.onclick = () => {
169
+ this.image?.decreaseSize();
170
+ };
171
+ const resetSizeButton = document.createElement('button');
172
+ resetSizeButton.innerText = this.localizationTable.resetImage;
173
+ resetSizeButton.onclick = () => {
174
+ this.image?.reset();
175
+ };
176
+ this.statusView.replaceChildren(sizeText);
177
+ const largeImageThreshold = 0.12; // MiB
178
+ if (sizeInMiB > largeImageThreshold) {
179
+ this.statusView.appendChild(decreaseSizeButton);
180
+ }
181
+ else if (this.image?.isChanged()) {
182
+ this.statusView.appendChild(resetSizeButton);
183
+ }
115
184
  }
116
185
  updateInputs() {
117
186
  const resetInputs = () => {
@@ -131,11 +200,8 @@ class InsertImageWidget extends BaseWidget_1.default {
131
200
  if (selectedObjects.length === 1 && selectedObjects[0] instanceof ImageComponent_1.default) {
132
201
  editingImage = selectedObjects[0];
133
202
  this.imageAltTextInput.value = editingImage.getAltText() ?? '';
134
- this.imagePreview.style.display = 'block';
135
- this.submitButton.disabled = false;
136
- this.imageBase64URL = editingImage.getURL();
137
- this.imagePreview.src = this.imageBase64URL;
138
- this.updateImageSizeDisplay();
203
+ this.image = ImageWrapper.fromSrcAndPreview(editingImage.getURL(), this.imagePreview, () => this.onImageDataUpdate());
204
+ this.onImageDataUpdate();
139
205
  }
140
206
  else if (selectedObjects.length > 0) {
141
207
  // If not, clear the selection.
@@ -149,13 +215,21 @@ class InsertImageWidget extends BaseWidget_1.default {
149
215
  }
150
216
  };
151
217
  this.submitButton.onclick = async () => {
152
- if (!this.imageBase64URL) {
218
+ if (!this.image) {
153
219
  return;
154
220
  }
155
221
  const image = new Image();
156
- image.src = this.imageBase64URL;
222
+ image.src = this.image.getBase64Url();
157
223
  image.setAttribute('alt', this.imageAltTextInput.value);
158
- const component = await ImageComponent_1.default.fromImage(image, math_1.Mat33.identity);
224
+ let component;
225
+ try {
226
+ component = await ImageComponent_1.default.fromImage(image, math_1.Mat33.identity);
227
+ }
228
+ catch (error) {
229
+ console.error('Error loading image', error);
230
+ this.statusView.innerText = this.localizationTable.imageLoadError(error);
231
+ return;
232
+ }
159
233
  if (component.getBBox().area === 0) {
160
234
  this.statusView.innerText = this.localizationTable.errorImageHasZeroSize;
161
235
  return;
@@ -163,9 +237,15 @@ class InsertImageWidget extends BaseWidget_1.default {
163
237
  this.hideDialog();
164
238
  if (editingImage) {
165
239
  const eraseCommand = new Erase_1.default([editingImage]);
240
+ // Try to preserve the original width
241
+ const originalTransform = editingImage.getTransformation();
242
+ // || 1: Prevent division by zero
243
+ const originalWidth = editingImage.getBBox().width || 1;
244
+ const newWidth = component.getBBox().transformedBoundingBox(originalTransform).width || 1;
245
+ const widthAdjustTransform = math_1.Mat33.scaling2D(originalWidth / newWidth);
166
246
  await this.editor.dispatch((0, uniteCommands_1.default)([
167
247
  EditorImage_1.default.addElement(component),
168
- component.transformBy(editingImage.getTransformation()),
248
+ component.transformBy(originalTransform.rightMul(widthAdjustTransform)),
169
249
  component.setZIndex(editingImage.getZIndex()),
170
250
  eraseCommand,
171
251
  ]));
@@ -24,8 +24,7 @@ export default class PenToolWidget extends BaseToolWidget {
24
24
  private createIconForRecord;
25
25
  protected createIcon(): Element;
26
26
  private createPenTypeSelector;
27
- private setInputStabilizationEnabled;
28
- protected createStabilizationOption(): {
27
+ protected createStrokeCorrectionOptions(): {
29
28
  update: () => void;
30
29
  addTo: (parent: HTMLElement) => void;
31
30
  };