js-draw 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/README.md +20 -6
  2. package/dist/bundle.js +1 -1
  3. package/dist/cjs/Editor.js +1 -1
  4. package/dist/cjs/Editor.loadFrom.test.d.ts +1 -0
  5. package/dist/cjs/Editor.test.d.ts +1 -0
  6. package/dist/cjs/Editor.toSVG.test.d.ts +1 -0
  7. package/dist/cjs/EditorImage.test.d.ts +1 -0
  8. package/dist/cjs/EventDispatcher.test.d.ts +1 -0
  9. package/dist/cjs/SVGLoader.test.d.ts +1 -0
  10. package/dist/cjs/UndoRedoHistory.test.d.ts +1 -0
  11. package/dist/cjs/commands/uniteCommands.test.d.ts +1 -0
  12. package/dist/cjs/components/AbstractComponent.transformBy.test.d.ts +1 -0
  13. package/dist/cjs/components/BackgroundComponent.test.d.ts +1 -0
  14. package/dist/cjs/components/Stroke.test.d.ts +1 -0
  15. package/dist/cjs/components/TextComponent.test.d.ts +1 -0
  16. package/dist/cjs/components/UnknownSVGObject.test.d.ts +1 -0
  17. package/dist/cjs/components/builders/FreehandLineBuilder.test.d.ts +1 -0
  18. package/dist/cjs/localizations/getLocalizationTable.test.d.ts +1 -0
  19. package/dist/cjs/rendering/RenderingStyle.test.d.ts +1 -0
  20. package/dist/cjs/rendering/caching/CacheRecord.test.d.ts +1 -0
  21. package/dist/cjs/rendering/caching/RenderingCache.test.d.ts +1 -0
  22. package/dist/cjs/rendering/renderers/DummyRenderer.test.d.ts +1 -0
  23. package/dist/cjs/rendering/renderers/TextOnlyRenderer.test.d.ts +1 -0
  24. package/dist/cjs/shortcuts/KeyBinding.test.d.ts +1 -0
  25. package/dist/cjs/shortcuts/KeyboardShortcutManager.test.d.ts +1 -0
  26. package/dist/cjs/toolbar/EdgeToolbar.test.d.ts +1 -0
  27. package/dist/cjs/tools/Eraser.test.d.ts +1 -0
  28. package/dist/cjs/tools/FindTool.test.d.ts +1 -0
  29. package/dist/cjs/tools/InputFilter/InputPipeline.test.d.ts +1 -0
  30. package/dist/cjs/tools/PanZoom.test.d.ts +1 -0
  31. package/dist/cjs/tools/Pen.test.d.ts +1 -0
  32. package/dist/cjs/tools/SelectionTool/SelectionTool.test.d.ts +1 -0
  33. package/dist/cjs/tools/UndoRedoShortcut.test.d.ts +1 -0
  34. package/dist/cjs/util/ReactiveValue.test.d.ts +1 -0
  35. package/dist/cjs/version.js +1 -1
  36. package/dist/cjs/version.test.d.ts +1 -0
  37. package/dist/mjs/Editor.loadFrom.test.d.ts +1 -0
  38. package/dist/mjs/Editor.mjs +1 -1
  39. package/dist/mjs/Editor.test.d.ts +1 -0
  40. package/dist/mjs/Editor.toSVG.test.d.ts +1 -0
  41. package/dist/mjs/EditorImage.test.d.ts +1 -0
  42. package/dist/mjs/EventDispatcher.test.d.ts +1 -0
  43. package/dist/mjs/SVGLoader.test.d.ts +1 -0
  44. package/dist/mjs/UndoRedoHistory.test.d.ts +1 -0
  45. package/dist/mjs/commands/uniteCommands.test.d.ts +1 -0
  46. package/dist/mjs/components/AbstractComponent.transformBy.test.d.ts +1 -0
  47. package/dist/mjs/components/BackgroundComponent.test.d.ts +1 -0
  48. package/dist/mjs/components/Stroke.test.d.ts +1 -0
  49. package/dist/mjs/components/TextComponent.test.d.ts +1 -0
  50. package/dist/mjs/components/UnknownSVGObject.test.d.ts +1 -0
  51. package/dist/mjs/components/builders/FreehandLineBuilder.test.d.ts +1 -0
  52. package/dist/mjs/localizations/getLocalizationTable.test.d.ts +1 -0
  53. package/dist/mjs/rendering/RenderingStyle.test.d.ts +1 -0
  54. package/dist/mjs/rendering/caching/CacheRecord.test.d.ts +1 -0
  55. package/dist/mjs/rendering/caching/RenderingCache.test.d.ts +1 -0
  56. package/dist/mjs/rendering/renderers/DummyRenderer.test.d.ts +1 -0
  57. package/dist/mjs/rendering/renderers/TextOnlyRenderer.test.d.ts +1 -0
  58. package/dist/mjs/shortcuts/KeyBinding.test.d.ts +1 -0
  59. package/dist/mjs/shortcuts/KeyboardShortcutManager.test.d.ts +1 -0
  60. package/dist/mjs/toolbar/EdgeToolbar.test.d.ts +1 -0
  61. package/dist/mjs/tools/Eraser.test.d.ts +1 -0
  62. package/dist/mjs/tools/FindTool.test.d.ts +1 -0
  63. package/dist/mjs/tools/InputFilter/InputPipeline.test.d.ts +1 -0
  64. package/dist/mjs/tools/PanZoom.test.d.ts +1 -0
  65. package/dist/mjs/tools/Pen.test.d.ts +1 -0
  66. package/dist/mjs/tools/SelectionTool/SelectionTool.test.d.ts +1 -0
  67. package/dist/mjs/tools/UndoRedoShortcut.test.d.ts +1 -0
  68. package/dist/mjs/util/ReactiveValue.test.d.ts +1 -0
  69. package/dist/mjs/version.mjs +1 -1
  70. package/dist/mjs/version.test.d.ts +1 -0
  71. package/dist-test/test_imports/package-lock.json +13 -0
  72. package/dist-test/test_imports/package.json +12 -0
  73. package/dist-test/test_imports/test-imports.js +11 -0
  74. package/dist-test/test_imports/test-require.cjs +14 -0
  75. package/package.json +2 -2
  76. package/src/Editor.loadFrom.test.ts +24 -0
  77. package/src/Editor.test.ts +107 -0
  78. package/src/Editor.toSVG.test.ts +294 -0
  79. package/src/Editor.ts +1443 -0
  80. package/src/EditorImage.test.ts +117 -0
  81. package/src/EditorImage.ts +609 -0
  82. package/src/EventDispatcher.test.ts +123 -0
  83. package/src/EventDispatcher.ts +72 -0
  84. package/src/Pointer.ts +183 -0
  85. package/src/SVGLoader.test.ts +114 -0
  86. package/src/SVGLoader.ts +672 -0
  87. package/src/UndoRedoHistory.test.ts +34 -0
  88. package/src/UndoRedoHistory.ts +102 -0
  89. package/src/Viewport.ts +322 -0
  90. package/src/bundle/bundled.ts +7 -0
  91. package/src/commands/Command.ts +45 -0
  92. package/src/commands/Duplicate.ts +75 -0
  93. package/src/commands/Erase.ts +95 -0
  94. package/src/commands/SerializableCommand.ts +49 -0
  95. package/src/commands/UnresolvedCommand.ts +37 -0
  96. package/src/commands/invertCommand.ts +58 -0
  97. package/src/commands/lib.ts +16 -0
  98. package/src/commands/localization.ts +47 -0
  99. package/src/commands/uniteCommands.test.ts +23 -0
  100. package/src/commands/uniteCommands.ts +140 -0
  101. package/src/components/AbstractComponent.transformBy.test.ts +23 -0
  102. package/src/components/AbstractComponent.ts +383 -0
  103. package/src/components/BackgroundComponent.test.ts +44 -0
  104. package/src/components/BackgroundComponent.ts +348 -0
  105. package/src/components/ImageComponent.ts +176 -0
  106. package/src/components/RestylableComponent.ts +161 -0
  107. package/src/components/SVGGlobalAttributesObject.ts +79 -0
  108. package/src/components/Stroke.test.ts +137 -0
  109. package/src/components/Stroke.ts +294 -0
  110. package/src/components/TextComponent.test.ts +202 -0
  111. package/src/components/TextComponent.ts +429 -0
  112. package/src/components/UnknownSVGObject.test.ts +10 -0
  113. package/src/components/UnknownSVGObject.ts +60 -0
  114. package/src/components/builders/ArrowBuilder.ts +106 -0
  115. package/src/components/builders/CircleBuilder.ts +100 -0
  116. package/src/components/builders/FreehandLineBuilder.test.ts +24 -0
  117. package/src/components/builders/FreehandLineBuilder.ts +210 -0
  118. package/src/components/builders/LineBuilder.ts +77 -0
  119. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +453 -0
  120. package/src/components/builders/RectangleBuilder.ts +73 -0
  121. package/src/components/builders/types.ts +15 -0
  122. package/src/components/lib.ts +31 -0
  123. package/src/components/localization.ts +24 -0
  124. package/src/components/util/StrokeSmoother.ts +302 -0
  125. package/src/components/util/describeComponentList.ts +18 -0
  126. package/src/dialogs/makeAboutDialog.ts +82 -0
  127. package/src/inputEvents.ts +143 -0
  128. package/src/lib.ts +91 -0
  129. package/src/localization.ts +34 -0
  130. package/src/localizations/de.ts +146 -0
  131. package/src/localizations/en.ts +8 -0
  132. package/src/localizations/es.ts +74 -0
  133. package/src/localizations/getLocalizationTable.test.ts +27 -0
  134. package/src/localizations/getLocalizationTable.ts +74 -0
  135. package/src/rendering/Display.ts +247 -0
  136. package/src/rendering/RenderablePathSpec.ts +88 -0
  137. package/src/rendering/RenderingStyle.test.ts +68 -0
  138. package/src/rendering/RenderingStyle.ts +55 -0
  139. package/src/rendering/TextRenderingStyle.ts +55 -0
  140. package/src/rendering/caching/CacheRecord.test.ts +48 -0
  141. package/src/rendering/caching/CacheRecord.ts +76 -0
  142. package/src/rendering/caching/CacheRecordManager.ts +71 -0
  143. package/src/rendering/caching/RenderingCache.test.ts +43 -0
  144. package/src/rendering/caching/RenderingCache.ts +66 -0
  145. package/src/rendering/caching/RenderingCacheNode.ts +404 -0
  146. package/src/rendering/caching/testUtils.ts +35 -0
  147. package/src/rendering/caching/types.ts +34 -0
  148. package/src/rendering/lib.ts +8 -0
  149. package/src/rendering/localization.ts +20 -0
  150. package/src/rendering/renderers/AbstractRenderer.ts +232 -0
  151. package/src/rendering/renderers/CanvasRenderer.ts +312 -0
  152. package/src/rendering/renderers/DummyRenderer.test.ts +41 -0
  153. package/src/rendering/renderers/DummyRenderer.ts +142 -0
  154. package/src/rendering/renderers/SVGRenderer.ts +434 -0
  155. package/src/rendering/renderers/TextOnlyRenderer.test.ts +34 -0
  156. package/src/rendering/renderers/TextOnlyRenderer.ts +68 -0
  157. package/src/shortcuts/KeyBinding.test.ts +61 -0
  158. package/src/shortcuts/KeyBinding.ts +257 -0
  159. package/src/shortcuts/KeyboardShortcutManager.test.ts +95 -0
  160. package/src/shortcuts/KeyboardShortcutManager.ts +163 -0
  161. package/src/shortcuts/lib.ts +3 -0
  162. package/src/testing/createEditor.ts +11 -0
  163. package/src/testing/getUniquePointerId.ts +18 -0
  164. package/src/testing/lib.ts +3 -0
  165. package/src/testing/sendPenEvent.ts +36 -0
  166. package/src/testing/sendTouchEvent.ts +71 -0
  167. package/src/toolbar/AbstractToolbar.ts +542 -0
  168. package/src/toolbar/DropdownToolbar.ts +220 -0
  169. package/src/toolbar/EdgeToolbar.test.ts +54 -0
  170. package/src/toolbar/EdgeToolbar.ts +543 -0
  171. package/src/toolbar/IconProvider.ts +861 -0
  172. package/src/toolbar/constants.ts +1 -0
  173. package/src/toolbar/lib.ts +6 -0
  174. package/src/toolbar/localization.ts +136 -0
  175. package/src/toolbar/types.ts +13 -0
  176. package/src/toolbar/widgets/ActionButtonWidget.ts +39 -0
  177. package/src/toolbar/widgets/BaseToolWidget.ts +81 -0
  178. package/src/toolbar/widgets/BaseWidget.ts +495 -0
  179. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +250 -0
  180. package/src/toolbar/widgets/EraserToolWidget.ts +84 -0
  181. package/src/toolbar/widgets/HandToolWidget.ts +239 -0
  182. package/src/toolbar/widgets/InsertImageWidget.ts +248 -0
  183. package/src/toolbar/widgets/OverflowWidget.ts +92 -0
  184. package/src/toolbar/widgets/PenToolWidget.ts +369 -0
  185. package/src/toolbar/widgets/SelectionToolWidget.ts +195 -0
  186. package/src/toolbar/widgets/TextToolWidget.ts +149 -0
  187. package/src/toolbar/widgets/components/makeColorInput.ts +184 -0
  188. package/src/toolbar/widgets/components/makeFileInput.ts +128 -0
  189. package/src/toolbar/widgets/components/makeGridSelector.ts +179 -0
  190. package/src/toolbar/widgets/components/makeSeparator.ts +17 -0
  191. package/src/toolbar/widgets/components/makeThicknessSlider.ts +62 -0
  192. package/src/toolbar/widgets/keybindings.ts +19 -0
  193. package/src/toolbar/widgets/layout/DropdownLayoutManager.ts +262 -0
  194. package/src/toolbar/widgets/layout/EdgeToolbarLayoutManager.ts +71 -0
  195. package/src/toolbar/widgets/layout/types.ts +74 -0
  196. package/src/toolbar/widgets/lib.ts +13 -0
  197. package/src/tools/BaseTool.ts +169 -0
  198. package/src/tools/Eraser.test.ts +103 -0
  199. package/src/tools/Eraser.ts +173 -0
  200. package/src/tools/FindTool.test.ts +67 -0
  201. package/src/tools/FindTool.ts +153 -0
  202. package/src/tools/InputFilter/FunctionMapper.ts +17 -0
  203. package/src/tools/InputFilter/InputMapper.ts +41 -0
  204. package/src/tools/InputFilter/InputPipeline.test.ts +41 -0
  205. package/src/tools/InputFilter/InputPipeline.ts +34 -0
  206. package/src/tools/InputFilter/InputStabilizer.ts +254 -0
  207. package/src/tools/InputFilter/StrokeKeyboardControl.ts +104 -0
  208. package/src/tools/PanZoom.test.ts +339 -0
  209. package/src/tools/PanZoom.ts +525 -0
  210. package/src/tools/PasteHandler.ts +94 -0
  211. package/src/tools/Pen.test.ts +260 -0
  212. package/src/tools/Pen.ts +284 -0
  213. package/src/tools/PipetteTool.ts +84 -0
  214. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +29 -0
  215. package/src/tools/SelectionTool/Selection.ts +647 -0
  216. package/src/tools/SelectionTool/SelectionHandle.ts +142 -0
  217. package/src/tools/SelectionTool/SelectionTool.test.ts +370 -0
  218. package/src/tools/SelectionTool/SelectionTool.ts +510 -0
  219. package/src/tools/SelectionTool/TransformMode.ts +112 -0
  220. package/src/tools/SelectionTool/types.ts +11 -0
  221. package/src/tools/SoundUITool.ts +221 -0
  222. package/src/tools/TextTool.ts +339 -0
  223. package/src/tools/ToolController.ts +224 -0
  224. package/src/tools/ToolEnabledGroup.ts +14 -0
  225. package/src/tools/ToolSwitcherShortcut.ts +39 -0
  226. package/src/tools/ToolbarShortcutHandler.ts +39 -0
  227. package/src/tools/UndoRedoShortcut.test.ts +62 -0
  228. package/src/tools/UndoRedoShortcut.ts +24 -0
  229. package/src/tools/keybindings.ts +85 -0
  230. package/src/tools/lib.ts +22 -0
  231. package/src/tools/localization.ts +76 -0
  232. package/src/types.ts +151 -0
  233. package/src/util/ReactiveValue.test.ts +168 -0
  234. package/src/util/ReactiveValue.ts +241 -0
  235. package/src/util/assertions.ts +55 -0
  236. package/src/util/fileToBase64.ts +18 -0
  237. package/src/util/guessKeyCodeFromKey.ts +36 -0
  238. package/src/util/listPrefixMatch.ts +19 -0
  239. package/src/util/stopPropagationOfScrollingWheelEvents.ts +20 -0
  240. package/src/util/untilNextAnimationFrame.ts +9 -0
  241. package/src/util/waitForAll.ts +18 -0
  242. package/src/util/waitForTimeout.ts +9 -0
  243. package/src/version.test.ts +12 -0
  244. package/src/version.ts +3 -0
@@ -0,0 +1,248 @@
1
+ import ImageComponent from '../../components/ImageComponent';
2
+ import Editor from '../../Editor';
3
+ import Erase from '../../commands/Erase';
4
+ import EditorImage from '../../EditorImage';
5
+ import uniteCommands from '../../commands/uniteCommands';
6
+ import SelectionTool from '../../tools/SelectionTool/SelectionTool';
7
+ import { Mat33 } from '@js-draw/math';
8
+ import fileToBase64 from '../../util/fileToBase64';
9
+ import { ToolbarLocalization } from '../localization';
10
+ import BaseWidget from './BaseWidget';
11
+ import { EditorEventType } from '../../types';
12
+ import { toolbarCSSPrefix } from '../constants';
13
+ import makeFileInput from './components/makeFileInput';
14
+ import { MutableReactiveValue } from '../../util/ReactiveValue';
15
+
16
+ export default class InsertImageWidget extends BaseWidget {
17
+ private imagePreview: HTMLImageElement;
18
+ private selectedFiles: MutableReactiveValue<File[]>|null;
19
+ private imageAltTextInput: HTMLInputElement;
20
+ private statusView: HTMLElement;
21
+ private imageBase64URL: string|null;
22
+ private submitButton: HTMLButtonElement;
23
+
24
+ public constructor(editor: Editor, localization?: ToolbarLocalization) {
25
+ localization ??= editor.localization;
26
+
27
+ super(editor, 'insert-image-widget', localization);
28
+
29
+ // Make the dropdown showable
30
+ this.container.classList.add('dropdownShowable');
31
+
32
+ editor.notifier.on(EditorEventType.SelectionUpdated, event => {
33
+ if (event.kind === EditorEventType.SelectionUpdated && this.isDropdownVisible()) {
34
+ this.updateInputs();
35
+ }
36
+ });
37
+ }
38
+
39
+ protected override getTitle(): string {
40
+ return this.localizationTable.image;
41
+ }
42
+
43
+ protected override createIcon(): Element | null {
44
+ return this.editor.icons.makeInsertImageIcon();
45
+ }
46
+
47
+ protected override setDropdownVisible(visible: boolean): void {
48
+ super.setDropdownVisible(visible);
49
+
50
+ // Update the dropdown just before showing.
51
+ if (this.isDropdownVisible()) {
52
+ this.updateInputs();
53
+ }
54
+ }
55
+
56
+ protected override handleClick() {
57
+ this.setDropdownVisible(!this.isDropdownVisible());
58
+ }
59
+
60
+ private static nextInputId = 0;
61
+
62
+ protected override fillDropdown(dropdown: HTMLElement): boolean {
63
+ const container = document.createElement('div');
64
+ container.classList.add(
65
+ 'insert-image-widget-dropdown-content',
66
+ `${toolbarCSSPrefix}spacedList`,
67
+ `${toolbarCSSPrefix}nonbutton-controls-main-list`,
68
+ );
69
+
70
+ const {
71
+ container: chooseImageRow,
72
+ selectedFiles,
73
+ } = makeFileInput(
74
+ this.localizationTable.chooseFile,
75
+ this.editor,
76
+ 'image/*',
77
+ );
78
+ const altTextRow = document.createElement('div');
79
+ this.imagePreview = document.createElement('img');
80
+ this.statusView = document.createElement('div');
81
+ const actionButtonRow = document.createElement('div');
82
+
83
+ actionButtonRow.classList.add('action-button-row');
84
+
85
+ this.submitButton = document.createElement('button');
86
+ this.selectedFiles = selectedFiles;
87
+ this.imageAltTextInput = document.createElement('input');
88
+
89
+ // Label the alt text input
90
+ const imageAltTextLabel = document.createElement('label');
91
+
92
+ const altTextInputId = `insert-image-alt-text-input-${InsertImageWidget.nextInputId++}`;
93
+ this.imageAltTextInput.setAttribute('id', altTextInputId);
94
+ imageAltTextLabel.htmlFor = altTextInputId;
95
+
96
+ imageAltTextLabel.innerText = this.localizationTable.inputAltText;
97
+ this.imageAltTextInput.type = 'text';
98
+
99
+ this.statusView.setAttribute('aria-live', 'polite');
100
+
101
+ this.submitButton.innerText = this.localizationTable.submit;
102
+
103
+ this.selectedFiles.onUpdateAndNow(async files => {
104
+ if (files.length === 0) {
105
+ this.imagePreview.style.display = 'none';
106
+ this.submitButton.disabled = true;
107
+ this.submitButton.style.display = 'none';
108
+ return;
109
+ }
110
+
111
+ this.imagePreview.style.display = 'block';
112
+
113
+ const image = files[0];
114
+
115
+ let data: string|null = null;
116
+
117
+ try {
118
+ data = await fileToBase64(image);
119
+ } catch(e) {
120
+ this.statusView.innerText = this.localizationTable.imageLoadError(e);
121
+ }
122
+
123
+ this.imageBase64URL = data;
124
+
125
+ if (data) {
126
+ this.imagePreview.src = data;
127
+ this.submitButton.disabled = false;
128
+ this.submitButton.style.display = '';
129
+ this.updateImageSizeDisplay();
130
+ } else {
131
+ this.submitButton.disabled = true;
132
+ this.submitButton.style.display = 'none';
133
+ this.statusView.innerText = '';
134
+ }
135
+ });
136
+
137
+ altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
138
+ actionButtonRow.replaceChildren(this.submitButton);
139
+
140
+ container.replaceChildren(
141
+ chooseImageRow, altTextRow, this.imagePreview, this.statusView, actionButtonRow
142
+ );
143
+
144
+ dropdown.replaceChildren(container);
145
+ return true;
146
+ }
147
+
148
+ private hideDialog() {
149
+ this.setDropdownVisible(false);
150
+ }
151
+
152
+ private updateImageSizeDisplay() {
153
+ const imageData = this.imageBase64URL ?? '';
154
+
155
+ const sizeInKiB = imageData.length / 1024;
156
+ const sizeInMiB = sizeInKiB / 1024;
157
+
158
+ let units = 'KiB';
159
+ let size = sizeInKiB;
160
+
161
+ if (sizeInMiB >= 1) {
162
+ size = sizeInMiB;
163
+ units = 'MiB';
164
+ }
165
+
166
+ this.statusView.innerText = this.localizationTable.imageSize(
167
+ Math.round(size), units
168
+ );
169
+ }
170
+
171
+ private updateInputs() {
172
+ const resetInputs = () => {
173
+ this.selectedFiles?.set([]);
174
+ this.imageAltTextInput.value = '';
175
+ this.imagePreview.style.display = 'none';
176
+ this.submitButton.disabled = true;
177
+ this.statusView.innerText = '';
178
+
179
+ this.submitButton.style.display = '';
180
+
181
+ this.imageAltTextInput.oninput = null;
182
+ };
183
+ resetInputs();
184
+
185
+ const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool);
186
+ const selectedObjects = selectionTools.map(tool => tool.getSelectedObjects()).flat();
187
+
188
+ // Check: Is there a selected image that can be edited?
189
+ let editingImage: ImageComponent|null = null;
190
+ if (selectedObjects.length === 1 && selectedObjects[0] instanceof ImageComponent) {
191
+ editingImage = selectedObjects[0];
192
+
193
+ this.imageAltTextInput.value = editingImage.getAltText() ?? '';
194
+ this.imagePreview.style.display = 'block';
195
+ this.submitButton.disabled = false;
196
+
197
+ this.imageBase64URL = editingImage.getURL();
198
+ this.imagePreview.src = this.imageBase64URL;
199
+
200
+ this.updateImageSizeDisplay();
201
+ } else if (selectedObjects.length > 0) {
202
+ // If not, clear the selection.
203
+ selectionTools.forEach(tool => tool.clearSelection());
204
+ }
205
+
206
+ // Show the submit button only when there is data to submit.
207
+ this.submitButton.style.display = 'none';
208
+ this.imageAltTextInput.oninput = () => {
209
+ if (this.imagePreview.src?.length > 0) {
210
+ this.submitButton.style.display = '';
211
+ }
212
+ };
213
+
214
+ this.submitButton.onclick = async () => {
215
+ if (!this.imageBase64URL) {
216
+ return;
217
+ }
218
+
219
+ const image = new Image();
220
+ image.src = this.imageBase64URL;
221
+ image.setAttribute('alt', this.imageAltTextInput.value);
222
+
223
+ const component = await ImageComponent.fromImage(image, Mat33.identity);
224
+
225
+ if (component.getBBox().area === 0) {
226
+ this.statusView.innerText = this.localizationTable.errorImageHasZeroSize;
227
+ return;
228
+ }
229
+
230
+ this.hideDialog();
231
+
232
+ if (editingImage) {
233
+ const eraseCommand = new Erase([ editingImage ]);
234
+
235
+ await this.editor.dispatch(uniteCommands([
236
+ EditorImage.addElement(component),
237
+ component.transformBy(editingImage.getTransformation()),
238
+ component.setZIndex(editingImage.getZIndex()),
239
+ eraseCommand,
240
+ ]));
241
+
242
+ selectionTools[0]?.setSelection([ component ]);
243
+ } else {
244
+ await this.editor.addAndCenterComponents([ component ]);
245
+ }
246
+ };
247
+ }
248
+ }
@@ -0,0 +1,92 @@
1
+ import Editor from '../../Editor';
2
+ import { ToolbarLocalization } from '../localization';
3
+ import BaseWidget from './BaseWidget';
4
+
5
+
6
+ export default class OverflowWidget extends BaseWidget {
7
+ private overflowChildren: BaseWidget[] = [];
8
+ private overflowContainer: HTMLElement;
9
+
10
+ public constructor(editor: Editor, localizationTable?: ToolbarLocalization) {
11
+ super(editor, 'overflow-widget', localizationTable);
12
+
13
+
14
+ this.container.classList.add('toolbar-overflow-widget');
15
+
16
+ // Make the dropdown openable
17
+ this.container.classList.add('dropdownShowable');
18
+ this.overflowContainer ??= document.createElement('div');
19
+ }
20
+
21
+ protected getTitle(): string {
22
+ return this.localizationTable.toggleOverflow;
23
+ }
24
+
25
+ protected createIcon(): Element | null {
26
+ return this.editor.icons.makeOverflowIcon();
27
+ }
28
+
29
+ protected handleClick(): void {
30
+ this.setDropdownVisible(!this.isDropdownVisible());
31
+ }
32
+
33
+ protected override fillDropdown(dropdown: HTMLElement) {
34
+ this.overflowContainer ??= document.createElement('div');
35
+ if (this.overflowContainer.parentElement) {
36
+ this.overflowContainer.remove();
37
+ }
38
+
39
+ this.overflowContainer.classList.add('toolbar-overflow-widget-overflow-list');
40
+ dropdown.appendChild(this.overflowContainer);
41
+
42
+ return true;
43
+ }
44
+
45
+ /**
46
+ * Removes all `BaseWidget`s from this and returns them.
47
+ */
48
+ public clearChildren(): BaseWidget[] {
49
+ this.overflowContainer.replaceChildren();
50
+ this.container.classList.remove('horizontal');
51
+
52
+ const overflowChildren = this.overflowChildren;
53
+ this.overflowChildren = [];
54
+ return overflowChildren;
55
+ }
56
+
57
+ public getChildWidgets(): BaseWidget[] {
58
+ return [ ...this.overflowChildren ];
59
+ }
60
+
61
+ public hasAsChild(widget: BaseWidget) {
62
+ for (const otherWidget of this.overflowChildren) {
63
+ if (widget === otherWidget) {
64
+ return true;
65
+ }
66
+ }
67
+
68
+ return false;
69
+ }
70
+
71
+ /**
72
+ * Adds `widget` to this.
73
+ * `widget`'s previous parent is still responsible
74
+ * for serializing/deserializing its state.
75
+ */
76
+ public addToOverflow(widget: BaseWidget) {
77
+ this.overflowChildren.push(widget);
78
+ widget.addTo(this.overflowContainer);
79
+ widget.setIsToplevel(false);
80
+
81
+ // Switch to a horizontal layout if enough children
82
+ if (this.overflowChildren.length > 2) {
83
+ this.container.classList.add('horizontal');
84
+ }
85
+ }
86
+
87
+ // This always returns false.
88
+ // Don't try to move the overflow menu to itself.
89
+ public override canBeInOverflowMenu(): boolean {
90
+ return false;
91
+ }
92
+ }
@@ -0,0 +1,369 @@
1
+ import { makeArrowBuilder } from '../../components/builders/ArrowBuilder';
2
+ import { makeFreehandLineBuilder } from '../../components/builders/FreehandLineBuilder';
3
+ import { makePressureSensitiveFreehandLineBuilder } from '../../components/builders/PressureSensitiveFreehandLineBuilder';
4
+ import { makeLineBuilder } from '../../components/builders/LineBuilder';
5
+ import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../../components/builders/RectangleBuilder';
6
+ import { makeOutlinedCircleBuilder } from '../../components/builders/CircleBuilder';
7
+ import { ComponentBuilderFactory } from '../../components/builders/types';
8
+ import Editor from '../../Editor';
9
+ import Pen from '../../tools/Pen';
10
+ import { EditorEventType } from '../../types';
11
+ import { KeyPressEvent } from '../../inputEvents';
12
+ import { ToolbarLocalization } from '../localization';
13
+ import makeColorInput from './components/makeColorInput';
14
+ import BaseToolWidget from './BaseToolWidget';
15
+ import { Color4 } from '@js-draw/math';
16
+ import { SavedToolbuttonState } from './BaseWidget';
17
+ import { selectStrokeTypeKeyboardShortcutIds } from './keybindings';
18
+ import { toolbarCSSPrefix } from '../constants';
19
+ import makeThicknessSlider from './components/makeThicknessSlider';
20
+ import makeGridSelector from './components/makeGridSelector';
21
+
22
+ export interface PenTypeRecord {
23
+ // Description of the factory (e.g. 'Freehand line')
24
+ name: string;
25
+
26
+ // A unique ID for the facotory (e.g. 'chisel-tip-pen')
27
+ id: string;
28
+
29
+ // True if the pen type generates shapes (and should thus be shown in the GUI
30
+ // as a shape generator). Defaults to false.
31
+ isShapeBuilder?: boolean;
32
+
33
+ // Creates an `AbstractComponent` from pen input.
34
+ factory: ComponentBuilderFactory;
35
+ }
36
+
37
+ export default class PenToolWidget extends BaseToolWidget {
38
+ private updateInputs: ()=> void = () => {};
39
+ protected penTypes: PenTypeRecord[];
40
+ protected shapelikeIDs: string[];
41
+
42
+ // A counter variable that ensures different HTML elements are given unique names/ids.
43
+ private static idCounter: number = 0;
44
+
45
+ public constructor(
46
+ editor: Editor, private tool: Pen, localization?: ToolbarLocalization
47
+ ) {
48
+ super(editor, tool, 'pen', localization);
49
+
50
+ // Pen types that correspond to
51
+ this.shapelikeIDs = [ 'pressure-sensitive-pen', 'freehand-pen' ];
52
+
53
+ // Default pen types
54
+ this.penTypes = [
55
+ {
56
+ name: this.localizationTable.flatTipPen,
57
+ id: 'pressure-sensitive-pen',
58
+
59
+ factory: makePressureSensitiveFreehandLineBuilder,
60
+ },
61
+ {
62
+ name: this.localizationTable.roundedTipPen,
63
+ id: 'freehand-pen',
64
+
65
+ factory: makeFreehandLineBuilder,
66
+ },
67
+ {
68
+ name: this.localizationTable.arrowPen,
69
+ id: 'arrow',
70
+
71
+ isShapeBuilder: true,
72
+ factory: makeArrowBuilder,
73
+ },
74
+ {
75
+ name: this.localizationTable.linePen,
76
+ id: 'line',
77
+
78
+ isShapeBuilder: true,
79
+ factory: makeLineBuilder,
80
+ },
81
+ {
82
+ name: this.localizationTable.filledRectanglePen,
83
+ id: 'filled-rectangle',
84
+
85
+ isShapeBuilder: true,
86
+ factory: makeFilledRectangleBuilder,
87
+ },
88
+ {
89
+ name: this.localizationTable.outlinedRectanglePen,
90
+ id: 'outlined-rectangle',
91
+
92
+ isShapeBuilder: true,
93
+ factory: makeOutlinedRectangleBuilder,
94
+ },
95
+ {
96
+ name: this.localizationTable.outlinedCirclePen,
97
+ id: 'outlined-circle',
98
+
99
+ isShapeBuilder: true,
100
+ factory: makeOutlinedCircleBuilder,
101
+ }
102
+ ];
103
+
104
+ this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
105
+ if (toolEvt.kind !== EditorEventType.ToolUpdated) {
106
+ throw new Error('Invalid event type!');
107
+ }
108
+
109
+ // The button icon may depend on tool properties.
110
+ if (toolEvt.tool === this.tool) {
111
+ this.updateIcon();
112
+ this.updateInputs();
113
+ }
114
+ });
115
+ }
116
+
117
+ protected getTitle(): string {
118
+ return this.targetTool.description;
119
+ }
120
+
121
+ // Return the index of this tool's stroke factory in the list of
122
+ // all stroke factories.
123
+ //
124
+ // Returns -1 if the stroke factory is not in the list of all stroke factories.
125
+ private getCurrentPenTypeIdx(): number {
126
+ const currentFactory = this.tool.getStrokeFactory();
127
+
128
+ for (let i = 0; i < this.penTypes.length; i ++) {
129
+ if (this.penTypes[i].factory === currentFactory) {
130
+ return i;
131
+ }
132
+ }
133
+ return -1;
134
+ }
135
+
136
+ private getCurrentPenType(): PenTypeRecord|null {
137
+ for (const penType of this.penTypes) {
138
+ if (penType.factory === this.tool.getStrokeFactory()) {
139
+ return penType;
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+
145
+ private createIconForRecord(record: PenTypeRecord|null) {
146
+ const style = {
147
+ ...this.tool.getStyleValue().get(),
148
+ };
149
+
150
+ if (record?.factory) {
151
+ style.factory = record.factory;
152
+ }
153
+
154
+ const strokeFactory = record?.factory;
155
+ if (!strokeFactory || strokeFactory === makeFreehandLineBuilder || strokeFactory === makePressureSensitiveFreehandLineBuilder) {
156
+ return this.editor.icons.makePenIcon(style);
157
+ } else {
158
+ return this.editor.icons.makeIconFromFactory(style);
159
+ }
160
+ }
161
+
162
+ protected createIcon(): Element {
163
+ return this.createIconForRecord(this.getCurrentPenType());
164
+ }
165
+
166
+
167
+ // Creates a widget that allows selecting different pen types
168
+ private createPenTypeSelector() {
169
+ const allChoices = this.penTypes.map((penType, index) => {
170
+ return {
171
+ id: index,
172
+ makeIcon: () => this.createIconForRecord(penType),
173
+ title: penType.name,
174
+ isShapeBuilder: penType.isShapeBuilder ?? false,
175
+ };
176
+ });
177
+
178
+ const penSelector = makeGridSelector(
179
+ this.localizationTable.selectPenTip,
180
+ this.getCurrentPenTypeIdx(),
181
+ allChoices.filter(choice => !choice.isShapeBuilder),
182
+ );
183
+
184
+ const shapeSelector = makeGridSelector(
185
+ this.localizationTable.selectShape,
186
+ this.getCurrentPenTypeIdx(),
187
+ allChoices.filter(choice => choice.isShapeBuilder),
188
+ );
189
+
190
+ const onSelectorUpdate = (newPenTypeIndex: number) => {
191
+ this.tool.setStrokeFactory(this.penTypes[newPenTypeIndex].factory);
192
+ };
193
+
194
+ penSelector.value.onUpdate(onSelectorUpdate);
195
+ shapeSelector.value.onUpdate(onSelectorUpdate);
196
+
197
+ return {
198
+ setValue: (penTypeIndex: number) => {
199
+ penSelector.value.set(penTypeIndex);
200
+ shapeSelector.value.set(penTypeIndex);
201
+ },
202
+
203
+ updateIcons: () => {
204
+ penSelector.updateIcons();
205
+ shapeSelector.updateIcons();
206
+ },
207
+
208
+ addTo: (parent: HTMLElement) => {
209
+ penSelector.addTo(parent);
210
+ shapeSelector.addTo(parent);
211
+ },
212
+ };
213
+ }
214
+
215
+ private setInputStabilizationEnabled(enabled: boolean) {
216
+ this.tool.setHasStabilization(enabled);
217
+ }
218
+
219
+ protected createStabilizationOption() {
220
+ const stabilizationOption = document.createElement('div');
221
+ const stabilizationCheckbox = document.createElement('input');
222
+ const stabilizationLabel = document.createElement('label');
223
+ stabilizationLabel.innerText = this.localizationTable.inputStabilization;
224
+
225
+ stabilizationCheckbox.type = 'checkbox';
226
+ stabilizationCheckbox.id = `${toolbarCSSPrefix}-penInputStabilizationCheckbox-${PenToolWidget.idCounter++}`;
227
+ stabilizationLabel.htmlFor = stabilizationCheckbox.id;
228
+
229
+ stabilizationOption.replaceChildren(stabilizationLabel, stabilizationCheckbox);
230
+
231
+ stabilizationCheckbox.oninput = () => {
232
+ this.setInputStabilizationEnabled(stabilizationCheckbox.checked);
233
+ };
234
+
235
+ return {
236
+ update: () => {
237
+ stabilizationCheckbox.checked = !!this.tool.getInputMapper();
238
+ },
239
+
240
+ addTo: (parent: HTMLElement) => {
241
+ parent.appendChild(stabilizationOption);
242
+ }
243
+ };
244
+ }
245
+
246
+ protected override fillDropdown(dropdown: HTMLElement): boolean {
247
+ const container = document.createElement('div');
248
+ container.classList.add(
249
+ `${toolbarCSSPrefix}spacedList`, `${toolbarCSSPrefix}nonbutton-controls-main-list`
250
+ );
251
+
252
+ // Thickness: Value of the input is squared to allow for finer control/larger values.
253
+ const { container: thicknessRow, setValue: setThickness } = makeThicknessSlider(this.editor, thickness => {
254
+ this.tool.setThickness(thickness);
255
+ });
256
+
257
+ const penTypeSelect = this.createPenTypeSelector();
258
+
259
+ const colorRow = document.createElement('div');
260
+ const colorLabel = document.createElement('label');
261
+ const {
262
+ input: colorInput, container: colorInputContainer, setValue: setColorInputValue
263
+ } = makeColorInput(this.editor, color => {
264
+ this.tool.setColor(color);
265
+ });
266
+
267
+ colorInput.id = `${toolbarCSSPrefix}colorInput${PenToolWidget.idCounter++}`;
268
+ colorLabel.innerText = this.localizationTable.colorLabel;
269
+ colorLabel.setAttribute('for', colorInput.id);
270
+
271
+ colorRow.appendChild(colorLabel);
272
+ colorRow.appendChild(colorInputContainer);
273
+
274
+ const stabilizationOption = this.createStabilizationOption();
275
+
276
+ this.updateInputs = () => {
277
+ setColorInputValue(this.tool.getColor());
278
+ setThickness(this.tool.getThickness());
279
+
280
+ penTypeSelect.updateIcons();
281
+
282
+ // Update the selected stroke factory.
283
+ penTypeSelect.setValue(this.getCurrentPenTypeIdx());
284
+ stabilizationOption.update();
285
+ };
286
+ this.updateInputs();
287
+
288
+ container.replaceChildren(colorRow, thicknessRow);
289
+ penTypeSelect.addTo(container);
290
+ stabilizationOption.addTo(container);
291
+
292
+ dropdown.replaceChildren(container);
293
+ return true;
294
+ }
295
+
296
+ protected override onKeyPress(event: KeyPressEvent): boolean {
297
+ if (!this.isSelected()) {
298
+ return false;
299
+ }
300
+
301
+ for (let i = 0; i < selectStrokeTypeKeyboardShortcutIds.length; i++) {
302
+ const shortcut = selectStrokeTypeKeyboardShortcutIds[i];
303
+ if (this.editor.shortcuts.matchesShortcut(shortcut, event)) {
304
+ const penTypeIdx = i;
305
+ if (penTypeIdx < this.penTypes.length) {
306
+ this.tool.setStrokeFactory(this.penTypes[penTypeIdx].factory);
307
+ return true;
308
+ }
309
+ }
310
+ }
311
+
312
+ // Run any default actions registered by the parent class.
313
+ if (super.onKeyPress(event)) {
314
+ return true;
315
+ }
316
+ return false;
317
+ }
318
+
319
+ public override serializeState(): SavedToolbuttonState {
320
+ return {
321
+ ...super.serializeState(),
322
+
323
+ color: this.tool.getColor().toHexString(),
324
+ thickness: this.tool.getThickness(),
325
+ strokeFactoryId: this.getCurrentPenType()?.id,
326
+ inputStabilization: !!this.tool.getInputMapper(),
327
+ };
328
+ }
329
+
330
+ public override deserializeFrom(state: SavedToolbuttonState) {
331
+ super.deserializeFrom(state);
332
+
333
+ const verifyPropertyType = (propertyName: string, expectedType: 'string'|'number'|'object') => {
334
+ const actualType = typeof(state[propertyName]);
335
+ if (actualType !== expectedType) {
336
+ throw new Error(
337
+ `Deserializing property ${propertyName}: Invalid type. Expected ${expectedType},` +
338
+ ` was ${actualType}.`
339
+ );
340
+ }
341
+ };
342
+
343
+ if (state.color) {
344
+ verifyPropertyType('color', 'string');
345
+ this.tool.setColor(Color4.fromHex(state.color));
346
+ }
347
+
348
+ if (state.thickness) {
349
+ verifyPropertyType('thickness', 'number');
350
+ this.tool.setThickness(state.thickness);
351
+ }
352
+
353
+ if (state.strokeFactoryId) {
354
+ verifyPropertyType('strokeFactoryId', 'string');
355
+
356
+ const factoryId: string = state.strokeFactoryId;
357
+ for (const penType of this.penTypes) {
358
+ if (factoryId === penType.id) {
359
+ this.tool.setStrokeFactory(penType.factory);
360
+ break;
361
+ }
362
+ }
363
+ }
364
+
365
+ if (state.inputStabilization !== undefined) {
366
+ this.setInputStabilizationEnabled(!!state.inputStabilization);
367
+ }
368
+ }
369
+ }