js-draw 0.1.5 → 0.1.8

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 (139) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +16 -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 +24 -9
  8. package/dist/src/EditorImage.d.ts +8 -13
  9. package/dist/src/EditorImage.js +51 -29
  10. package/dist/src/SVGLoader.js +6 -2
  11. package/dist/src/Viewport.d.ts +10 -2
  12. package/dist/src/Viewport.js +8 -6
  13. package/dist/src/commands/Command.d.ts +9 -8
  14. package/dist/src/commands/Command.js +15 -14
  15. package/dist/src/commands/Duplicate.d.ts +14 -0
  16. package/dist/src/commands/Duplicate.js +34 -0
  17. package/dist/src/commands/Erase.d.ts +5 -2
  18. package/dist/src/commands/Erase.js +28 -9
  19. package/dist/src/commands/SerializableCommand.d.ts +13 -0
  20. package/dist/src/commands/SerializableCommand.js +28 -0
  21. package/dist/src/commands/localization.d.ts +2 -0
  22. package/dist/src/commands/localization.js +2 -0
  23. package/dist/src/components/AbstractComponent.d.ts +15 -2
  24. package/dist/src/components/AbstractComponent.js +122 -26
  25. package/dist/src/components/SVGGlobalAttributesObject.d.ts +6 -1
  26. package/dist/src/components/SVGGlobalAttributesObject.js +23 -1
  27. package/dist/src/components/Stroke.d.ts +5 -0
  28. package/dist/src/components/Stroke.js +32 -1
  29. package/dist/src/components/Text.d.ts +11 -4
  30. package/dist/src/components/Text.js +57 -3
  31. package/dist/src/components/UnknownSVGObject.d.ts +2 -0
  32. package/dist/src/components/UnknownSVGObject.js +12 -1
  33. package/dist/src/components/builders/RectangleBuilder.d.ts +3 -1
  34. package/dist/src/components/builders/RectangleBuilder.js +17 -8
  35. package/dist/src/components/util/describeComponentList.d.ts +4 -0
  36. package/dist/src/components/util/describeComponentList.js +14 -0
  37. package/dist/src/geometry/Path.d.ts +4 -1
  38. package/dist/src/geometry/Path.js +4 -0
  39. package/dist/src/rendering/Display.d.ts +3 -0
  40. package/dist/src/rendering/Display.js +13 -0
  41. package/dist/src/rendering/RenderingStyle.d.ts +24 -0
  42. package/dist/src/rendering/RenderingStyle.js +32 -0
  43. package/dist/src/rendering/caching/RenderingCacheNode.js +5 -1
  44. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -8
  45. package/dist/src/rendering/renderers/AbstractRenderer.js +1 -6
  46. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  47. package/dist/src/rendering/renderers/DummyRenderer.d.ts +2 -1
  48. package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -1
  49. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +2 -1
  50. package/dist/src/toolbar/HTMLToolbar.d.ts +1 -1
  51. package/dist/src/toolbar/HTMLToolbar.js +52 -534
  52. package/dist/src/toolbar/icons.d.ts +5 -0
  53. package/dist/src/toolbar/icons.js +186 -13
  54. package/dist/src/toolbar/localization.d.ts +4 -0
  55. package/dist/src/toolbar/localization.js +4 -0
  56. package/dist/src/toolbar/makeColorInput.d.ts +5 -0
  57. package/dist/src/toolbar/makeColorInput.js +81 -0
  58. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +12 -0
  59. package/dist/src/toolbar/widgets/BaseToolWidget.js +44 -0
  60. package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -0
  61. package/dist/src/toolbar/widgets/BaseWidget.js +148 -0
  62. package/dist/src/toolbar/widgets/EraserWidget.d.ts +6 -0
  63. package/dist/src/toolbar/widgets/EraserWidget.js +14 -0
  64. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +13 -0
  65. package/dist/src/toolbar/widgets/HandToolWidget.js +133 -0
  66. package/dist/src/toolbar/widgets/PenWidget.d.ts +20 -0
  67. package/dist/src/toolbar/widgets/PenWidget.js +131 -0
  68. package/dist/src/toolbar/widgets/SelectionWidget.d.ts +11 -0
  69. package/dist/src/toolbar/widgets/SelectionWidget.js +56 -0
  70. package/dist/src/toolbar/widgets/TextToolWidget.d.ts +13 -0
  71. package/dist/src/toolbar/widgets/TextToolWidget.js +72 -0
  72. package/dist/src/tools/Pen.js +1 -1
  73. package/dist/src/tools/PipetteTool.d.ts +20 -0
  74. package/dist/src/tools/PipetteTool.js +40 -0
  75. package/dist/src/tools/SelectionTool.d.ts +2 -0
  76. package/dist/src/tools/SelectionTool.js +41 -23
  77. package/dist/src/tools/TextTool.js +1 -1
  78. package/dist/src/tools/ToolController.d.ts +3 -1
  79. package/dist/src/tools/ToolController.js +4 -0
  80. package/dist/src/tools/localization.d.ts +2 -1
  81. package/dist/src/tools/localization.js +3 -2
  82. package/dist/src/types.d.ts +7 -2
  83. package/dist/src/types.js +1 -0
  84. package/jest.config.js +2 -0
  85. package/package.json +6 -6
  86. package/src/Color4.ts +9 -3
  87. package/src/Editor.ts +29 -12
  88. package/src/EditorImage.test.ts +5 -5
  89. package/src/EditorImage.ts +61 -20
  90. package/src/SVGLoader.ts +9 -3
  91. package/src/Viewport.ts +7 -6
  92. package/src/commands/Command.ts +21 -19
  93. package/src/commands/Duplicate.ts +49 -0
  94. package/src/commands/Erase.ts +34 -13
  95. package/src/commands/SerializableCommand.ts +41 -0
  96. package/src/commands/localization.ts +5 -0
  97. package/src/components/AbstractComponent.ts +168 -26
  98. package/src/components/SVGGlobalAttributesObject.ts +34 -2
  99. package/src/components/Stroke.test.ts +53 -0
  100. package/src/components/Stroke.ts +37 -2
  101. package/src/components/Text.test.ts +38 -0
  102. package/src/components/Text.ts +80 -5
  103. package/src/components/UnknownSVGObject.test.ts +10 -0
  104. package/src/components/UnknownSVGObject.ts +15 -1
  105. package/src/components/builders/FreehandLineBuilder.ts +2 -1
  106. package/src/components/builders/RectangleBuilder.ts +23 -8
  107. package/src/components/util/describeComponentList.ts +18 -0
  108. package/src/geometry/Path.ts +8 -1
  109. package/src/rendering/Display.ts +17 -1
  110. package/src/rendering/RenderingStyle.test.ts +68 -0
  111. package/src/rendering/RenderingStyle.ts +46 -0
  112. package/src/rendering/caching/RenderingCache.test.ts +1 -1
  113. package/src/rendering/caching/RenderingCacheNode.ts +6 -1
  114. package/src/rendering/renderers/AbstractRenderer.ts +1 -15
  115. package/src/rendering/renderers/CanvasRenderer.ts +2 -1
  116. package/src/rendering/renderers/DummyRenderer.ts +2 -1
  117. package/src/rendering/renderers/SVGRenderer.ts +2 -1
  118. package/src/rendering/renderers/TextOnlyRenderer.ts +2 -1
  119. package/src/toolbar/HTMLToolbar.ts +58 -660
  120. package/src/toolbar/icons.ts +205 -13
  121. package/src/toolbar/localization.ts +10 -2
  122. package/src/toolbar/makeColorInput.ts +105 -0
  123. package/src/toolbar/toolbar.css +116 -78
  124. package/src/toolbar/widgets/BaseToolWidget.ts +53 -0
  125. package/src/toolbar/widgets/BaseWidget.ts +175 -0
  126. package/src/toolbar/widgets/EraserWidget.ts +16 -0
  127. package/src/toolbar/widgets/HandToolWidget.ts +186 -0
  128. package/src/toolbar/widgets/PenWidget.ts +165 -0
  129. package/src/toolbar/widgets/SelectionWidget.ts +72 -0
  130. package/src/toolbar/widgets/TextToolWidget.ts +90 -0
  131. package/src/tools/Pen.ts +1 -1
  132. package/src/tools/PipetteTool.ts +56 -0
  133. package/src/tools/SelectionTool.test.ts +2 -4
  134. package/src/tools/SelectionTool.ts +47 -27
  135. package/src/tools/TextTool.ts +1 -1
  136. package/src/tools/ToolController.ts +10 -6
  137. package/src/tools/UndoRedoShortcut.test.ts +1 -1
  138. package/src/tools/localization.ts +6 -3
  139. package/src/types.ts +12 -1
@@ -0,0 +1,53 @@
1
+ import Editor from '../../Editor';
2
+ import BaseTool from '../../tools/BaseTool';
3
+ import { EditorEventType } from '../../types';
4
+ import { ToolbarLocalization } from '../localization';
5
+ import BaseWidget from './BaseWidget';
6
+
7
+ export default abstract class BaseToolWidget extends BaseWidget {
8
+ public constructor(
9
+ protected editor: Editor,
10
+ protected targetTool: BaseTool,
11
+ protected localizationTable: ToolbarLocalization,
12
+ ) {
13
+ super(editor, localizationTable);
14
+
15
+ editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
16
+ if (toolEvt.kind !== EditorEventType.ToolEnabled) {
17
+ throw new Error('Incorrect event type! (Expected ToolEnabled)');
18
+ }
19
+
20
+ if (toolEvt.tool === targetTool) {
21
+ this.setSelected(true);
22
+ }
23
+ });
24
+
25
+ editor.notifier.on(EditorEventType.ToolDisabled, toolEvt => {
26
+ if (toolEvt.kind !== EditorEventType.ToolDisabled) {
27
+ throw new Error('Incorrect event type! (Expected ToolDisabled)');
28
+ }
29
+
30
+ if (toolEvt.tool === targetTool) {
31
+ this.setSelected(false);
32
+ this.setDropdownVisible(false);
33
+ }
34
+ });
35
+ }
36
+
37
+ protected handleClick() {
38
+ if (this.hasDropdown) {
39
+ if (!this.targetTool.isEnabled()) {
40
+ this.targetTool.setEnabled(true);
41
+ } else {
42
+ this.setDropdownVisible(!this.isDropdownVisible());
43
+ }
44
+ } else {
45
+ this.targetTool.setEnabled(!this.targetTool.isEnabled());
46
+ }
47
+ }
48
+
49
+ public addTo(parent: HTMLElement) {
50
+ super.addTo(parent);
51
+ this.setSelected(this.targetTool.isEnabled());
52
+ }
53
+ }
@@ -0,0 +1,175 @@
1
+ import Editor from '../../Editor';
2
+ import { toolbarCSSPrefix } from '../HTMLToolbar';
3
+ import { makeDropdownIcon } from '../icons';
4
+ import { ToolbarLocalization } from '../localization';
5
+
6
+ export default abstract class BaseWidget {
7
+ protected readonly container: HTMLElement;
8
+ private button: HTMLElement;
9
+ private icon: Element|null;
10
+ private dropdownContainer: HTMLElement;
11
+ private dropdownIcon: Element;
12
+ private label: HTMLLabelElement;
13
+ #hasDropdown: boolean;
14
+ private disabled: boolean = false;
15
+ private subWidgets: BaseWidget[] = [];
16
+
17
+ public constructor(
18
+ protected editor: Editor,
19
+ protected localizationTable: ToolbarLocalization,
20
+ ) {
21
+ this.icon = null;
22
+ this.container = document.createElement('div');
23
+ this.container.classList.add(`${toolbarCSSPrefix}toolContainer`);
24
+ this.dropdownContainer = document.createElement('div');
25
+ this.dropdownContainer.classList.add(`${toolbarCSSPrefix}dropdown`);
26
+ this.dropdownContainer.classList.add('hidden');
27
+ this.#hasDropdown = false;
28
+
29
+ this.button = document.createElement('div');
30
+ this.button.classList.add(`${toolbarCSSPrefix}button`);
31
+ this.label = document.createElement('label');
32
+ this.button.setAttribute('role', 'button');
33
+ this.button.tabIndex = 0;
34
+ }
35
+
36
+ protected abstract getTitle(): string;
37
+ protected abstract createIcon(): Element;
38
+
39
+ // Add content to the widget's associated dropdown menu.
40
+ // Returns true if such a menu should be created, false otherwise.
41
+ protected fillDropdown(dropdown: HTMLElement): boolean {
42
+ if (this.subWidgets.length === 0) {
43
+ return false;
44
+ }
45
+
46
+ for (const widget of this.subWidgets) {
47
+ widget.addTo(dropdown);
48
+ }
49
+ return true;
50
+ }
51
+
52
+ protected setupActionBtnClickListener(button: HTMLElement) {
53
+ button.onclick = () => {
54
+ if (!this.disabled) {
55
+ this.handleClick();
56
+ }
57
+ };
58
+ }
59
+
60
+ protected abstract handleClick(): void;
61
+
62
+ protected get hasDropdown() {
63
+ return this.#hasDropdown;
64
+ }
65
+
66
+ // Add a widget to this' dropdown. Must be called before this.addTo.
67
+ protected addSubWidget(widget: BaseWidget) {
68
+ this.subWidgets.push(widget);
69
+ }
70
+
71
+ // Adds this to [parent]. This can only be called once for each ToolbarWidget.
72
+ public addTo(parent: HTMLElement) {
73
+ this.label.innerText = this.getTitle();
74
+
75
+ this.setupActionBtnClickListener(this.button);
76
+
77
+ this.icon = null;
78
+ this.updateIcon();
79
+
80
+ this.button.replaceChildren(this.icon!, this.label);
81
+ this.container.appendChild(this.button);
82
+
83
+ this.#hasDropdown = this.fillDropdown(this.dropdownContainer);
84
+ if (this.#hasDropdown) {
85
+ this.dropdownIcon = this.createDropdownIcon();
86
+ this.button.appendChild(this.dropdownIcon);
87
+ this.container.appendChild(this.dropdownContainer);
88
+ }
89
+
90
+ this.setDropdownVisible(false);
91
+ parent.appendChild(this.container);
92
+ }
93
+
94
+
95
+ protected updateIcon() {
96
+ const newIcon = this.createIcon();
97
+ this.icon?.replaceWith(newIcon);
98
+ this.icon = newIcon;
99
+ this.icon.classList.add(`${toolbarCSSPrefix}icon`);
100
+ }
101
+
102
+ public setDisabled(disabled: boolean) {
103
+ this.disabled = disabled;
104
+ if (this.disabled) {
105
+ this.button.classList.add('disabled');
106
+ } else {
107
+ this.button.classList.remove('disabled');
108
+ }
109
+ }
110
+
111
+ public setSelected(selected: boolean) {
112
+ const currentlySelected = this.isSelected();
113
+ if (currentlySelected === selected) {
114
+ return;
115
+ }
116
+
117
+ if (selected) {
118
+ this.container.classList.add('selected');
119
+ this.button.ariaSelected = 'true';
120
+ } else {
121
+ this.container.classList.remove('selected');
122
+ this.button.ariaSelected = 'false';
123
+ }
124
+ }
125
+
126
+ protected setDropdownVisible(visible: boolean) {
127
+ const currentlyVisible = this.container.classList.contains('dropdownVisible');
128
+ if (currentlyVisible === visible) {
129
+ return;
130
+ }
131
+
132
+ if (visible) {
133
+ this.dropdownContainer.classList.remove('hidden');
134
+ this.container.classList.add('dropdownVisible');
135
+ this.editor.announceForAccessibility(
136
+ this.localizationTable.dropdownShown(this.getTitle())
137
+ );
138
+ } else {
139
+ this.dropdownContainer.classList.add('hidden');
140
+ this.container.classList.remove('dropdownVisible');
141
+ this.editor.announceForAccessibility(
142
+ this.localizationTable.dropdownHidden(this.getTitle())
143
+ );
144
+ }
145
+
146
+ this.repositionDropdown();
147
+ }
148
+
149
+ protected repositionDropdown() {
150
+ const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
151
+ const screenWidth = document.body.clientWidth;
152
+
153
+ if (dropdownBBox.left > screenWidth / 2) {
154
+ this.dropdownContainer.style.marginLeft = this.button.clientWidth + 'px';
155
+ this.dropdownContainer.style.transform = 'translate(-100%, 0)';
156
+ } else {
157
+ this.dropdownContainer.style.marginLeft = '';
158
+ this.dropdownContainer.style.transform = '';
159
+ }
160
+ }
161
+
162
+ protected isDropdownVisible(): boolean {
163
+ return !this.dropdownContainer.classList.contains('hidden');
164
+ }
165
+
166
+ protected isSelected(): boolean {
167
+ return this.container.classList.contains('selected');
168
+ }
169
+
170
+ private createDropdownIcon(): Element {
171
+ const icon = makeDropdownIcon();
172
+ icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
173
+ return icon;
174
+ }
175
+ }
@@ -0,0 +1,16 @@
1
+ import { makeEraserIcon } from '../icons';
2
+ import BaseToolWidget from './BaseToolWidget';
3
+
4
+ export default class EraserWidget extends BaseToolWidget {
5
+ protected getTitle(): string {
6
+ return this.localizationTable.eraser;
7
+ }
8
+ protected createIcon(): Element {
9
+ return makeEraserIcon();
10
+ }
11
+
12
+ protected fillDropdown(_dropdown: HTMLElement): boolean {
13
+ // No dropdown associated with the eraser
14
+ return false;
15
+ }
16
+ }
@@ -0,0 +1,186 @@
1
+ import Editor from '../../Editor';
2
+ import Mat33 from '../../geometry/Mat33';
3
+ import PanZoom, { PanZoomMode } from '../../tools/PanZoom';
4
+ import { EditorEventType } from '../../types';
5
+ import Viewport from '../../Viewport';
6
+ import { toolbarCSSPrefix } from '../HTMLToolbar';
7
+ import { makeAllDevicePanningIcon, makeHandToolIcon, makeTouchPanningIcon, makeZoomIcon } from '../icons';
8
+ import { ToolbarLocalization } from '../localization';
9
+ import BaseToolWidget from './BaseToolWidget';
10
+ import BaseWidget from './BaseWidget';
11
+
12
+ const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor) => {
13
+ const zoomLevelRow = document.createElement('div');
14
+
15
+ const increaseButton = document.createElement('button');
16
+ const decreaseButton = document.createElement('button');
17
+ const zoomLevelDisplay = document.createElement('span');
18
+ increaseButton.innerText = '+';
19
+ decreaseButton.innerText = '-';
20
+ zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton);
21
+
22
+ zoomLevelRow.classList.add(`${toolbarCSSPrefix}zoomLevelEditor`);
23
+ zoomLevelDisplay.classList.add('zoomDisplay');
24
+
25
+ let lastZoom: number|undefined;
26
+ const updateZoomDisplay = () => {
27
+ let zoomLevel = editor.viewport.getScaleFactor() * 100;
28
+
29
+ if (zoomLevel > 0.1) {
30
+ zoomLevel = Math.round(zoomLevel * 10) / 10;
31
+ } else {
32
+ zoomLevel = Math.round(zoomLevel * 1000) / 1000;
33
+ }
34
+
35
+ if (zoomLevel !== lastZoom) {
36
+ zoomLevelDisplay.innerText = localizationTable.zoomLevel(zoomLevel);
37
+ lastZoom = zoomLevel;
38
+ }
39
+ };
40
+ updateZoomDisplay();
41
+
42
+ editor.notifier.on(EditorEventType.ViewportChanged, (event) => {
43
+ if (event.kind === EditorEventType.ViewportChanged) {
44
+ updateZoomDisplay();
45
+ }
46
+ });
47
+
48
+ const zoomBy = (factor: number) => {
49
+ const screenCenter = editor.viewport.visibleRect.center;
50
+ const transformUpdate = Mat33.scaling2D(factor, screenCenter);
51
+ editor.dispatch(new Viewport.ViewportTransform(transformUpdate), false);
52
+ };
53
+
54
+ increaseButton.onclick = () => {
55
+ zoomBy(5.0/4);
56
+ };
57
+
58
+ decreaseButton.onclick = () => {
59
+ zoomBy(4.0/5);
60
+ };
61
+
62
+ return zoomLevelRow;
63
+ };
64
+
65
+ class ZoomWidget extends BaseWidget {
66
+ public constructor(editor: Editor, localizationTable: ToolbarLocalization) {
67
+ super(editor, localizationTable);
68
+
69
+ // Make it possible to open the dropdown, even if this widget isn't selected.
70
+ this.container.classList.add('dropdownShowable');
71
+ }
72
+
73
+ protected getTitle(): string {
74
+ return this.localizationTable.zoom;
75
+ }
76
+
77
+ protected createIcon(): Element {
78
+ return makeZoomIcon();
79
+ }
80
+
81
+ protected handleClick(): void {
82
+ this.setDropdownVisible(!this.isDropdownVisible());
83
+ }
84
+
85
+ protected fillDropdown(dropdown: HTMLElement): boolean {
86
+ dropdown.appendChild(makeZoomControl(this.localizationTable, this.editor));
87
+ return true;
88
+ }
89
+ }
90
+
91
+ class HandModeWidget extends BaseWidget {
92
+ public constructor(
93
+ editor: Editor, localizationTable: ToolbarLocalization,
94
+
95
+ protected tool: PanZoom, protected flag: PanZoomMode, protected makeIcon: ()=> Element,
96
+ private title: string,
97
+ ) {
98
+ super(editor, localizationTable);
99
+
100
+ editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
101
+ if (toolEvt.kind === EditorEventType.ToolUpdated && toolEvt.tool === tool) {
102
+ const allEnabled = !!(tool.getMode() & PanZoomMode.SinglePointerGestures);
103
+ this.setSelected(!!(tool.getMode() & flag) || allEnabled);
104
+
105
+ // Unless this widget toggles all single pointer gestures, toggling while
106
+ // single pointer gestures are enabled should have no effect
107
+ this.setDisabled(allEnabled && flag !== PanZoomMode.SinglePointerGestures);
108
+ }
109
+ });
110
+ this.setSelected(false);
111
+ }
112
+
113
+ private setModeFlag(enabled: boolean) {
114
+ const mode = this.tool.getMode();
115
+ if (enabled) {
116
+ this.tool.setMode(mode | this.flag);
117
+ } else {
118
+ this.tool.setMode(mode & ~this.flag);
119
+ }
120
+ }
121
+
122
+ protected handleClick() {
123
+ this.setModeFlag(!this.isSelected());
124
+ }
125
+
126
+ protected getTitle(): string {
127
+ return this.title;
128
+ }
129
+
130
+ protected createIcon(): Element {
131
+ return this.makeIcon();
132
+ }
133
+
134
+ protected fillDropdown(_dropdown: HTMLElement): boolean {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ export default class HandToolWidget extends BaseToolWidget {
140
+ private touchPanningWidget: HandModeWidget;
141
+ public constructor(
142
+ editor: Editor, protected tool: PanZoom, localizationTable: ToolbarLocalization
143
+ ) {
144
+ super(editor, tool, localizationTable);
145
+ this.container.classList.add('dropdownShowable');
146
+
147
+ this.touchPanningWidget = new HandModeWidget(
148
+ editor, localizationTable,
149
+
150
+ tool, PanZoomMode.OneFingerTouchGestures,
151
+ makeTouchPanningIcon,
152
+
153
+ localizationTable.touchPanning
154
+ );
155
+
156
+ this.addSubWidget(this.touchPanningWidget);
157
+ this.addSubWidget(
158
+ new HandModeWidget(
159
+ editor, localizationTable,
160
+
161
+ tool, PanZoomMode.SinglePointerGestures,
162
+ makeAllDevicePanningIcon,
163
+
164
+ localizationTable.anyDevicePanning
165
+ )
166
+ );
167
+ this.addSubWidget(
168
+ new ZoomWidget(editor, localizationTable)
169
+ );
170
+ }
171
+
172
+ protected getTitle(): string {
173
+ return this.localizationTable.handTool;
174
+ }
175
+
176
+ protected createIcon(): Element {
177
+ return makeHandToolIcon();
178
+ }
179
+
180
+ public setSelected(_selected: boolean): void {
181
+ }
182
+
183
+ protected handleClick() {
184
+ this.setDropdownVisible(!this.isDropdownVisible());
185
+ }
186
+ }
@@ -0,0 +1,165 @@
1
+ import { makeArrowBuilder } from '../../components/builders/ArrowBuilder';
2
+ import { makeFreehandLineBuilder } from '../../components/builders/FreehandLineBuilder';
3
+ import { makeLineBuilder } from '../../components/builders/LineBuilder';
4
+ import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../../components/builders/RectangleBuilder';
5
+ import { ComponentBuilderFactory } from '../../components/builders/types';
6
+ import Editor from '../../Editor';
7
+ import Pen from '../../tools/Pen';
8
+ import { EditorEventType } from '../../types';
9
+ import { toolbarCSSPrefix } from '../HTMLToolbar';
10
+ import { makeIconFromFactory, makePenIcon } from '../icons';
11
+ import { ToolbarLocalization } from '../localization';
12
+ import makeColorInput from '../makeColorInput';
13
+ import BaseToolWidget from './BaseToolWidget';
14
+
15
+
16
+ interface PenTypeRecord {
17
+ name: string;
18
+ factory: ComponentBuilderFactory;
19
+ }
20
+
21
+ export default class PenWidget extends BaseToolWidget {
22
+ private updateInputs: ()=> void = () => {};
23
+ protected penTypes: PenTypeRecord[];
24
+
25
+ public constructor(
26
+ editor: Editor, private tool: Pen, localization: ToolbarLocalization
27
+ ) {
28
+ super(editor, tool, localization);
29
+
30
+ // Default pen types
31
+ this.penTypes = [
32
+ {
33
+ name: localization.freehandPen,
34
+ factory: makeFreehandLineBuilder,
35
+ },
36
+ {
37
+ name: localization.arrowPen,
38
+ factory: makeArrowBuilder,
39
+ },
40
+ {
41
+ name: localization.linePen,
42
+ factory: makeLineBuilder,
43
+ },
44
+ {
45
+ name: localization.filledRectanglePen,
46
+ factory: makeFilledRectangleBuilder,
47
+ },
48
+ {
49
+ name: localization.outlinedRectanglePen,
50
+ factory: makeOutlinedRectangleBuilder,
51
+ },
52
+ ];
53
+
54
+ this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
55
+ if (toolEvt.kind !== EditorEventType.ToolUpdated) {
56
+ throw new Error('Invalid event type!');
57
+ }
58
+
59
+ // The button icon may depend on tool properties.
60
+ if (toolEvt.tool === this.tool) {
61
+ this.updateIcon();
62
+ this.updateInputs();
63
+ }
64
+ });
65
+ }
66
+
67
+ protected getTitle(): string {
68
+ return this.targetTool.description;
69
+ }
70
+
71
+ protected createIcon(): Element {
72
+ const strokeFactory = this.tool.getStrokeFactory();
73
+ if (strokeFactory === makeFreehandLineBuilder) {
74
+ // Use a square-root scale to prevent the pen's tip from overflowing.
75
+ const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
76
+ const color = this.tool.getColor();
77
+ return makePenIcon(scale, color.toHexString());
78
+ } else {
79
+ const strokeFactory = this.tool.getStrokeFactory();
80
+ return makeIconFromFactory(this.tool, strokeFactory);
81
+ }
82
+ }
83
+
84
+ private static idCounter: number = 0;
85
+ protected fillDropdown(dropdown: HTMLElement): boolean {
86
+ const container = document.createElement('div');
87
+
88
+ const thicknessRow = document.createElement('div');
89
+ const objectTypeRow = document.createElement('div');
90
+
91
+ // Thickness: Value of the input is squared to allow for finer control/larger values.
92
+ const thicknessLabel = document.createElement('label');
93
+ const thicknessInput = document.createElement('input');
94
+ const objectSelectLabel = document.createElement('label');
95
+ const objectTypeSelect = document.createElement('select');
96
+
97
+ // Give inputs IDs so we can label them with a <label for=...>Label text</label>
98
+ thicknessInput.id = `${toolbarCSSPrefix}thicknessInput${PenWidget.idCounter++}`;
99
+ objectTypeSelect.id = `${toolbarCSSPrefix}builderSelect${PenWidget.idCounter++}`;
100
+
101
+ thicknessLabel.innerText = this.localizationTable.thicknessLabel;
102
+ thicknessLabel.setAttribute('for', thicknessInput.id);
103
+ objectSelectLabel.innerText = this.localizationTable.selectObjectType;
104
+ objectSelectLabel.setAttribute('for', objectTypeSelect.id);
105
+
106
+ thicknessInput.type = 'range';
107
+ thicknessInput.min = '1';
108
+ thicknessInput.max = '20';
109
+ thicknessInput.step = '1';
110
+ thicknessInput.oninput = () => {
111
+ this.tool.setThickness(parseFloat(thicknessInput.value) ** 2);
112
+ };
113
+ thicknessRow.appendChild(thicknessLabel);
114
+ thicknessRow.appendChild(thicknessInput);
115
+
116
+ objectTypeSelect.oninput = () => {
117
+ const penTypeIdx = parseInt(objectTypeSelect.value);
118
+ if (penTypeIdx < 0 || penTypeIdx >= this.penTypes.length) {
119
+ console.error('Invalid pen type index', penTypeIdx);
120
+ return;
121
+ }
122
+
123
+ this.tool.setStrokeFactory(this.penTypes[penTypeIdx].factory);
124
+ };
125
+ objectTypeRow.appendChild(objectSelectLabel);
126
+ objectTypeRow.appendChild(objectTypeSelect);
127
+
128
+ const colorRow = document.createElement('div');
129
+ const colorLabel = document.createElement('label');
130
+ const [ colorInput, colorInputContainer ] = makeColorInput(this.editor, color => {
131
+ this.tool.setColor(color);
132
+ });
133
+
134
+ colorInput.id = `${toolbarCSSPrefix}colorInput${PenWidget.idCounter++}`;
135
+ colorLabel.innerText = this.localizationTable.colorLabel;
136
+ colorLabel.setAttribute('for', colorInput.id);
137
+
138
+ colorRow.appendChild(colorLabel);
139
+ colorRow.appendChild(colorInputContainer);
140
+
141
+ this.updateInputs = () => {
142
+ colorInput.value = this.tool.getColor().toHexString();
143
+ thicknessInput.value = Math.sqrt(this.tool.getThickness()).toString();
144
+
145
+ objectTypeSelect.replaceChildren();
146
+ for (let i = 0; i < this.penTypes.length; i ++) {
147
+ const penType = this.penTypes[i];
148
+ const option = document.createElement('option');
149
+ option.value = i.toString();
150
+ option.innerText = penType.name;
151
+
152
+ objectTypeSelect.appendChild(option);
153
+
154
+ if (penType.factory === this.tool.getStrokeFactory()) {
155
+ objectTypeSelect.value = i.toString();
156
+ }
157
+ }
158
+ };
159
+ this.updateInputs();
160
+
161
+ container.replaceChildren(colorRow, thicknessRow, objectTypeRow);
162
+ dropdown.replaceChildren(container);
163
+ return true;
164
+ }
165
+ }
@@ -0,0 +1,72 @@
1
+ import Editor from '../../Editor';
2
+ import SelectionTool from '../../tools/SelectionTool';
3
+ import { EditorEventType } from '../../types';
4
+ import { makeSelectionIcon } from '../icons';
5
+ import { ToolbarLocalization } from '../localization';
6
+ import BaseToolWidget from './BaseToolWidget';
7
+
8
+ export class SelectionWidget extends BaseToolWidget {
9
+ public constructor(
10
+ editor: Editor, private tool: SelectionTool, localization: ToolbarLocalization
11
+ ) {
12
+ super(editor, tool, localization);
13
+ }
14
+
15
+ protected getTitle(): string {
16
+ return this.localizationTable.select;
17
+ }
18
+
19
+ protected createIcon(): Element {
20
+ return makeSelectionIcon();
21
+ }
22
+
23
+ protected fillDropdown(dropdown: HTMLElement): boolean {
24
+ const container = document.createElement('div');
25
+ const resizeButton = document.createElement('button');
26
+ const duplicateButton = document.createElement('button');
27
+ const deleteButton = document.createElement('button');
28
+
29
+ resizeButton.innerText = this.localizationTable.resizeImageToSelection;
30
+ resizeButton.disabled = true;
31
+ deleteButton.innerText = this.localizationTable.deleteSelection;
32
+ deleteButton.disabled = true;
33
+ duplicateButton.innerText = this.localizationTable.duplicateSelection;
34
+ duplicateButton.disabled = true;
35
+
36
+ resizeButton.onclick = () => {
37
+ const selection = this.tool.getSelection();
38
+ this.editor.dispatch(this.editor.setImportExportRect(selection!.region));
39
+ };
40
+
41
+ deleteButton.onclick = () => {
42
+ const selection = this.tool.getSelection();
43
+ this.editor.dispatch(selection!.deleteSelectedObjects());
44
+ this.tool.clearSelection();
45
+ };
46
+
47
+ duplicateButton.onclick = () => {
48
+ const selection = this.tool.getSelection();
49
+ this.editor.dispatch(selection!.duplicateSelectedObjects());
50
+ };
51
+
52
+ // Enable/disable actions based on whether items are selected
53
+ this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
54
+ if (toolEvt.kind !== EditorEventType.ToolUpdated) {
55
+ throw new Error('Invalid event type!');
56
+ }
57
+
58
+ if (toolEvt.tool === this.tool) {
59
+ const selection = this.tool.getSelection();
60
+ const hasSelection = selection && selection.region.area > 0;
61
+
62
+ resizeButton.disabled = !hasSelection;
63
+ deleteButton.disabled = resizeButton.disabled;
64
+ duplicateButton.disabled = resizeButton.disabled;
65
+ }
66
+ });
67
+
68
+ container.replaceChildren(resizeButton, duplicateButton, deleteButton);
69
+ dropdown.appendChild(container);
70
+ return true;
71
+ }
72
+ }