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
@@ -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;
@@ -644,6 +644,23 @@ class IconProvider {
644
644
  icon.setAttribute('viewBox', '5 -40 140 140');
645
645
  return icon;
646
646
  }
647
+ makeShapeAutocorrectIcon() {
648
+ const fill = 'none';
649
+ const strokeColor = 'var(--icon-color)';
650
+ return this.makeIconFromPath(`
651
+ m 79.129476,33.847107 9.967823,-0.03218 v 55 h -55 l 0.03218,-9.96782
652
+ 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
653
+ M 34.1,58.8 v -25 h 25 v 0
654
+ `, fill, strokeColor, '7px');
655
+ }
656
+ makeStrokeSmoothingIcon() {
657
+ const fill = 'none';
658
+ const strokeColor = 'var(--icon-color)';
659
+ return this.makeIconFromPath(`
660
+ m 31,83.2 c -50,0 30,-65 -20,-65
661
+ M 75,17.3 40,59.7 38.2,77.6 55.5,72.4 90.5,30 Z
662
+ `, fill, strokeColor, '7px');
663
+ }
647
664
  /** Unused. @deprecated */
648
665
  makeFormatSelectionIcon() {
649
666
  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;
@@ -7,6 +7,8 @@ export const defaultToolbarLocalization = {
7
7
  image: 'Image',
8
8
  reformatSelection: 'Format selection',
9
9
  inputAltText: 'Alt text',
10
+ decreaseImageSize: 'Decrease size',
11
+ resetImage: 'Reset',
10
12
  chooseFile: 'Choose file',
11
13
  dragAndDropHereOrBrowse: 'Drag and drop here\nor\n{{browse}}',
12
14
  submit: 'Submit',
@@ -37,7 +39,8 @@ export const defaultToolbarLocalization = {
37
39
  enableAutoresizeOption: 'Auto-resize',
38
40
  toggleOverflow: 'More',
39
41
  about: 'About',
40
- inputStabilization: 'Input stabilization',
42
+ inputStabilization: 'Stabilization',
43
+ strokeAutocorrect: 'Autocorrect',
41
44
  touchPanning: 'Touchscreen panning',
42
45
  roundedTipPen: 'Round',
43
46
  flatTipPen: 'Flat',
@@ -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;
@@ -96,34 +96,45 @@ class HandModeWidget extends BaseWidget {
96
96
  }
97
97
  export default class HandToolWidget extends BaseToolWidget {
98
98
  constructor(editor,
99
- // Pan zoom tool that overrides all other tools (enabling this tool for a device
100
- // causes that device to pan/zoom instead of interact with the primary tools)
101
- overridePanZoomTool, localizationTable) {
102
- const primaryHandTool = HandToolWidget.getPrimaryHandTool(editor.toolController);
103
- const tool = primaryHandTool ?? overridePanZoomTool;
104
- super(editor, tool, 'hand-tool-widget', localizationTable);
105
- this.overridePanZoomTool = overridePanZoomTool;
99
+ // Can either be the primary pan/zoom tool (in the primary tools list) or
100
+ // the override pan/zoom tool.
101
+ // If the override pan/zoom tool, the primary will be gotten from the editor's
102
+ // tool controller.
103
+ // If the primary, the override will be gotten from the editor's tool controller.
104
+ tool, localizationTable) {
105
+ const isGivenToolPrimary = editor.toolController.getPrimaryTools().includes(tool);
106
+ const primaryTool = (isGivenToolPrimary ? tool : HandToolWidget.getPrimaryHandTool(editor.toolController))
107
+ ?? tool;
108
+ super(editor, primaryTool, 'hand-tool-widget', localizationTable);
109
+ this.overridePanZoomTool =
110
+ (isGivenToolPrimary ? HandToolWidget.getOverrideHandTool(editor.toolController) : tool)
111
+ ?? tool;
106
112
  // Only allow toggling a hand tool if we're using the primary hand tool and not the override
107
113
  // hand tool for this button.
108
- this.allowTogglingBaseTool = primaryHandTool !== null;
114
+ this.allowTogglingBaseTool = primaryTool !== null;
109
115
  // Allow showing/hiding the dropdown, even if `overridePanZoomTool` isn't enabled.
110
116
  if (!this.allowTogglingBaseTool) {
111
117
  this.container.classList.add('dropdownShowable');
112
118
  }
113
119
  // Controls for the overriding hand tool.
114
- const touchPanningWidget = new HandModeWidget(editor, overridePanZoomTool, PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning, localizationTable);
115
- const rotationLockWidget = new HandModeWidget(editor, overridePanZoomTool, PanZoomMode.RotationLocked, () => this.editor.icons.makeRotationLockIcon(), localizationTable.lockRotation, localizationTable);
120
+ const touchPanningWidget = new HandModeWidget(editor, this.overridePanZoomTool, PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning, localizationTable);
121
+ const rotationLockWidget = new HandModeWidget(editor, this.overridePanZoomTool, PanZoomMode.RotationLocked, () => this.editor.icons.makeRotationLockIcon(), localizationTable.lockRotation, localizationTable);
116
122
  this.addSubWidget(touchPanningWidget);
117
123
  this.addSubWidget(rotationLockWidget);
118
124
  }
119
- shouldAutoDisableInReadOnlyEditor() {
120
- return false;
121
- }
122
125
  static getPrimaryHandTool(toolController) {
123
126
  const primaryPanZoomToolList = toolController.getPrimaryTools().filter(tool => tool instanceof PanZoom);
124
127
  const primaryPanZoomTool = primaryPanZoomToolList[0];
125
128
  return primaryPanZoomTool;
126
129
  }
130
+ static getOverrideHandTool(toolController) {
131
+ const panZoomToolList = toolController.getMatchingTools(PanZoom);
132
+ const panZoomTool = panZoomToolList[0];
133
+ return panZoomTool;
134
+ }
135
+ shouldAutoDisableInReadOnlyEditor() {
136
+ return false;
137
+ }
127
138
  getTitle() {
128
139
  return this.localizationTable.handTool;
129
140
  }
@@ -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;
@@ -9,10 +9,48 @@ import BaseWidget from './BaseWidget.mjs';
9
9
  import { EditorEventType } from '../../types.mjs';
10
10
  import { toolbarCSSPrefix } from '../constants.mjs';
11
11
  import makeFileInput from './components/makeFileInput.mjs';
12
+ class ImageWrapper {
13
+ constructor(imageBase64Url, preview, onUrlUpdate) {
14
+ this.imageBase64Url = imageBase64Url;
15
+ this.preview = preview;
16
+ this.onUrlUpdate = onUrlUpdate;
17
+ this.originalSrc = imageBase64Url;
18
+ preview.src = imageBase64Url;
19
+ }
20
+ updateImageData(base64DataUrl) {
21
+ this.preview.src = base64DataUrl;
22
+ this.imageBase64Url = base64DataUrl;
23
+ this.onUrlUpdate();
24
+ }
25
+ decreaseSize(resizeFactor = 3 / 4) {
26
+ const canvas = document.createElement('canvas');
27
+ canvas.width = this.preview.naturalWidth * resizeFactor;
28
+ canvas.height = this.preview.naturalHeight * resizeFactor;
29
+ const ctx = canvas.getContext('2d');
30
+ ctx?.drawImage(this.preview, 0, 0, canvas.width, canvas.height);
31
+ // JPEG can be much smaller than PNG for the same image size. Prefer it if
32
+ // the image is already a JPEG.
33
+ const format = this.originalSrc?.startsWith('data:image/jpeg;') ? 'image/jpeg' : 'image/png';
34
+ this.updateImageData(canvas.toDataURL(format));
35
+ }
36
+ reset() {
37
+ this.updateImageData(this.originalSrc);
38
+ }
39
+ isChanged() {
40
+ return this.imageBase64Url !== this.originalSrc;
41
+ }
42
+ getBase64Url() {
43
+ return this.imageBase64Url;
44
+ }
45
+ static fromSrcAndPreview(initialBase64Src, preview, onUrlUpdate) {
46
+ return new ImageWrapper(initialBase64Src, preview, onUrlUpdate);
47
+ }
48
+ }
12
49
  class InsertImageWidget extends BaseWidget {
13
50
  constructor(editor, localization) {
14
51
  localization ??= editor.localization;
15
52
  super(editor, 'insert-image-widget', localization);
53
+ this.image = null;
16
54
  // Make the dropdown showable
17
55
  this.container.classList.add('dropdownShowable');
18
56
  editor.notifier.on(EditorEventType.SelectionUpdated, event => {
@@ -46,6 +84,7 @@ class InsertImageWidget extends BaseWidget {
46
84
  this.statusView = document.createElement('div');
47
85
  const actionButtonRow = document.createElement('div');
48
86
  actionButtonRow.classList.add('action-button-row');
87
+ this.statusView.classList.add('insert-image-image-status-view');
49
88
  this.submitButton = document.createElement('button');
50
89
  this.selectedFiles = selectedFiles;
51
90
  this.imageAltTextInput = document.createElement('input');
@@ -60,9 +99,8 @@ class InsertImageWidget extends BaseWidget {
60
99
  this.submitButton.innerText = this.localizationTable.submit;
61
100
  this.selectedFiles.onUpdateAndNow(async (files) => {
62
101
  if (files.length === 0) {
63
- this.imagePreview.style.display = 'none';
64
- this.submitButton.disabled = true;
65
- this.submitButton.style.display = 'none';
102
+ this.image = null;
103
+ this.onImageDataUpdate();
66
104
  return;
67
105
  }
68
106
  this.imagePreview.style.display = 'block';
@@ -74,18 +112,13 @@ class InsertImageWidget extends BaseWidget {
74
112
  catch (e) {
75
113
  this.statusView.innerText = this.localizationTable.imageLoadError(e);
76
114
  }
77
- this.imageBase64URL = data;
78
115
  if (data) {
79
- this.imagePreview.src = data;
80
- this.submitButton.disabled = false;
81
- this.submitButton.style.display = '';
82
- this.updateImageSizeDisplay();
116
+ this.image = ImageWrapper.fromSrcAndPreview(data, this.imagePreview, () => this.onImageDataUpdate());
83
117
  }
84
118
  else {
85
- this.submitButton.disabled = true;
86
- this.submitButton.style.display = 'none';
87
- this.statusView.innerText = '';
119
+ this.image = null;
88
120
  }
121
+ this.onImageDataUpdate();
89
122
  });
90
123
  altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
91
124
  actionButtonRow.replaceChildren(this.submitButton);
@@ -93,11 +126,27 @@ class InsertImageWidget extends BaseWidget {
93
126
  dropdown.replaceChildren(container);
94
127
  return true;
95
128
  }
129
+ onImageDataUpdate() {
130
+ const base64Data = this.image?.getBase64Url();
131
+ if (base64Data) {
132
+ this.submitButton.disabled = false;
133
+ this.submitButton.style.display = '';
134
+ this.imagePreview.style.display = '';
135
+ this.updateImageSizeDisplay();
136
+ }
137
+ else {
138
+ this.submitButton.disabled = true;
139
+ this.submitButton.style.display = 'none';
140
+ this.statusView.innerText = '';
141
+ this.imagePreview.style.display = 'none';
142
+ this.submitButton.disabled = true;
143
+ }
144
+ }
96
145
  hideDialog() {
97
146
  this.setDropdownVisible(false);
98
147
  }
99
148
  updateImageSizeDisplay() {
100
- const imageData = this.imageBase64URL ?? '';
149
+ const imageData = this.image?.getBase64Url() ?? '';
101
150
  const sizeInKiB = imageData.length / 1024;
102
151
  const sizeInMiB = sizeInKiB / 1024;
103
152
  let units = 'KiB';
@@ -106,7 +155,27 @@ class InsertImageWidget extends BaseWidget {
106
155
  size = sizeInMiB;
107
156
  units = 'MiB';
108
157
  }
109
- this.statusView.innerText = this.localizationTable.imageSize(Math.round(size), units);
158
+ const sizeText = document.createElement('span');
159
+ sizeText.innerText = this.localizationTable.imageSize(Math.round(size), units);
160
+ // Add a button to allow decreasing the size of large images.
161
+ const decreaseSizeButton = document.createElement('button');
162
+ decreaseSizeButton.innerText = this.localizationTable.decreaseImageSize;
163
+ decreaseSizeButton.onclick = () => {
164
+ this.image?.decreaseSize();
165
+ };
166
+ const resetSizeButton = document.createElement('button');
167
+ resetSizeButton.innerText = this.localizationTable.resetImage;
168
+ resetSizeButton.onclick = () => {
169
+ this.image?.reset();
170
+ };
171
+ this.statusView.replaceChildren(sizeText);
172
+ const largeImageThreshold = 0.12; // MiB
173
+ if (sizeInMiB > largeImageThreshold) {
174
+ this.statusView.appendChild(decreaseSizeButton);
175
+ }
176
+ else if (this.image?.isChanged()) {
177
+ this.statusView.appendChild(resetSizeButton);
178
+ }
110
179
  }
111
180
  updateInputs() {
112
181
  const resetInputs = () => {
@@ -126,11 +195,8 @@ class InsertImageWidget extends BaseWidget {
126
195
  if (selectedObjects.length === 1 && selectedObjects[0] instanceof ImageComponent) {
127
196
  editingImage = selectedObjects[0];
128
197
  this.imageAltTextInput.value = editingImage.getAltText() ?? '';
129
- this.imagePreview.style.display = 'block';
130
- this.submitButton.disabled = false;
131
- this.imageBase64URL = editingImage.getURL();
132
- this.imagePreview.src = this.imageBase64URL;
133
- this.updateImageSizeDisplay();
198
+ this.image = ImageWrapper.fromSrcAndPreview(editingImage.getURL(), this.imagePreview, () => this.onImageDataUpdate());
199
+ this.onImageDataUpdate();
134
200
  }
135
201
  else if (selectedObjects.length > 0) {
136
202
  // If not, clear the selection.
@@ -144,13 +210,21 @@ class InsertImageWidget extends BaseWidget {
144
210
  }
145
211
  };
146
212
  this.submitButton.onclick = async () => {
147
- if (!this.imageBase64URL) {
213
+ if (!this.image) {
148
214
  return;
149
215
  }
150
216
  const image = new Image();
151
- image.src = this.imageBase64URL;
217
+ image.src = this.image.getBase64Url();
152
218
  image.setAttribute('alt', this.imageAltTextInput.value);
153
- const component = await ImageComponent.fromImage(image, Mat33.identity);
219
+ let component;
220
+ try {
221
+ component = await ImageComponent.fromImage(image, Mat33.identity);
222
+ }
223
+ catch (error) {
224
+ console.error('Error loading image', error);
225
+ this.statusView.innerText = this.localizationTable.imageLoadError(error);
226
+ return;
227
+ }
154
228
  if (component.getBBox().area === 0) {
155
229
  this.statusView.innerText = this.localizationTable.errorImageHasZeroSize;
156
230
  return;
@@ -158,9 +232,15 @@ class InsertImageWidget extends BaseWidget {
158
232
  this.hideDialog();
159
233
  if (editingImage) {
160
234
  const eraseCommand = new Erase([editingImage]);
235
+ // Try to preserve the original width
236
+ const originalTransform = editingImage.getTransformation();
237
+ // || 1: Prevent division by zero
238
+ const originalWidth = editingImage.getBBox().width || 1;
239
+ const newWidth = component.getBBox().transformedBoundingBox(originalTransform).width || 1;
240
+ const widthAdjustTransform = Mat33.scaling2D(originalWidth / newWidth);
161
241
  await this.editor.dispatch(uniteCommands([
162
242
  EditorImage.addElement(component),
163
- component.transformBy(editingImage.getTransformation()),
243
+ component.transformBy(originalTransform.rightMul(widthAdjustTransform)),
164
244
  component.setZIndex(editingImage.getZIndex()),
165
245
  eraseCommand,
166
246
  ]));
@@ -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
  };
@@ -147,27 +147,51 @@ class PenToolWidget extends BaseToolWidget {
147
147
  },
148
148
  };
149
149
  }
150
- setInputStabilizationEnabled(enabled) {
151
- this.tool.setHasStabilization(enabled);
152
- }
153
- createStabilizationOption() {
154
- const stabilizationOption = document.createElement('div');
155
- const stabilizationCheckbox = document.createElement('input');
156
- const stabilizationLabel = document.createElement('label');
157
- stabilizationLabel.innerText = this.localizationTable.inputStabilization;
158
- stabilizationCheckbox.type = 'checkbox';
159
- stabilizationCheckbox.id = `${toolbarCSSPrefix}-penInputStabilizationCheckbox-${PenToolWidget.idCounter++}`;
160
- stabilizationLabel.htmlFor = stabilizationCheckbox.id;
161
- stabilizationOption.replaceChildren(stabilizationLabel, stabilizationCheckbox);
162
- stabilizationCheckbox.oninput = () => {
163
- this.setInputStabilizationEnabled(stabilizationCheckbox.checked);
150
+ createStrokeCorrectionOptions() {
151
+ const container = document.createElement('div');
152
+ container.classList.add('action-button-row', `${toolbarCSSPrefix}-pen-tool-toggle-buttons`);
153
+ const addToggleButton = (labelText, icon) => {
154
+ const button = document.createElement('button');
155
+ button.classList.add(`${toolbarCSSPrefix}-toggle-button`);
156
+ const iconElement = icon.cloneNode(true);
157
+ iconElement.classList.add('icon');
158
+ const label = document.createElement('span');
159
+ label.innerText = labelText;
160
+ button.replaceChildren(iconElement, label);
161
+ button.setAttribute('role', 'switch');
162
+ container.appendChild(button);
163
+ let checked = false;
164
+ let onChangeListener = (_checked) => { };
165
+ const result = {
166
+ setChecked(newChecked) {
167
+ checked = newChecked;
168
+ button.setAttribute('aria-checked', `${checked}`);
169
+ onChangeListener(checked);
170
+ },
171
+ setOnInputListener(listener) {
172
+ onChangeListener = listener;
173
+ },
174
+ };
175
+ button.onclick = () => {
176
+ result.setChecked(!checked);
177
+ };
178
+ return result;
164
179
  };
180
+ const stabilizationOption = addToggleButton(this.localizationTable.inputStabilization, this.editor.icons.makeStrokeSmoothingIcon());
181
+ stabilizationOption.setOnInputListener(enabled => {
182
+ this.tool.setHasStabilization(enabled);
183
+ });
184
+ const autocorrectOption = addToggleButton(this.localizationTable.strokeAutocorrect, this.editor.icons.makeShapeAutocorrectIcon());
185
+ autocorrectOption.setOnInputListener(enabled => {
186
+ this.tool.setStrokeAutocorrectEnabled(enabled);
187
+ });
165
188
  return {
166
189
  update: () => {
167
- stabilizationCheckbox.checked = !!this.tool.getInputMapper();
190
+ stabilizationOption.setChecked(!!this.tool.getInputMapper());
191
+ autocorrectOption.setChecked(this.tool.getStrokeAutocorrectionEnabled());
168
192
  },
169
193
  addTo: (parent) => {
170
- parent.appendChild(stabilizationOption);
194
+ parent.appendChild(container);
171
195
  }
172
196
  };
173
197
  }
@@ -189,20 +213,22 @@ class PenToolWidget extends BaseToolWidget {
189
213
  colorLabel.setAttribute('for', colorInput.id);
190
214
  colorRow.appendChild(colorLabel);
191
215
  colorRow.appendChild(colorInputContainer);
192
- const stabilizationOption = this.createStabilizationOption();
216
+ const toggleButtonRow = this.createStrokeCorrectionOptions();
193
217
  this.updateInputs = () => {
194
218
  setColorInputValue(this.tool.getColor());
195
219
  setThickness(this.tool.getThickness());
196
220
  penTypeSelect.updateIcons();
197
221
  // Update the selected stroke factory.
198
222
  penTypeSelect.setValue(this.getCurrentPenTypeIdx());
199
- stabilizationOption.update();
223
+ toggleButtonRow.update();
200
224
  };
201
225
  this.updateInputs();
202
226
  container.replaceChildren(colorRow, thicknessRow);
203
227
  penTypeSelect.addTo(container);
204
- stabilizationOption.addTo(container);
205
228
  dropdown.replaceChildren(container);
229
+ // Add the toggle button row *outside* of the main content (use different
230
+ // spacing with respect to the sides of the container).
231
+ toggleButtonRow.addTo(dropdown);
206
232
  return true;
207
233
  }
208
234
  onKeyPress(event) {
@@ -232,6 +258,7 @@ class PenToolWidget extends BaseToolWidget {
232
258
  thickness: this.tool.getThickness(),
233
259
  strokeFactoryId: this.getCurrentPenType()?.id,
234
260
  inputStabilization: !!this.tool.getInputMapper(),
261
+ strokeAutocorrect: this.tool.getStrokeAutocorrectionEnabled(),
235
262
  };
236
263
  }
237
264
  deserializeFrom(state) {
@@ -262,7 +289,10 @@ class PenToolWidget extends BaseToolWidget {
262
289
  }
263
290
  }
264
291
  if (state.inputStabilization !== undefined) {
265
- this.setInputStabilizationEnabled(!!state.inputStabilization);
292
+ this.tool.setHasStabilization(!!state.inputStabilization);
293
+ }
294
+ if (state.strokeAutocorrect !== undefined) {
295
+ this.tool.setStrokeAutocorrectEnabled(!!state.strokeAutocorrect);
266
296
  }
267
297
  }
268
298
  }
@@ -1,3 +1,4 @@
1
1
  export declare const resizeImageToSelectionKeyboardShortcut = "jsdraw.toolbar.SelectionTool.resizeImageToSelection";
2
2
  export declare const selectStrokeTypeKeyboardShortcutIds: string[];
3
3
  export declare const saveKeyboardShortcut = "jsdraw.toolbar.SaveActionWidget.save";
4
+ export declare const exitKeyboardShortcut = "jsdraw.toolbar.ExitActionWidget.exit";
@@ -11,3 +11,6 @@ for (let i = 0; i < selectStrokeTypeKeyboardShortcutIds.length; i++) {
11
11
  // Save
12
12
  export const saveKeyboardShortcut = 'jsdraw.toolbar.SaveActionWidget.save';
13
13
  KeyboardShortcutManager.registerDefaultKeyboardShortcut(saveKeyboardShortcut, ['ctrlOrMeta+KeyS'], 'Save');
14
+ // Exit
15
+ export const exitKeyboardShortcut = 'jsdraw.toolbar.ExitActionWidget.exit';
16
+ KeyboardShortcutManager.registerDefaultKeyboardShortcut(exitKeyboardShortcut, ['Alt+KeyQ'], 'Exit');
@@ -1,4 +1,4 @@
1
- import ReactiveValue from 'js-draw/src/util/ReactiveValue';
1
+ import ReactiveValue from '../../../util/ReactiveValue';
2
2
  /**
3
3
  * A class that manages whether/what content is shown for a widget.
4
4
  *
@@ -19,6 +19,11 @@ export default class Pen extends BaseTool {
19
19
  private currentDeviceType;
20
20
  private styleValue;
21
21
  private style;
22
+ private shapeAutocompletionEnabled;
23
+ private autocorrectedShape;
24
+ private lastAutocorrectedShape;
25
+ private removedAutocorrectedShapeTime;
26
+ private stationaryDetector;
22
27
  constructor(editor: Editor, description: string, style: Partial<PenStyle>);
23
28
  private getPressureMultiplier;
24
29
  protected toStrokePoint(pointer: Pointer): StrokeDataPoint;
@@ -30,12 +35,16 @@ export default class Pen extends BaseTool {
30
35
  onPointerMove({ current }: PointerEvt): void;
31
36
  onPointerUp({ current }: PointerEvt): boolean;
32
37
  onGestureCancel(): void;
38
+ private removedAutocorrectedShapeRecently;
39
+ private autocorrectShape;
33
40
  private finalizeStroke;
34
41
  private noteUpdated;
35
42
  setColor(color: Color4): void;
36
43
  setThickness(thickness: number): void;
37
44
  setStrokeFactory(factory: ComponentBuilderFactory): void;
38
45
  setHasStabilization(hasStabilization: boolean): void;
46
+ setStrokeAutocorrectEnabled(enabled: boolean): void;
47
+ getStrokeAutocorrectionEnabled(): boolean;
39
48
  getThickness(): number;
40
49
  getColor(): Color4;
41
50
  getStrokeFactory(): ComponentBuilderFactory;