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,195 @@
1
+ import { Color4 } from '@js-draw/math';
2
+ import { isRestylableComponent } from '../../components/RestylableComponent';
3
+ import Editor from '../../Editor';
4
+ import uniteCommands from '../../commands/uniteCommands';
5
+ import SelectionTool from '../../tools/SelectionTool/SelectionTool';
6
+ import { EditorEventType } from '../../types';
7
+ import { KeyPressEvent } from '../../inputEvents';
8
+ import { ToolbarLocalization } from '../localization';
9
+ import makeColorInput from './components/makeColorInput';
10
+ import ActionButtonWidget from './ActionButtonWidget';
11
+ import BaseToolWidget from './BaseToolWidget';
12
+ import { resizeImageToSelectionKeyboardShortcut } from './keybindings';
13
+ import makeSeparator from './components/makeSeparator';
14
+ import { toolbarCSSPrefix } from '../constants';
15
+
16
+ const makeFormatMenu = (editor: Editor, selectionTool: SelectionTool, localizationTable: ToolbarLocalization) => {
17
+ const container = document.createElement('div');
18
+ container.classList.add(
19
+ 'selection-format-menu', `${toolbarCSSPrefix}spacedList`, `${toolbarCSSPrefix}indentedList`
20
+ );
21
+
22
+ const colorRow = document.createElement('div');
23
+ const colorLabel = document.createElement('label');
24
+ const {
25
+ input: colorInput, container: colorInputContainer, setValue: setColorInputValue
26
+ } = makeColorInput(editor, color => {
27
+ const selection = selectionTool.getSelection();
28
+
29
+ if (selection) {
30
+ const updateStyleCommands = [];
31
+
32
+ for (const elem of selection.getSelectedObjects()) {
33
+ if (isRestylableComponent(elem)) {
34
+ updateStyleCommands.push(elem.updateStyle({ color }));
35
+ }
36
+ }
37
+
38
+ const unitedCommand = uniteCommands(updateStyleCommands);
39
+ editor.dispatch(unitedCommand);
40
+ }
41
+ });
42
+
43
+ colorLabel.innerText = localizationTable.colorLabel;
44
+
45
+ const update = () => {
46
+ const selection = selectionTool.getSelection();
47
+ if (selection && selection.getSelectedItemCount() > 0) {
48
+ colorInput.disabled = false;
49
+ container.classList.remove('disabled');
50
+
51
+ const colors = [];
52
+ for (const elem of selection.getSelectedObjects()) {
53
+ if (isRestylableComponent(elem)) {
54
+ const color = elem.getStyle().color;
55
+ if (color) {
56
+ colors.push(color);
57
+ }
58
+ }
59
+ }
60
+ setColorInputValue(Color4.average(colors));
61
+ } else {
62
+ colorInput.disabled = true;
63
+ container.classList.add('disabled');
64
+ setColorInputValue(Color4.transparent);
65
+ }
66
+ };
67
+
68
+ colorRow.replaceChildren(colorLabel, colorInputContainer);
69
+ container.replaceChildren(colorRow);
70
+
71
+ return {
72
+ addTo: (parent: HTMLElement) => {
73
+ parent.appendChild(container);
74
+ },
75
+ update,
76
+ };
77
+ };
78
+
79
+ export default class SelectionToolWidget extends BaseToolWidget {
80
+ private updateFormatMenu: ()=>void = () => {};
81
+
82
+ public constructor(
83
+ editor: Editor, private tool: SelectionTool, localization?: ToolbarLocalization
84
+ ) {
85
+ super(editor, tool, 'selection-tool-widget', localization);
86
+
87
+ const resizeButton = new ActionButtonWidget(
88
+ editor, 'resize-btn',
89
+ () => editor.icons.makeResizeImageToSelectionIcon(),
90
+ this.localizationTable.resizeImageToSelection,
91
+ () => {
92
+ this.resizeImageToSelection();
93
+ },
94
+ localization,
95
+ );
96
+ const deleteButton = new ActionButtonWidget(
97
+ editor, 'delete-btn',
98
+ () => editor.icons.makeDeleteSelectionIcon(),
99
+ this.localizationTable.deleteSelection,
100
+ () => {
101
+ const selection = this.tool.getSelection();
102
+ this.editor.dispatch(selection!.deleteSelectedObjects());
103
+ this.tool.clearSelection();
104
+ },
105
+ localization,
106
+ );
107
+ const duplicateButton = new ActionButtonWidget(
108
+ editor, 'duplicate-btn',
109
+ () => editor.icons.makeDuplicateSelectionIcon(),
110
+ this.localizationTable.duplicateSelection,
111
+ async () => {
112
+ const selection = this.tool.getSelection();
113
+ this.editor.dispatch(await selection!.duplicateSelectedObjects());
114
+ this.setDropdownVisible(false);
115
+ },
116
+ localization,
117
+ );
118
+
119
+ this.addSubWidget(resizeButton);
120
+ this.addSubWidget(deleteButton);
121
+ this.addSubWidget(duplicateButton);
122
+
123
+ const updateDisabled = (disabled: boolean) => {
124
+ resizeButton.setDisabled(disabled);
125
+ deleteButton.setDisabled(disabled);
126
+ duplicateButton.setDisabled(disabled);
127
+ };
128
+ updateDisabled(true);
129
+
130
+ // Enable/disable actions based on whether items are selected
131
+ this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
132
+ if (toolEvt.kind !== EditorEventType.ToolUpdated) {
133
+ throw new Error('Invalid event type!');
134
+ }
135
+
136
+ if (toolEvt.tool === this.tool) {
137
+ const selection = this.tool.getSelection();
138
+ const hasSelection = selection && selection.getSelectedItemCount() > 0;
139
+
140
+ updateDisabled(!hasSelection);
141
+ this.updateFormatMenu();
142
+ }
143
+ });
144
+ }
145
+
146
+ private resizeImageToSelection() {
147
+ const selection = this.tool.getSelection();
148
+ if (selection) {
149
+ this.editor.dispatch(this.editor.setImportExportRect(selection.region));
150
+ }
151
+ }
152
+
153
+ protected override onKeyPress(event: KeyPressEvent): boolean {
154
+ const shortcuts = this.editor.shortcuts;
155
+
156
+ // Resize image to selection:
157
+ // Other keys are handled directly by the selection tool.
158
+ if (shortcuts.matchesShortcut(resizeImageToSelectionKeyboardShortcut, event)) {
159
+ this.resizeImageToSelection();
160
+ return true;
161
+ }
162
+
163
+ // If we didn't handle the event, allow the superclass to handle it.
164
+ if (super.onKeyPress(event)) {
165
+ return true;
166
+ }
167
+ return false;
168
+ }
169
+
170
+ protected getTitle(): string {
171
+ return this.localizationTable.select;
172
+ }
173
+
174
+ protected createIcon(): Element {
175
+ return this.editor.icons.makeSelectionIcon();
176
+ }
177
+
178
+ protected override fillDropdown(dropdown: HTMLElement): boolean {
179
+ super.fillDropdown(dropdown);
180
+
181
+ const controlsContainer = document.createElement('div');
182
+ controlsContainer.classList.add(`${toolbarCSSPrefix}nonbutton-controls-main-list`);
183
+ dropdown.appendChild(controlsContainer);
184
+
185
+ makeSeparator(this.localizationTable.reformatSelection).addTo(controlsContainer);
186
+
187
+ const formatMenu = makeFormatMenu(this.editor, this.tool, this.localizationTable);
188
+ formatMenu.addTo(controlsContainer);
189
+ this.updateFormatMenu = () => formatMenu.update();
190
+
191
+ formatMenu.update();
192
+
193
+ return true;
194
+ }
195
+ }
@@ -0,0 +1,149 @@
1
+ import { Color4 } from '@js-draw/math';
2
+ import Editor from '../../Editor';
3
+ import TextTool from '../../tools/TextTool';
4
+ import { EditorEventType } from '../../types';
5
+ import { toolbarCSSPrefix } from '../constants';
6
+ import { ToolbarLocalization } from '../localization';
7
+ import makeColorInput from './components/makeColorInput';
8
+ import BaseToolWidget from './BaseToolWidget';
9
+ import { SavedToolbuttonState } from './BaseWidget';
10
+
11
+ export default class TextToolWidget extends BaseToolWidget {
12
+ private updateDropdownInputs: (()=>void)|null = null;
13
+ public constructor(editor: Editor, private tool: TextTool, localization?: ToolbarLocalization) {
14
+ super(editor, tool, 'text-tool-widget', localization);
15
+
16
+ editor.notifier.on(EditorEventType.ToolUpdated, evt => {
17
+ if (evt.kind === EditorEventType.ToolUpdated && evt.tool === tool) {
18
+ this.updateIcon();
19
+ this.updateDropdownInputs?.();
20
+ }
21
+ });
22
+ }
23
+
24
+ protected getTitle(): string {
25
+ return this.targetTool.description;
26
+ }
27
+
28
+ protected createIcon(): Element {
29
+ const textStyle = this.tool.getTextStyle();
30
+ return this.editor.icons.makeTextIcon(textStyle);
31
+ }
32
+
33
+ private static idCounter: number = 0;
34
+ protected override fillDropdown(dropdown: HTMLElement): boolean {
35
+ const container = document.createElement('div');
36
+ container.classList.add(
37
+ `${toolbarCSSPrefix}spacedList`, `${toolbarCSSPrefix}nonbutton-controls-main-list`
38
+ );
39
+ const fontRow = document.createElement('div');
40
+ const colorRow = document.createElement('div');
41
+ const sizeRow = document.createElement('div');
42
+
43
+ const fontInput = document.createElement('select');
44
+ const fontLabel = document.createElement('label');
45
+
46
+ const sizeInput = document.createElement('input');
47
+ const sizeLabel = document.createElement('label');
48
+
49
+ const {
50
+ input: colorInput, container: colorInputContainer, setValue: setColorInputValue
51
+ } = makeColorInput(this.editor, color => {
52
+ this.tool.setColor(color);
53
+ });
54
+ const colorLabel = document.createElement('label');
55
+
56
+ const fontsInInput = new Set();
57
+ const addFontToInput = (fontName: string) => {
58
+ const option = document.createElement('option');
59
+ option.value = fontName;
60
+ option.textContent = fontName;
61
+ fontInput.appendChild(option);
62
+ fontsInInput.add(fontName);
63
+ };
64
+
65
+ sizeInput.setAttribute('type', 'number');
66
+ sizeInput.min = '1';
67
+ sizeInput.max = '128';
68
+
69
+ fontLabel.innerText = this.localizationTable.fontLabel;
70
+ colorLabel.innerText = this.localizationTable.colorLabel;
71
+ sizeLabel.innerText = this.localizationTable.textSize;
72
+
73
+ colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
74
+ colorLabel.setAttribute('for', colorInput.id);
75
+
76
+ sizeInput.id = `${toolbarCSSPrefix}-text-size-input-${TextToolWidget.idCounter++}`;
77
+ sizeLabel.setAttribute('for', sizeInput.id);
78
+
79
+ addFontToInput('monospace');
80
+ addFontToInput('serif');
81
+ addFontToInput('sans-serif');
82
+ fontInput.id = `${toolbarCSSPrefix}-text-font-input-${TextToolWidget.idCounter++}`;
83
+ fontLabel.setAttribute('for', fontInput.id);
84
+
85
+ fontInput.onchange = () => {
86
+ this.tool.setFontFamily(fontInput.value);
87
+ };
88
+
89
+ sizeInput.onchange = () => {
90
+ const size = parseInt(sizeInput.value);
91
+ if (!isNaN(size) && size > 0) {
92
+ this.tool.setFontSize(size);
93
+ }
94
+ };
95
+
96
+ colorRow.appendChild(colorLabel);
97
+ colorRow.appendChild(colorInputContainer);
98
+
99
+ fontRow.appendChild(fontLabel);
100
+ fontRow.appendChild(fontInput);
101
+
102
+ sizeRow.appendChild(sizeLabel);
103
+ sizeRow.appendChild(sizeInput);
104
+
105
+ this.updateDropdownInputs = () => {
106
+ const style = this.tool.getTextStyle();
107
+ setColorInputValue(style.renderingStyle.fill);
108
+
109
+ if (!fontsInInput.has(style.fontFamily)) {
110
+ addFontToInput(style.fontFamily);
111
+ }
112
+ fontInput.value = style.fontFamily;
113
+ sizeInput.value = `${style.size}`;
114
+ };
115
+ this.updateDropdownInputs();
116
+
117
+ container.replaceChildren(colorRow, sizeRow, fontRow);
118
+ dropdown.appendChild(container);
119
+ return true;
120
+ }
121
+
122
+ public override serializeState(): SavedToolbuttonState {
123
+ const textStyle = this.tool.getTextStyle();
124
+
125
+ return {
126
+ ...super.serializeState(),
127
+
128
+ fontFamily: textStyle.fontFamily,
129
+ textSize: textStyle.size,
130
+ color: textStyle.renderingStyle.fill.toHexString(),
131
+ };
132
+ }
133
+
134
+ public override deserializeFrom(state: SavedToolbuttonState) {
135
+ if (state.fontFamily && typeof(state.fontFamily) === 'string') {
136
+ this.tool.setFontFamily(state.fontFamily);
137
+ }
138
+
139
+ if (state.color && typeof(state.color) === 'string') {
140
+ this.tool.setColor(Color4.fromHex(state.color));
141
+ }
142
+
143
+ if (state.textSize && typeof(state.textSize) === 'number') {
144
+ this.tool.setFontSize(state.textSize);
145
+ }
146
+
147
+ super.deserializeFrom(state);
148
+ }
149
+ }
@@ -0,0 +1,184 @@
1
+ import { Color4 } from '@js-draw/math';
2
+ import Editor from '../../../Editor';
3
+ import PipetteTool from '../../../tools/PipetteTool';
4
+ import { EditorEventType } from '../../../types';
5
+
6
+ type OnColorChangeListener = (color: Color4)=>void;
7
+
8
+ // Returns [ color input, input container, callback to change the color value ].
9
+ export const makeColorInput = (
10
+ editor: Editor, onColorChange: OnColorChangeListener
11
+ ) => {
12
+
13
+ const colorInputContainer = document.createElement('span');
14
+ const colorInput = document.createElement('input');
15
+
16
+ colorInput.type = 'button';
17
+ colorInput.classList.add('coloris_input');
18
+ colorInputContainer.classList.add('color-input-container');
19
+
20
+ colorInputContainer.appendChild(colorInput);
21
+ const pipetteController = addPipetteTool(editor, colorInputContainer, (color: Color4) => {
22
+ colorInput.value = color.toHexString();
23
+ onInputEnd();
24
+
25
+ // Update the color preview, if it exists (may be managed by Coloris).
26
+ const parentElem = colorInput.parentElement;
27
+ if (parentElem && parentElem.classList.contains('clr-field')) {
28
+ parentElem.style.color = colorInput.value;
29
+ }
30
+ });
31
+
32
+ let currentColor: Color4|undefined;
33
+ const handleColorInput = () => {
34
+ currentColor = Color4.fromHex(colorInput.value);
35
+ };
36
+
37
+ // Only change the pen color when we finish sending input (this limits the number of
38
+ // editor events triggered and accessibility announcements).
39
+ const onInputEnd = () => {
40
+ handleColorInput();
41
+
42
+ if (currentColor) {
43
+ editor.announceForAccessibility(
44
+ editor.localization.colorChangedAnnouncement(currentColor.toHexString())
45
+ );
46
+ onColorChange(currentColor);
47
+ editor.notifier.dispatch(EditorEventType.ColorPickerColorSelected, {
48
+ kind: EditorEventType.ColorPickerColorSelected,
49
+ color: currentColor,
50
+ });
51
+ }
52
+ };
53
+
54
+ colorInput.oninput = handleColorInput;
55
+ let isOpen = false;
56
+ colorInput.addEventListener('open', () => {
57
+ isOpen = true;
58
+ editor.notifier.dispatch(EditorEventType.ColorPickerToggled, {
59
+ kind: EditorEventType.ColorPickerToggled,
60
+ open: true,
61
+ });
62
+ pipetteController.cancel();
63
+ colorInputContainer.classList.add('picker-open');
64
+
65
+ // Focus the Coloris color picker, if it exists.
66
+ // Don't focus the text input within the color picker, however,
67
+ // as this displays a keyboard on mobile devices.
68
+ const colorPickerElem: HTMLElement|null = document.querySelector('#clr-picker #clr-hue-slider');
69
+ colorPickerElem?.focus();
70
+ });
71
+
72
+ const onClose = () => {
73
+ isOpen = false;
74
+ editor.notifier.dispatch(EditorEventType.ColorPickerToggled, {
75
+ kind: EditorEventType.ColorPickerToggled,
76
+ open: false,
77
+ });
78
+ onInputEnd();
79
+
80
+ // Restore focus to the input that opened the color picker
81
+ colorInput.focus();
82
+
83
+ colorInputContainer.classList.remove('picker-open');
84
+ };
85
+ colorInput.addEventListener('close', () => {
86
+ onClose();
87
+ });
88
+
89
+ const setColorInputValue = (color: Color4|string) => {
90
+ if (typeof color === 'object') {
91
+ color = color.toHexString();
92
+ }
93
+
94
+ colorInput.value = color;
95
+
96
+ // Fire all color event listeners. See
97
+ // https://github.com/mdbassit/Coloris#manually-updating-the-thumbnail
98
+ colorInput.dispatchEvent(new Event('input', { bubbles: true }));
99
+ };
100
+
101
+ return {
102
+ input: colorInput,
103
+ container: colorInputContainer,
104
+ setValue: setColorInputValue,
105
+ closePicker: () => {
106
+ if (isOpen) {
107
+ onInputEnd();
108
+ }
109
+ },
110
+ };
111
+ };
112
+
113
+ const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: OnColorChangeListener) => {
114
+ const pipetteButton = document.createElement('button');
115
+ pipetteButton.classList.add('pipetteButton');
116
+ pipetteButton.title = editor.localization.pickColorFromScreen;
117
+ pipetteButton.setAttribute('alt', pipetteButton.title);
118
+
119
+ const pickColorLabel = document.createElement('span');
120
+ pickColorLabel.classList.add('pickColorInstructions');
121
+ pickColorLabel.innerText = editor.localization.clickToPickColorAnnouncement;
122
+
123
+ const updatePipetteButtonContent = (color?: Color4) => {
124
+ pipetteButton.replaceChildren(
125
+ editor.icons.makePipetteIcon(color), pickColorLabel
126
+ );
127
+ };
128
+ updatePipetteButtonContent();
129
+
130
+ const pipetteTool: PipetteTool|undefined = editor.toolController.getMatchingTools(PipetteTool)[0];
131
+
132
+ const endColorSelectMode = () => {
133
+ pipetteTool?.clearColorListener();
134
+ updatePipetteButtonContent();
135
+ pipetteButton.classList.remove('active');
136
+ };
137
+
138
+ const pipetteColorSelect = (color: Color4|null) => {
139
+ endColorSelectMode();
140
+
141
+ if (color) {
142
+ onColorChange(color);
143
+ }
144
+ };
145
+
146
+ const pipetteColorPreview = (color: Color4|null) => {
147
+ if (color) {
148
+ updatePipetteButtonContent(color);
149
+ } else {
150
+ updatePipetteButtonContent();
151
+ }
152
+ };
153
+
154
+ pipetteButton.onclick = () => {
155
+ // If already picking, cancel it.
156
+ if (pipetteButton.classList.contains('active')) {
157
+ endColorSelectMode();
158
+ editor.announceForAccessibility(editor.localization.colorSelectionCanceledAnnouncement);
159
+ return;
160
+ }
161
+
162
+ pipetteTool?.setColorListener(
163
+ pipetteColorPreview,
164
+ pipetteColorSelect,
165
+ );
166
+
167
+ if (pipetteTool) {
168
+ pipetteButton.classList.add('active');
169
+
170
+ editor.announceForAccessibility(editor.localization.clickToPickColorAnnouncement);
171
+ }
172
+ };
173
+
174
+ container.appendChild(pipetteButton);
175
+
176
+ return {
177
+ // Cancel a pipette color selection if one is in progress.
178
+ cancel: () => {
179
+ endColorSelectMode();
180
+ },
181
+ };
182
+ };
183
+
184
+ export default makeColorInput;
@@ -0,0 +1,128 @@
1
+ import ReactiveValue, { MutableReactiveValue } from '../../../util/ReactiveValue';
2
+ import { ToolbarContext } from '../../types';
3
+
4
+ let idCounter = 0;
5
+
6
+ /**
7
+ * Creates a stylized file input.
8
+ */
9
+ const makeFileInput = (labelText: string, context: ToolbarContext, accepts: string = '*') => {
10
+ const container = document.createElement('div');
11
+ const label = document.createElement('label');
12
+ const input = document.createElement('input');
13
+
14
+ const descriptionBox = document.createElement('div');
15
+ descriptionBox.classList.add('toolbar--file-input-description');
16
+ const descriptionText = document.createElement('span');
17
+
18
+ container.classList.add('toolbar--file-input-container');
19
+
20
+ label.appendChild(document.createTextNode(labelText));
21
+ input.accept = accepts;
22
+ input.type = 'file';
23
+
24
+ // Associate the label with the input
25
+ const inputId = `js-draw-file-input-${idCounter ++}`;
26
+ input.setAttribute('id', inputId);
27
+ label.htmlFor = inputId;
28
+
29
+ const icon = context.icons.makeUploadFileIcon();
30
+ icon.classList.add('icon');
31
+
32
+ descriptionBox.replaceChildren(icon, descriptionText);
33
+ label.appendChild(descriptionBox);
34
+ container.replaceChildren(label, input);
35
+
36
+ const selectedFiles: MutableReactiveValue<File[]> = ReactiveValue.fromInitialValue([]);
37
+
38
+ // Support droping files
39
+ label.addEventListener('dragover', event => {
40
+ event.preventDefault();
41
+ label.classList.add('drag-target');
42
+ });
43
+ label.addEventListener('dragenter', event => {
44
+ event.preventDefault();
45
+ label.classList.add('drag-target');
46
+ });
47
+ label.addEventListener('dragleave', event => {
48
+ event.preventDefault();
49
+
50
+ // Ensure the event wasn't targeting a child.
51
+ // See https://stackoverflow.com/a/54271161 and
52
+ // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/relatedTarget
53
+ const enteringElement = event.relatedTarget as HTMLElement;
54
+ if (!enteringElement || !label.contains(enteringElement)) {
55
+ label.classList.remove('drag-target');
56
+ }
57
+ });
58
+
59
+ // See https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop#process_the_drop
60
+ label.addEventListener('drop', event => {
61
+ event.preventDefault();
62
+ label.classList.remove('drag-target');
63
+
64
+ const fileList: File[] = [];
65
+
66
+ if (event.dataTransfer) {
67
+ fileList.push(...event.dataTransfer.files);
68
+ }
69
+
70
+ selectedFiles.set(fileList);
71
+ });
72
+ input.addEventListener('change', () => {
73
+ const fileList = input.files ?? [];
74
+ selectedFiles.set([ ...fileList ]);
75
+ });
76
+
77
+ selectedFiles.onUpdate(files => {
78
+ if (files.length === 0 && input.files && input.files.length > 0) {
79
+ input.value = '';
80
+ }
81
+ });
82
+
83
+ // Update the status text and hide/show the icon.
84
+ selectedFiles.onUpdateAndNow(files => {
85
+ if (files.length > 0) {
86
+ descriptionText.innerText = files.map(file => file.name).join('\n');
87
+
88
+ // Only show the icon when there are files
89
+ icon.style.display = 'none';
90
+ } else {
91
+ // Show the icon
92
+ icon.style.display = '';
93
+
94
+ const text = context.localization.dragAndDropHereOrBrowse;
95
+
96
+ // Split into regions surrounded by {{curly braces}} and regions that are
97
+ // not.
98
+ // When given a regular expression, `.split` outputs an array. For example,
99
+ // "a test __of__ split".split(/__(.*)__/)
100
+ // results in
101
+ // ['a test ', 'of', ' split'].
102
+ const segments = text.split(/[{]{2}(.*)[}]{2}/g);
103
+ descriptionText.replaceChildren();
104
+
105
+ for (let i = 0; i < segments.length; i++) {
106
+ // Inside a {{pair of curly braces}}?
107
+ if (i % 2 === 1) {
108
+ const boldedText = document.createElement('b');
109
+ boldedText.innerText = segments[i];
110
+ descriptionText.appendChild(boldedText);
111
+ } else {
112
+ descriptionText.appendChild(document.createTextNode(segments[i]));
113
+ }
114
+ }
115
+ }
116
+ });
117
+
118
+ return {
119
+ container,
120
+ input,
121
+ selectedFiles,
122
+ addTo: (parent: HTMLElement) => {
123
+ parent.appendChild(container);
124
+ },
125
+ };
126
+ };
127
+
128
+ export default makeFileInput;