js-draw 0.1.6 → 0.1.7

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 (133) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +2 -2
  4. package/dist/bundle.js +1 -1
  5. package/dist/src/Color4.js +6 -2
  6. package/dist/src/Editor.d.ts +1 -0
  7. package/dist/src/Editor.js +19 -8
  8. package/dist/src/EditorImage.d.ts +8 -13
  9. package/dist/src/EditorImage.js +51 -29
  10. package/dist/src/Viewport.d.ts +9 -1
  11. package/dist/src/Viewport.js +3 -1
  12. package/dist/src/commands/Command.d.ts +9 -8
  13. package/dist/src/commands/Command.js +15 -14
  14. package/dist/src/commands/Duplicate.d.ts +14 -0
  15. package/dist/src/commands/Duplicate.js +34 -0
  16. package/dist/src/commands/Erase.d.ts +5 -2
  17. package/dist/src/commands/Erase.js +28 -9
  18. package/dist/src/commands/SerializableCommand.d.ts +13 -0
  19. package/dist/src/commands/SerializableCommand.js +28 -0
  20. package/dist/src/commands/localization.d.ts +2 -0
  21. package/dist/src/commands/localization.js +2 -0
  22. package/dist/src/components/AbstractComponent.d.ts +15 -2
  23. package/dist/src/components/AbstractComponent.js +122 -26
  24. package/dist/src/components/SVGGlobalAttributesObject.d.ts +6 -1
  25. package/dist/src/components/SVGGlobalAttributesObject.js +23 -1
  26. package/dist/src/components/Stroke.d.ts +5 -0
  27. package/dist/src/components/Stroke.js +32 -1
  28. package/dist/src/components/Text.d.ts +11 -4
  29. package/dist/src/components/Text.js +57 -3
  30. package/dist/src/components/UnknownSVGObject.d.ts +2 -0
  31. package/dist/src/components/UnknownSVGObject.js +12 -1
  32. package/dist/src/components/util/describeComponentList.d.ts +4 -0
  33. package/dist/src/components/util/describeComponentList.js +14 -0
  34. package/dist/src/geometry/Path.d.ts +4 -1
  35. package/dist/src/geometry/Path.js +4 -0
  36. package/dist/src/rendering/Display.d.ts +3 -0
  37. package/dist/src/rendering/Display.js +13 -0
  38. package/dist/src/rendering/RenderingStyle.d.ts +24 -0
  39. package/dist/src/rendering/RenderingStyle.js +32 -0
  40. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -8
  41. package/dist/src/rendering/renderers/AbstractRenderer.js +1 -6
  42. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  43. package/dist/src/rendering/renderers/DummyRenderer.d.ts +2 -1
  44. package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -1
  45. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +2 -1
  46. package/dist/src/toolbar/HTMLToolbar.d.ts +1 -1
  47. package/dist/src/toolbar/HTMLToolbar.js +52 -534
  48. package/dist/src/toolbar/icons.d.ts +5 -0
  49. package/dist/src/toolbar/icons.js +186 -13
  50. package/dist/src/toolbar/localization.d.ts +4 -0
  51. package/dist/src/toolbar/localization.js +4 -0
  52. package/dist/src/toolbar/makeColorInput.d.ts +5 -0
  53. package/dist/src/toolbar/makeColorInput.js +81 -0
  54. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +12 -0
  55. package/dist/src/toolbar/widgets/BaseToolWidget.js +44 -0
  56. package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -0
  57. package/dist/src/toolbar/widgets/BaseWidget.js +148 -0
  58. package/dist/src/toolbar/widgets/EraserWidget.d.ts +6 -0
  59. package/dist/src/toolbar/widgets/EraserWidget.js +14 -0
  60. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +13 -0
  61. package/dist/src/toolbar/widgets/HandToolWidget.js +133 -0
  62. package/dist/src/toolbar/widgets/PenWidget.d.ts +20 -0
  63. package/dist/src/toolbar/widgets/PenWidget.js +131 -0
  64. package/dist/src/toolbar/widgets/SelectionWidget.d.ts +11 -0
  65. package/dist/src/toolbar/widgets/SelectionWidget.js +56 -0
  66. package/dist/src/toolbar/widgets/TextToolWidget.d.ts +13 -0
  67. package/dist/src/toolbar/widgets/TextToolWidget.js +72 -0
  68. package/dist/src/tools/Pen.js +1 -1
  69. package/dist/src/tools/PipetteTool.d.ts +20 -0
  70. package/dist/src/tools/PipetteTool.js +40 -0
  71. package/dist/src/tools/SelectionTool.d.ts +2 -0
  72. package/dist/src/tools/SelectionTool.js +41 -23
  73. package/dist/src/tools/TextTool.js +1 -1
  74. package/dist/src/tools/ToolController.d.ts +3 -1
  75. package/dist/src/tools/ToolController.js +4 -0
  76. package/dist/src/tools/localization.d.ts +2 -1
  77. package/dist/src/tools/localization.js +3 -2
  78. package/dist/src/types.d.ts +7 -2
  79. package/dist/src/types.js +1 -0
  80. package/jest.config.js +2 -0
  81. package/package.json +6 -6
  82. package/src/Color4.ts +9 -3
  83. package/src/Editor.ts +23 -11
  84. package/src/EditorImage.test.ts +4 -4
  85. package/src/EditorImage.ts +61 -20
  86. package/src/SVGLoader.ts +2 -1
  87. package/src/Viewport.ts +2 -1
  88. package/src/commands/Command.ts +21 -19
  89. package/src/commands/Duplicate.ts +49 -0
  90. package/src/commands/Erase.ts +34 -13
  91. package/src/commands/SerializableCommand.ts +41 -0
  92. package/src/commands/localization.ts +5 -0
  93. package/src/components/AbstractComponent.ts +168 -26
  94. package/src/components/SVGGlobalAttributesObject.ts +34 -2
  95. package/src/components/Stroke.test.ts +53 -0
  96. package/src/components/Stroke.ts +37 -2
  97. package/src/components/Text.test.ts +38 -0
  98. package/src/components/Text.ts +80 -5
  99. package/src/components/UnknownSVGObject.test.ts +10 -0
  100. package/src/components/UnknownSVGObject.ts +15 -1
  101. package/src/components/builders/FreehandLineBuilder.ts +2 -1
  102. package/src/components/util/describeComponentList.ts +18 -0
  103. package/src/geometry/Path.ts +8 -1
  104. package/src/rendering/Display.ts +17 -1
  105. package/src/rendering/RenderingStyle.test.ts +68 -0
  106. package/src/rendering/RenderingStyle.ts +46 -0
  107. package/src/rendering/caching/RenderingCache.test.ts +1 -1
  108. package/src/rendering/renderers/AbstractRenderer.ts +1 -15
  109. package/src/rendering/renderers/CanvasRenderer.ts +2 -1
  110. package/src/rendering/renderers/DummyRenderer.ts +2 -1
  111. package/src/rendering/renderers/SVGRenderer.ts +2 -1
  112. package/src/rendering/renderers/TextOnlyRenderer.ts +2 -1
  113. package/src/toolbar/HTMLToolbar.ts +58 -660
  114. package/src/toolbar/icons.ts +205 -13
  115. package/src/toolbar/localization.ts +10 -2
  116. package/src/toolbar/makeColorInput.ts +105 -0
  117. package/src/toolbar/toolbar.css +116 -78
  118. package/src/toolbar/widgets/BaseToolWidget.ts +53 -0
  119. package/src/toolbar/widgets/BaseWidget.ts +175 -0
  120. package/src/toolbar/widgets/EraserWidget.ts +16 -0
  121. package/src/toolbar/widgets/HandToolWidget.ts +186 -0
  122. package/src/toolbar/widgets/PenWidget.ts +165 -0
  123. package/src/toolbar/widgets/SelectionWidget.ts +72 -0
  124. package/src/toolbar/widgets/TextToolWidget.ts +90 -0
  125. package/src/tools/Pen.ts +1 -1
  126. package/src/tools/PipetteTool.ts +56 -0
  127. package/src/tools/SelectionTool.test.ts +2 -4
  128. package/src/tools/SelectionTool.ts +47 -27
  129. package/src/tools/TextTool.ts +1 -1
  130. package/src/tools/ToolController.ts +10 -6
  131. package/src/tools/UndoRedoShortcut.test.ts +1 -1
  132. package/src/tools/localization.ts +6 -3
  133. package/src/types.ts +12 -1
@@ -6,635 +6,24 @@ import { coloris, init as colorisInit } from '@melloware/coloris';
6
6
  import Color4 from '../Color4';
7
7
  import Pen from '../tools/Pen';
8
8
  import Eraser from '../tools/Eraser';
9
- import BaseTool from '../tools/BaseTool';
10
9
  import SelectionTool from '../tools/SelectionTool';
11
- import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
12
- import { ComponentBuilderFactory } from '../components/builders/types';
13
- import { makeArrowBuilder } from '../components/builders/ArrowBuilder';
14
- import { makeLineBuilder } from '../components/builders/LineBuilder';
15
- import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../components/builders/RectangleBuilder';
16
10
  import { defaultToolbarLocalization, ToolbarLocalization } from './localization';
17
11
  import { ActionButtonIcon } from './types';
18
- import { makeDropdownIcon, makeEraserIcon, makeIconFromFactory, makePenIcon, makeRedoIcon, makeSelectionIcon, makeHandToolIcon, makeUndoIcon, makeTextIcon } from './icons';
19
- import PanZoom, { PanZoomMode } from '../tools/PanZoom';
20
- import Mat33 from '../geometry/Mat33';
21
- import Viewport from '../Viewport';
12
+ import { makeRedoIcon, makeUndoIcon } from './icons';
13
+ import PanZoom from '../tools/PanZoom';
22
14
  import TextTool from '../tools/TextTool';
15
+ import PenWidget from './widgets/PenWidget';
16
+ import EraserWidget from './widgets/EraserWidget';
17
+ import { SelectionWidget } from './widgets/SelectionWidget';
18
+ import TextToolWidget from './widgets/TextToolWidget';
19
+ import HandToolWidget from './widgets/HandToolWidget';
23
20
 
24
21
 
25
- const toolbarCSSPrefix = 'toolbar-';
22
+ export const toolbarCSSPrefix = 'toolbar-';
26
23
 
27
- abstract class ToolbarWidget {
28
- protected readonly container: HTMLElement;
29
- private button: HTMLElement;
30
- private icon: Element|null;
31
- private dropdownContainer: HTMLElement;
32
- private dropdownIcon: Element;
33
- private label: HTMLLabelElement;
34
- private hasDropdown: boolean;
35
-
36
- public constructor(
37
- protected editor: Editor,
38
- protected targetTool: BaseTool,
39
- protected localizationTable: ToolbarLocalization,
40
- ) {
41
- this.icon = null;
42
- this.container = document.createElement('div');
43
- this.container.classList.add(`${toolbarCSSPrefix}toolContainer`);
44
- this.dropdownContainer = document.createElement('div');
45
- this.dropdownContainer.classList.add(`${toolbarCSSPrefix}dropdown`);
46
- this.dropdownContainer.classList.add('hidden');
47
- this.hasDropdown = false;
48
-
49
- this.button = document.createElement('div');
50
- this.button.classList.add(`${toolbarCSSPrefix}button`);
51
- this.label = document.createElement('label');
52
- this.button.setAttribute('role', 'button');
53
- this.button.tabIndex = 0;
54
-
55
- editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
56
- if (toolEvt.kind !== EditorEventType.ToolEnabled) {
57
- throw new Error('Incorrect event type! (Expected ToolEnabled)');
58
- }
59
-
60
- if (toolEvt.tool === targetTool) {
61
- this.updateSelected(true);
62
- }
63
- });
64
-
65
- editor.notifier.on(EditorEventType.ToolDisabled, toolEvt => {
66
- if (toolEvt.kind !== EditorEventType.ToolDisabled) {
67
- throw new Error('Incorrect event type! (Expected ToolDisabled)');
68
- }
69
-
70
- if (toolEvt.tool === targetTool) {
71
- this.updateSelected(false);
72
- this.setDropdownVisible(false);
73
- }
74
- });
75
- }
76
-
77
- protected abstract getTitle(): string;
78
- protected abstract createIcon(): Element;
79
-
80
- // Add content to the widget's associated dropdown menu.
81
- // Returns true if such a menu should be created, false otherwise.
82
- protected abstract fillDropdown(dropdown: HTMLElement): boolean;
83
-
84
- protected setupActionBtnClickListener(button: HTMLElement) {
85
- button.onclick = () => {
86
- this.handleClick();
87
- };
88
- }
89
-
90
- protected handleClick() {
91
- if (this.hasDropdown) {
92
- if (!this.targetTool.isEnabled()) {
93
- this.targetTool.setEnabled(true);
94
- } else {
95
- this.setDropdownVisible(!this.isDropdownVisible());
96
- }
97
- } else {
98
- this.targetTool.setEnabled(!this.targetTool.isEnabled());
99
- }
100
- }
101
-
102
- // Adds this to [parent]. This can only be called once for each ToolbarWidget.
103
- public addTo(parent: HTMLElement) {
104
- this.label.innerText = this.getTitle();
105
-
106
- this.setupActionBtnClickListener(this.button);
107
-
108
- this.icon = null;
109
- this.updateIcon();
110
-
111
- this.updateSelected(this.targetTool.isEnabled());
112
-
113
- this.button.replaceChildren(this.icon!, this.label);
114
- this.container.appendChild(this.button);
115
-
116
- this.hasDropdown = this.fillDropdown(this.dropdownContainer);
117
- if (this.hasDropdown) {
118
- this.dropdownIcon = this.createDropdownIcon();
119
- this.button.appendChild(this.dropdownIcon);
120
- this.container.appendChild(this.dropdownContainer);
121
- }
122
-
123
- this.setDropdownVisible(false);
124
- parent.appendChild(this.container);
125
- }
126
-
127
- protected updateIcon() {
128
- const newIcon = this.createIcon();
129
- this.icon?.replaceWith(newIcon);
130
- this.icon = newIcon;
131
- this.icon.classList.add(`${toolbarCSSPrefix}icon`);
132
- }
133
-
134
- protected updateSelected(selected: boolean) {
135
- const currentlySelected = this.container.classList.contains('selected');
136
- if (currentlySelected === selected) {
137
- return;
138
- }
139
-
140
- if (selected) {
141
- this.container.classList.add('selected');
142
- this.button.ariaSelected = 'true';
143
- } else {
144
- this.container.classList.remove('selected');
145
- this.button.ariaSelected = 'false';
146
- }
147
- }
148
-
149
- protected setDropdownVisible(visible: boolean) {
150
- const currentlyVisible = this.container.classList.contains('dropdownVisible');
151
- if (currentlyVisible === visible) {
152
- return;
153
- }
154
-
155
- if (visible) {
156
- this.dropdownContainer.classList.remove('hidden');
157
- this.container.classList.add('dropdownVisible');
158
- this.editor.announceForAccessibility(
159
- this.localizationTable.dropdownShown(this.targetTool.description)
160
- );
161
- } else {
162
- this.dropdownContainer.classList.add('hidden');
163
- this.container.classList.remove('dropdownVisible');
164
- this.editor.announceForAccessibility(
165
- this.localizationTable.dropdownHidden(this.targetTool.description)
166
- );
167
- }
168
-
169
- this.repositionDropdown();
170
- }
171
-
172
- protected repositionDropdown() {
173
- const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
174
- const screenWidth = document.body.clientWidth;
175
-
176
- if (dropdownBBox.left > screenWidth / 2) {
177
- this.dropdownContainer.style.marginLeft = this.button.clientWidth + 'px';
178
- this.dropdownContainer.style.transform = 'translate(-100%, 0)';
179
- } else {
180
- this.dropdownContainer.style.marginLeft = '';
181
- this.dropdownContainer.style.transform = '';
182
- }
183
- }
184
-
185
- protected isDropdownVisible(): boolean {
186
- return !this.dropdownContainer.classList.contains('hidden');
187
- }
188
-
189
- private createDropdownIcon(): Element {
190
- const icon = makeDropdownIcon();
191
- icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
192
- return icon;
193
- }
194
- }
195
-
196
- class EraserWidget extends ToolbarWidget {
197
- protected getTitle(): string {
198
- return this.localizationTable.eraser;
199
- }
200
- protected createIcon(): Element {
201
- return makeEraserIcon();
202
- }
203
-
204
- protected fillDropdown(_dropdown: HTMLElement): boolean {
205
- // No dropdown associated with the eraser
206
- return false;
207
- }
208
- }
209
-
210
- class SelectionWidget extends ToolbarWidget {
211
- public constructor(
212
- editor: Editor, private tool: SelectionTool, localization: ToolbarLocalization
213
- ) {
214
- super(editor, tool, localization);
215
- }
216
-
217
- protected getTitle(): string {
218
- return this.localizationTable.select;
219
- }
220
-
221
- protected createIcon(): Element {
222
- return makeSelectionIcon();
223
- }
224
-
225
- protected fillDropdown(dropdown: HTMLElement): boolean {
226
- const container = document.createElement('div');
227
- const resizeButton = document.createElement('button');
228
- const deleteButton = document.createElement('button');
229
-
230
- resizeButton.innerText = this.localizationTable.resizeImageToSelection;
231
- resizeButton.disabled = true;
232
- deleteButton.innerText = this.localizationTable.deleteSelection;
233
- deleteButton.disabled = true;
234
-
235
- resizeButton.onclick = () => {
236
- const selection = this.tool.getSelection();
237
- this.editor.dispatch(this.editor.setImportExportRect(selection!.region));
238
- };
239
-
240
- deleteButton.onclick = () => {
241
- const selection = this.tool.getSelection();
242
- this.editor.dispatch(selection!.deleteSelectedObjects());
243
- this.tool.clearSelection();
244
- };
245
-
246
- // Enable/disable actions based on whether items are selected
247
- this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
248
- if (toolEvt.kind !== EditorEventType.ToolUpdated) {
249
- throw new Error('Invalid event type!');
250
- }
251
-
252
- if (toolEvt.tool === this.tool) {
253
- const selection = this.tool.getSelection();
254
- const hasSelection = selection && selection.region.area > 0;
255
-
256
- resizeButton.disabled = !hasSelection;
257
- deleteButton.disabled = resizeButton.disabled;
258
- }
259
- });
260
-
261
- container.replaceChildren(resizeButton, deleteButton);
262
- dropdown.appendChild(container);
263
- return true;
264
- }
265
- }
266
-
267
- const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor) => {
268
- const zoomLevelRow = document.createElement('div');
269
-
270
- const increaseButton = document.createElement('button');
271
- const decreaseButton = document.createElement('button');
272
- const zoomLevelDisplay = document.createElement('span');
273
- increaseButton.innerText = '+';
274
- decreaseButton.innerText = '-';
275
- zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton);
276
-
277
- zoomLevelRow.classList.add(`${toolbarCSSPrefix}zoomLevelEditor`);
278
- zoomLevelDisplay.classList.add('zoomDisplay');
279
-
280
- let lastZoom: number|undefined;
281
- const updateZoomDisplay = () => {
282
- let zoomLevel = editor.viewport.getScaleFactor() * 100;
283
-
284
- if (zoomLevel > 0.1) {
285
- zoomLevel = Math.round(zoomLevel * 10) / 10;
286
- } else {
287
- zoomLevel = Math.round(zoomLevel * 1000) / 1000;
288
- }
289
-
290
- if (zoomLevel !== lastZoom) {
291
- zoomLevelDisplay.innerText = localizationTable.zoomLevel(zoomLevel);
292
- lastZoom = zoomLevel;
293
- }
294
- };
295
- updateZoomDisplay();
296
-
297
- editor.notifier.on(EditorEventType.ViewportChanged, (event) => {
298
- if (event.kind === EditorEventType.ViewportChanged) {
299
- updateZoomDisplay();
300
- }
301
- });
302
-
303
- const zoomBy = (factor: number) => {
304
- const screenCenter = editor.viewport.visibleRect.center;
305
- const transformUpdate = Mat33.scaling2D(factor, screenCenter);
306
- editor.dispatch(new Viewport.ViewportTransform(transformUpdate), false);
307
- };
308
-
309
- increaseButton.onclick = () => {
310
- zoomBy(5.0/4);
311
- };
312
-
313
- decreaseButton.onclick = () => {
314
- zoomBy(4.0/5);
315
- };
316
-
317
- return zoomLevelRow;
318
- };
319
-
320
- class HandToolWidget extends ToolbarWidget {
321
- public constructor(
322
- editor: Editor, protected tool: PanZoom, localizationTable: ToolbarLocalization
323
- ) {
324
- super(editor, tool, localizationTable);
325
- this.container.classList.add('dropdownShowable');
326
- }
327
- protected getTitle(): string {
328
- return this.localizationTable.handTool;
329
- }
330
-
331
- protected createIcon(): Element {
332
- return makeHandToolIcon();
333
- }
334
-
335
- protected fillDropdown(dropdown: HTMLElement): boolean {
336
- type OnToggle = (checked: boolean)=>void;
337
- let idCounter = 0;
338
- const addCheckbox = (label: string, onToggle: OnToggle) => {
339
- const rowContainer = document.createElement('div');
340
- const labelElem = document.createElement('label');
341
- const checkboxElem = document.createElement('input');
342
-
343
- checkboxElem.type = 'checkbox';
344
- checkboxElem.id = `${toolbarCSSPrefix}hand-tool-option-${idCounter++}`;
345
- labelElem.setAttribute('for', checkboxElem.id);
346
-
347
- checkboxElem.oninput = () => {
348
- onToggle(checkboxElem.checked);
349
- };
350
- labelElem.innerText = label;
351
-
352
- rowContainer.replaceChildren(checkboxElem, labelElem);
353
- dropdown.appendChild(rowContainer);
354
-
355
- return checkboxElem;
356
- };
357
-
358
- const setModeFlag = (enabled: boolean, flag: PanZoomMode) => {
359
- const mode = this.tool.getMode();
360
- if (enabled) {
361
- this.tool.setMode(mode | flag);
362
- } else {
363
- this.tool.setMode(mode & ~flag);
364
- }
365
- };
366
-
367
- const touchPanningCheckbox = addCheckbox(this.localizationTable.touchPanning, checked => {
368
- setModeFlag(checked, PanZoomMode.OneFingerTouchGestures);
369
- });
370
-
371
- const anyDevicePanningCheckbox = addCheckbox(this.localizationTable.anyDevicePanning, checked => {
372
- setModeFlag(checked, PanZoomMode.SinglePointerGestures);
373
- });
374
-
375
- dropdown.appendChild(makeZoomControl(this.localizationTable, this.editor));
376
-
377
- const updateInputs = () => {
378
- const mode = this.tool.getMode();
379
- anyDevicePanningCheckbox.checked = !!(mode & PanZoomMode.SinglePointerGestures);
380
- if (anyDevicePanningCheckbox.checked) {
381
- touchPanningCheckbox.checked = true;
382
- touchPanningCheckbox.disabled = true;
383
- } else {
384
- touchPanningCheckbox.checked = !!(mode & PanZoomMode.OneFingerTouchGestures);
385
- touchPanningCheckbox.disabled = false;
386
- }
387
- };
388
-
389
- updateInputs();
390
- this.editor.notifier.on(EditorEventType.ToolUpdated, event => {
391
- if (event.kind === EditorEventType.ToolUpdated && event.tool === this.tool) {
392
- updateInputs();
393
- }
394
- });
395
-
396
- return true;
397
- }
398
-
399
- protected updateSelected(_active: boolean) {
400
- }
401
-
402
- protected handleClick() {
403
- this.setDropdownVisible(!this.isDropdownVisible());
404
- }
405
- }
406
-
407
- class TextToolWidget extends ToolbarWidget {
408
- private updateDropdownInputs: (()=>void)|null = null;
409
- public constructor(editor: Editor, private tool: TextTool, localization: ToolbarLocalization) {
410
- super(editor, tool, localization);
411
-
412
- editor.notifier.on(EditorEventType.ToolUpdated, evt => {
413
- if (evt.kind === EditorEventType.ToolUpdated && evt.tool === tool) {
414
- this.updateIcon();
415
- this.updateDropdownInputs?.();
416
- }
417
- });
418
- }
419
-
420
- protected getTitle(): string {
421
- return this.targetTool.description;
422
- }
423
-
424
- protected createIcon(): Element {
425
- const textStyle = this.tool.getTextStyle();
426
- return makeTextIcon(textStyle);
427
- }
428
-
429
- private static idCounter: number = 0;
430
- protected fillDropdown(dropdown: HTMLElement): boolean {
431
- const fontRow = document.createElement('div');
432
- const colorRow = document.createElement('div');
433
-
434
- const fontInput = document.createElement('select');
435
- const fontLabel = document.createElement('label');
436
-
437
- const colorInput = document.createElement('input');
438
- const colorLabel = document.createElement('label');
439
-
440
- const fontsInInput = new Set();
441
- const addFontToInput = (fontName: string) => {
442
- const option = document.createElement('option');
443
- option.value = fontName;
444
- option.textContent = fontName;
445
- fontInput.appendChild(option);
446
- fontsInInput.add(fontName);
447
- };
448
-
449
- fontLabel.innerText = this.localizationTable.fontLabel;
450
- colorLabel.innerText = this.localizationTable.colorLabel;
451
-
452
- colorInput.classList.add('coloris_input');
453
- colorInput.type = 'button';
454
- colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
455
- colorLabel.setAttribute('for', colorInput.id);
456
-
457
- addFontToInput('monospace');
458
- addFontToInput('serif');
459
- addFontToInput('sans-serif');
460
- fontInput.id = `${toolbarCSSPrefix}-text-font-input-${TextToolWidget.idCounter++}`;
461
- fontLabel.setAttribute('for', fontInput.id);
462
-
463
- fontInput.onchange = () => {
464
- this.tool.setFontFamily(fontInput.value);
465
- };
466
-
467
- colorInput.oninput = () => {
468
- this.tool.setColor(Color4.fromString(colorInput.value));
469
- };
470
-
471
- colorRow.appendChild(colorLabel);
472
- colorRow.appendChild(colorInput);
473
-
474
- fontRow.appendChild(fontLabel);
475
- fontRow.appendChild(fontInput);
476
-
477
- this.updateDropdownInputs = () => {
478
- const style = this.tool.getTextStyle();
479
- colorInput.value = style.renderingStyle.fill.toHexString();
480
-
481
- if (!fontsInInput.has(style.fontFamily)) {
482
- addFontToInput(style.fontFamily);
483
- }
484
- fontInput.value = style.fontFamily;
485
- };
486
- this.updateDropdownInputs();
487
-
488
- dropdown.replaceChildren(colorRow, fontRow);
489
- return true;
490
- }
491
- }
492
-
493
- class PenWidget extends ToolbarWidget {
494
- private updateInputs: ()=> void = () => {};
495
-
496
- public constructor(
497
- editor: Editor, private tool: Pen, localization: ToolbarLocalization, private penTypes: PenTypeRecord[]
498
- ) {
499
- super(editor, tool, localization);
500
-
501
- this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
502
- if (toolEvt.kind !== EditorEventType.ToolUpdated) {
503
- throw new Error('Invalid event type!');
504
- }
505
-
506
- // The button icon may depend on tool properties.
507
- if (toolEvt.tool === this.tool) {
508
- this.updateIcon();
509
- this.updateInputs();
510
- }
511
- });
512
- }
513
-
514
- protected getTitle(): string {
515
- return this.targetTool.description;
516
- }
517
-
518
- protected createIcon(): Element {
519
- const strokeFactory = this.tool.getStrokeFactory();
520
- if (strokeFactory === makeFreehandLineBuilder) {
521
- // Use a square-root scale to prevent the pen's tip from overflowing.
522
- const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
523
- const color = this.tool.getColor();
524
- return makePenIcon(scale, color.toHexString());
525
- } else {
526
- const strokeFactory = this.tool.getStrokeFactory();
527
- return makeIconFromFactory(this.tool, strokeFactory);
528
- }
529
- }
530
-
531
- private static idCounter: number = 0;
532
- protected fillDropdown(dropdown: HTMLElement): boolean {
533
- const container = document.createElement('div');
534
-
535
- const thicknessRow = document.createElement('div');
536
- const objectTypeRow = document.createElement('div');
537
-
538
- // Thickness: Value of the input is squared to allow for finer control/larger values.
539
- const thicknessLabel = document.createElement('label');
540
- const thicknessInput = document.createElement('input');
541
- const objectSelectLabel = document.createElement('label');
542
- const objectTypeSelect = document.createElement('select');
543
-
544
- // Give inputs IDs so we can label them with a <label for=...>Label text</label>
545
- thicknessInput.id = `${toolbarCSSPrefix}thicknessInput${PenWidget.idCounter++}`;
546
- objectTypeSelect.id = `${toolbarCSSPrefix}builderSelect${PenWidget.idCounter++}`;
547
-
548
- thicknessLabel.innerText = this.localizationTable.thicknessLabel;
549
- thicknessLabel.setAttribute('for', thicknessInput.id);
550
- objectSelectLabel.innerText = this.localizationTable.selectObjectType;
551
- objectSelectLabel.setAttribute('for', objectTypeSelect.id);
552
-
553
- thicknessInput.type = 'range';
554
- thicknessInput.min = '1';
555
- thicknessInput.max = '20';
556
- thicknessInput.step = '1';
557
- thicknessInput.oninput = () => {
558
- this.tool.setThickness(parseFloat(thicknessInput.value) ** 2);
559
- };
560
- thicknessRow.appendChild(thicknessLabel);
561
- thicknessRow.appendChild(thicknessInput);
562
-
563
- objectTypeSelect.oninput = () => {
564
- const penTypeIdx = parseInt(objectTypeSelect.value);
565
- if (penTypeIdx < 0 || penTypeIdx >= this.penTypes.length) {
566
- console.error('Invalid pen type index', penTypeIdx);
567
- return;
568
- }
569
-
570
- this.tool.setStrokeFactory(this.penTypes[penTypeIdx].factory);
571
- };
572
- objectTypeRow.appendChild(objectSelectLabel);
573
- objectTypeRow.appendChild(objectTypeSelect);
574
-
575
- const colorRow = document.createElement('div');
576
- const colorLabel = document.createElement('label');
577
- const colorInput = document.createElement('input');
578
-
579
- colorInput.id = `${toolbarCSSPrefix}colorInput${PenWidget.idCounter++}`;
580
- colorLabel.innerText = this.localizationTable.colorLabel;
581
- colorLabel.setAttribute('for', colorInput.id);
582
-
583
- colorInput.className = 'coloris_input';
584
- colorInput.type = 'button';
585
- colorInput.oninput = () => {
586
- this.tool.setColor(Color4.fromHex(colorInput.value));
587
- };
588
- colorInput.addEventListener('open', () => {
589
- this.editor.notifier.dispatch(EditorEventType.ColorPickerToggled, {
590
- kind: EditorEventType.ColorPickerToggled,
591
- open: true,
592
- });
593
- });
594
- colorInput.addEventListener('close', () => {
595
- this.editor.notifier.dispatch(EditorEventType.ColorPickerToggled, {
596
- kind: EditorEventType.ColorPickerToggled,
597
- open: false,
598
- });
599
- });
600
-
601
- colorRow.appendChild(colorLabel);
602
- colorRow.appendChild(colorInput);
603
-
604
- this.updateInputs = () => {
605
- colorInput.value = this.tool.getColor().toHexString();
606
- thicknessInput.value = Math.sqrt(this.tool.getThickness()).toString();
607
-
608
- objectTypeSelect.replaceChildren();
609
- for (let i = 0; i < this.penTypes.length; i ++) {
610
- const penType = this.penTypes[i];
611
- const option = document.createElement('option');
612
- option.value = i.toString();
613
- option.innerText = penType.name;
614
-
615
- objectTypeSelect.appendChild(option);
616
-
617
- if (penType.factory === this.tool.getStrokeFactory()) {
618
- objectTypeSelect.value = i.toString();
619
- }
620
- }
621
- };
622
- this.updateInputs();
623
-
624
- container.replaceChildren(colorRow, thicknessRow, objectTypeRow);
625
- dropdown.replaceChildren(container);
626
- return true;
627
- }
628
- }
629
-
630
- interface PenTypeRecord {
631
- name: string;
632
- factory: ComponentBuilderFactory;
633
- }
634
24
 
635
25
  export default class HTMLToolbar {
636
26
  private container: HTMLElement;
637
- private penTypes: PenTypeRecord[];
638
27
 
639
28
  public constructor(
640
29
  private editor: Editor, parent: HTMLElement,
@@ -647,30 +36,6 @@ export default class HTMLToolbar {
647
36
 
648
37
  colorisInit();
649
38
  this.setupColorPickers();
650
-
651
- // Default pen types
652
- this.penTypes = [
653
- {
654
- name: localizationTable.freehandPen,
655
- factory: makeFreehandLineBuilder,
656
- },
657
- {
658
- name: localizationTable.arrowPen,
659
- factory: makeArrowBuilder,
660
- },
661
- {
662
- name: localizationTable.linePen,
663
- factory: makeLineBuilder,
664
- },
665
- {
666
- name: localizationTable.filledRectanglePen,
667
- factory: makeFilledRectangleBuilder,
668
- },
669
- {
670
- name: localizationTable.outlinedRectanglePen,
671
- factory: makeOutlinedRectangleBuilder,
672
- },
673
- ];
674
39
  }
675
40
 
676
41
  public setupColorPickers() {
@@ -678,22 +43,48 @@ export default class HTMLToolbar {
678
43
  closePickerOverlay.className = `${toolbarCSSPrefix}closeColorPickerOverlay`;
679
44
  this.editor.createHTMLOverlay(closePickerOverlay);
680
45
 
681
- coloris({
682
- el: '.coloris_input',
683
- format: 'hex',
684
- selectInput: false,
685
- focusInput: false,
686
- themeMode: 'auto',
687
-
688
- swatches: [
689
- Color4.red.toHexString(),
690
- Color4.purple.toHexString(),
691
- Color4.blue.toHexString(),
692
- Color4.clay.toHexString(),
693
- Color4.black.toHexString(),
694
- Color4.white.toHexString(),
695
- ],
696
- });
46
+ const maxSwatchLen = 12;
47
+ const swatches = [
48
+ Color4.red.toHexString(),
49
+ Color4.purple.toHexString(),
50
+ Color4.blue.toHexString(),
51
+ Color4.clay.toHexString(),
52
+ Color4.black.toHexString(),
53
+ Color4.white.toHexString(),
54
+ ];
55
+ const presetColorEnd = swatches.length;
56
+
57
+ // (Re)init Coloris -- update the swatches list.
58
+ const initColoris = () => {
59
+ coloris({
60
+ el: '.coloris_input',
61
+ format: 'hex',
62
+ selectInput: false,
63
+ focusInput: false,
64
+ themeMode: 'auto',
65
+
66
+ swatches
67
+ });
68
+ };
69
+ initColoris();
70
+
71
+ const addColorToSwatch = (newColor: string) => {
72
+ let alreadyPresent = false;
73
+
74
+ for (const color of swatches) {
75
+ if (color === newColor) {
76
+ alreadyPresent = true;
77
+ }
78
+ }
79
+
80
+ if (!alreadyPresent) {
81
+ swatches.push(newColor);
82
+ if (swatches.length > maxSwatchLen) {
83
+ swatches.splice(presetColorEnd, 1);
84
+ }
85
+ initColoris();
86
+ }
87
+ };
697
88
 
698
89
  this.editor.notifier.on(EditorEventType.ColorPickerToggled, event => {
699
90
  if (event.kind !== EditorEventType.ColorPickerToggled) {
@@ -704,6 +95,13 @@ export default class HTMLToolbar {
704
95
  // on that shows/hides the color picker.
705
96
  closePickerOverlay.style.display = event.open ? 'block' : 'none';
706
97
  });
98
+
99
+ // Add newly-selected colors to the swatch.
100
+ this.editor.notifier.on(EditorEventType.ColorPickerColorSelected, event => {
101
+ if (event.kind === EditorEventType.ColorPickerColorSelected) {
102
+ addColorToSwatch(event.color.toHexString());
103
+ }
104
+ });
707
105
  }
708
106
 
709
107
  public addActionButton(title: string|ActionButtonIcon, command: ()=> void, parent?: Element) {
@@ -768,7 +166,7 @@ export default class HTMLToolbar {
768
166
  }
769
167
 
770
168
  const widget = new PenWidget(
771
- this.editor, tool, this.localizationTable, this.penTypes,
169
+ this.editor, tool, this.localizationTable,
772
170
  );
773
171
  widget.addTo(this.container);
774
172
  }