js-draw 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +9 -6
- package/dist/src/Editor.js +9 -3
- package/dist/src/EditorImage.d.ts +3 -0
- package/dist/src/EditorImage.js +7 -0
- package/dist/src/SVGLoader.js +5 -6
- package/dist/src/components/AbstractComponent.d.ts +1 -0
- package/dist/src/components/AbstractComponent.js +4 -0
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -0
- package/dist/src/components/SVGGlobalAttributesObject.js +3 -0
- package/dist/src/components/Text.d.ts +3 -5
- package/dist/src/components/Text.js +19 -10
- package/dist/src/components/UnknownSVGObject.d.ts +1 -0
- package/dist/src/components/UnknownSVGObject.js +3 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +3 -3
- package/dist/src/rendering/renderers/SVGRenderer.js +1 -1
- package/dist/src/testing/beforeEachFile.js +4 -0
- package/dist/src/toolbar/HTMLToolbar.js +2 -3
- package/dist/src/toolbar/IconProvider.d.ts +24 -0
- package/dist/src/toolbar/IconProvider.js +415 -0
- package/dist/src/toolbar/lib.d.ts +1 -1
- package/dist/src/toolbar/lib.js +1 -2
- package/dist/src/toolbar/localization.d.ts +0 -1
- package/dist/src/toolbar/localization.js +0 -1
- package/dist/src/toolbar/makeColorInput.js +1 -2
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +2 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +16 -2
- package/dist/src/toolbar/widgets/EraserToolWidget.js +1 -2
- package/dist/src/toolbar/widgets/HandToolWidget.d.ts +5 -3
- package/dist/src/toolbar/widgets/HandToolWidget.js +35 -12
- package/dist/src/toolbar/widgets/PenToolWidget.d.ts +2 -0
- package/dist/src/toolbar/widgets/PenToolWidget.js +16 -3
- package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +3 -0
- package/dist/src/toolbar/widgets/SelectionToolWidget.js +20 -7
- package/dist/src/toolbar/widgets/TextToolWidget.js +1 -2
- package/dist/src/tools/PanZoom.d.ts +1 -1
- package/dist/src/tools/PanZoom.js +4 -1
- package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -0
- package/dist/src/tools/SelectionTool/SelectionTool.js +66 -3
- package/dist/src/tools/ToolController.js +3 -0
- package/dist/src/tools/ToolbarShortcutHandler.d.ts +12 -0
- package/dist/src/tools/ToolbarShortcutHandler.js +23 -0
- package/dist/src/tools/lib.d.ts +1 -0
- package/dist/src/tools/lib.js +1 -0
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/dist/src/types.d.ts +4 -2
- package/package.json +1 -1
- package/src/Editor.ts +17 -7
- package/src/EditorImage.ts +9 -0
- package/src/SVGLoader.test.ts +37 -0
- package/src/SVGLoader.ts +5 -6
- package/src/components/AbstractComponent.ts +5 -0
- package/src/components/SVGGlobalAttributesObject.ts +4 -0
- package/src/components/Text.test.ts +1 -16
- package/src/components/Text.ts +21 -11
- package/src/components/UnknownSVGObject.ts +4 -0
- package/src/components/builders/FreehandLineBuilder.ts +3 -3
- package/src/rendering/renderers/SVGRenderer.ts +1 -1
- package/src/testing/beforeEachFile.ts +6 -1
- package/src/toolbar/HTMLToolbar.ts +2 -3
- package/src/toolbar/IconProvider.ts +476 -0
- package/src/toolbar/lib.ts +1 -1
- package/src/toolbar/localization.ts +0 -2
- package/src/toolbar/makeColorInput.ts +1 -2
- package/src/toolbar/widgets/BaseWidget.ts +20 -3
- package/src/toolbar/widgets/EraserToolWidget.ts +1 -2
- package/src/toolbar/widgets/HandToolWidget.ts +42 -20
- package/src/toolbar/widgets/PenToolWidget.ts +20 -4
- package/src/toolbar/widgets/SelectionToolWidget.ts +24 -8
- package/src/toolbar/widgets/TextToolWidget.ts +1 -2
- package/src/tools/PanZoom.ts +4 -1
- package/src/tools/SelectionTool/SelectionTool.css +2 -1
- package/src/tools/SelectionTool/SelectionTool.test.ts +40 -0
- package/src/tools/SelectionTool/SelectionTool.ts +73 -4
- package/src/tools/ToolController.ts +3 -0
- package/src/tools/ToolbarShortcutHandler.ts +34 -0
- package/src/tools/UndoRedoShortcut.test.ts +3 -0
- package/src/tools/lib.ts +1 -0
- package/src/tools/localization.ts +4 -0
- package/src/types.ts +13 -8
- package/typedoc.json +5 -1
- package/dist/src/toolbar/icons.d.ts +0 -20
- package/dist/src/toolbar/icons.js +0 -385
- package/src/toolbar/icons.ts +0 -443
@@ -1,7 +1,7 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
|
-
import
|
2
|
+
import ToolbarShortcutHandler from '../../tools/ToolbarShortcutHandler';
|
3
|
+
import { EditorEventType, InputEvtType, KeyPressEvent } from '../../types';
|
3
4
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
4
|
-
import { makeDropdownIcon } from '../icons';
|
5
5
|
import { ToolbarLocalization } from '../localization';
|
6
6
|
|
7
7
|
export default abstract class BaseWidget {
|
@@ -33,6 +33,14 @@ export default abstract class BaseWidget {
|
|
33
33
|
this.label = document.createElement('label');
|
34
34
|
this.button.setAttribute('role', 'button');
|
35
35
|
this.button.tabIndex = 0;
|
36
|
+
|
37
|
+
const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler);
|
38
|
+
|
39
|
+
// If the onKeyPress function has been extended and the editor is configured to send keypress events to
|
40
|
+
// toolbar widgets,
|
41
|
+
if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== BaseWidget.prototype.onKeyPress) {
|
42
|
+
toolbarShortcutHandlers[0].registerListener(event => this.onKeyPress(event));
|
43
|
+
}
|
36
44
|
}
|
37
45
|
|
38
46
|
protected abstract getTitle(): string;
|
@@ -70,6 +78,7 @@ export default abstract class BaseWidget {
|
|
70
78
|
kind: InputEvtType.KeyPressEvent,
|
71
79
|
key: evt.key,
|
72
80
|
ctrlKey: evt.ctrlKey,
|
81
|
+
altKey: evt.altKey,
|
73
82
|
});
|
74
83
|
}
|
75
84
|
};
|
@@ -83,6 +92,7 @@ export default abstract class BaseWidget {
|
|
83
92
|
kind: InputEvtType.KeyUpEvent,
|
84
93
|
key: evt.key,
|
85
94
|
ctrlKey: evt.ctrlKey,
|
95
|
+
altKey: evt.altKey,
|
86
96
|
});
|
87
97
|
};
|
88
98
|
|
@@ -93,6 +103,13 @@ export default abstract class BaseWidget {
|
|
93
103
|
};
|
94
104
|
}
|
95
105
|
|
106
|
+
// Add a listener that is triggered when a key is pressed.
|
107
|
+
// Listeners will fire regardless of whether this widget is selected and require that
|
108
|
+
// {@link lib!Editor.toolController} to have an enabled {@link lib!ToolbarShortcutHandler} tool.
|
109
|
+
protected onKeyPress(_event: KeyPressEvent): boolean {
|
110
|
+
return false;
|
111
|
+
}
|
112
|
+
|
96
113
|
protected abstract handleClick(): void;
|
97
114
|
|
98
115
|
protected get hasDropdown() {
|
@@ -230,7 +247,7 @@ export default abstract class BaseWidget {
|
|
230
247
|
}
|
231
248
|
|
232
249
|
private createDropdownIcon(): Element {
|
233
|
-
const icon = makeDropdownIcon();
|
250
|
+
const icon = this.editor.icons.makeDropdownIcon();
|
234
251
|
icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
|
235
252
|
return icon;
|
236
253
|
}
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import { makeEraserIcon } from '../icons';
|
2
1
|
import BaseToolWidget from './BaseToolWidget';
|
3
2
|
|
4
3
|
export default class EraserToolWidget extends BaseToolWidget {
|
@@ -6,7 +5,7 @@ export default class EraserToolWidget extends BaseToolWidget {
|
|
6
5
|
return this.localizationTable.eraser;
|
7
6
|
}
|
8
7
|
protected createIcon(): Element {
|
9
|
-
return makeEraserIcon();
|
8
|
+
return this.editor.icons.makeEraserIcon();
|
10
9
|
}
|
11
10
|
|
12
11
|
protected fillDropdown(_dropdown: HTMLElement): boolean {
|
@@ -1,10 +1,10 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
2
|
import Mat33 from '../../math/Mat33';
|
3
3
|
import PanZoom, { PanZoomMode } from '../../tools/PanZoom';
|
4
|
+
import ToolController from '../../tools/ToolController';
|
4
5
|
import { EditorEventType } from '../../types';
|
5
6
|
import Viewport from '../../Viewport';
|
6
7
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
7
|
-
import { makeAllDevicePanningIcon, makeHandToolIcon, makeTouchPanningIcon, makeZoomIcon } from '../icons';
|
8
8
|
import { ToolbarLocalization } from '../localization';
|
9
9
|
import BaseToolWidget from './BaseToolWidget';
|
10
10
|
import BaseWidget from './BaseWidget';
|
@@ -86,7 +86,7 @@ class ZoomWidget extends BaseWidget {
|
|
86
86
|
}
|
87
87
|
|
88
88
|
protected createIcon(): Element {
|
89
|
-
return makeZoomIcon();
|
89
|
+
return this.editor.icons.makeZoomIcon();
|
90
90
|
}
|
91
91
|
|
92
92
|
protected handleClick(): void {
|
@@ -149,49 +149,71 @@ class HandModeWidget extends BaseWidget {
|
|
149
149
|
|
150
150
|
export default class HandToolWidget extends BaseToolWidget {
|
151
151
|
private touchPanningWidget: HandModeWidget;
|
152
|
+
private allowTogglingBaseTool: boolean;
|
153
|
+
|
152
154
|
public constructor(
|
153
|
-
editor: Editor,
|
155
|
+
editor: Editor,
|
156
|
+
|
157
|
+
// Pan zoom tool that overrides all other tools (enabling this tool for a device
|
158
|
+
// causes that device to pan/zoom instead of interact with the primary tools)
|
159
|
+
protected overridePanZoomTool: PanZoom,
|
160
|
+
|
161
|
+
localizationTable: ToolbarLocalization,
|
154
162
|
) {
|
163
|
+
const primaryHandTool = HandToolWidget.getPrimaryHandTool(editor.toolController);
|
164
|
+
const tool = primaryHandTool ?? overridePanZoomTool;
|
155
165
|
super(editor, tool, localizationTable);
|
156
|
-
this.container.classList.add('dropdownShowable');
|
157
166
|
|
167
|
+
// Only allow toggling a hand tool if we're using the primary hand tool and not the override
|
168
|
+
// hand tool for this button.
|
169
|
+
this.allowTogglingBaseTool = primaryHandTool !== null;
|
170
|
+
|
171
|
+
// Allow showing/hiding the dropdown, even if `overridePanZoomTool` isn't enabled.
|
172
|
+
if (!this.allowTogglingBaseTool) {
|
173
|
+
this.container.classList.add('dropdownShowable');
|
174
|
+
}
|
175
|
+
|
176
|
+
// Controls for the overriding hand tool.
|
158
177
|
this.touchPanningWidget = new HandModeWidget(
|
159
178
|
editor, localizationTable,
|
160
179
|
|
161
|
-
|
162
|
-
makeTouchPanningIcon,
|
180
|
+
overridePanZoomTool, PanZoomMode.OneFingerTouchGestures,
|
181
|
+
() => this.editor.icons.makeTouchPanningIcon(),
|
163
182
|
|
164
183
|
localizationTable.touchPanning
|
165
184
|
);
|
166
185
|
|
167
186
|
this.addSubWidget(this.touchPanningWidget);
|
168
|
-
this.addSubWidget(
|
169
|
-
new HandModeWidget(
|
170
|
-
editor, localizationTable,
|
171
|
-
|
172
|
-
tool, PanZoomMode.SinglePointerGestures,
|
173
|
-
makeAllDevicePanningIcon,
|
174
|
-
|
175
|
-
localizationTable.anyDevicePanning
|
176
|
-
)
|
177
|
-
);
|
178
187
|
this.addSubWidget(
|
179
188
|
new ZoomWidget(editor, localizationTable)
|
180
189
|
);
|
181
190
|
}
|
182
191
|
|
192
|
+
private static getPrimaryHandTool(toolController: ToolController): PanZoom|null {
|
193
|
+
const primaryPanZoomToolList = toolController.getPrimaryTools().filter(tool => tool instanceof PanZoom);
|
194
|
+
const primaryPanZoomTool = primaryPanZoomToolList[0];
|
195
|
+
return primaryPanZoomTool as PanZoom|null;
|
196
|
+
}
|
197
|
+
|
183
198
|
protected getTitle(): string {
|
184
199
|
return this.localizationTable.handTool;
|
185
200
|
}
|
186
201
|
|
187
202
|
protected createIcon(): Element {
|
188
|
-
return makeHandToolIcon();
|
203
|
+
return this.editor.icons.makeHandToolIcon();
|
189
204
|
}
|
190
205
|
|
191
|
-
|
206
|
+
protected handleClick(): void {
|
207
|
+
if (this.allowTogglingBaseTool) {
|
208
|
+
super.handleClick();
|
209
|
+
} else {
|
210
|
+
this.setDropdownVisible(!this.isDropdownVisible());
|
211
|
+
}
|
192
212
|
}
|
193
213
|
|
194
|
-
|
195
|
-
|
214
|
+
public setSelected(selected: boolean): void {
|
215
|
+
if (this.allowTogglingBaseTool) {
|
216
|
+
super.setSelected(selected);
|
217
|
+
}
|
196
218
|
}
|
197
219
|
}
|
@@ -5,9 +5,8 @@ import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../../
|
|
5
5
|
import { ComponentBuilderFactory } from '../../components/builders/types';
|
6
6
|
import Editor from '../../Editor';
|
7
7
|
import Pen from '../../tools/Pen';
|
8
|
-
import { EditorEventType } from '../../types';
|
8
|
+
import { EditorEventType, KeyPressEvent } from '../../types';
|
9
9
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
10
|
-
import { makeIconFromFactory, makePenIcon } from '../icons';
|
11
10
|
import { ToolbarLocalization } from '../localization';
|
12
11
|
import makeColorInput from '../makeColorInput';
|
13
12
|
import BaseToolWidget from './BaseToolWidget';
|
@@ -77,10 +76,10 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
77
76
|
// Use a square-root scale to prevent the pen's tip from overflowing.
|
78
77
|
const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
|
79
78
|
const color = this.tool.getColor();
|
80
|
-
return makePenIcon(scale, color.toHexString());
|
79
|
+
return this.editor.icons.makePenIcon(scale, color.toHexString());
|
81
80
|
} else {
|
82
81
|
const strokeFactory = this.tool.getStrokeFactory();
|
83
|
-
return makeIconFromFactory(this.tool, strokeFactory);
|
82
|
+
return this.editor.icons.makeIconFromFactory(this.tool, strokeFactory);
|
84
83
|
}
|
85
84
|
}
|
86
85
|
|
@@ -165,4 +164,21 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
165
164
|
dropdown.replaceChildren(container);
|
166
165
|
return true;
|
167
166
|
}
|
167
|
+
|
168
|
+
protected onKeyPress(event: KeyPressEvent): boolean {
|
169
|
+
if (!this.isSelected()) {
|
170
|
+
return false;
|
171
|
+
}
|
172
|
+
|
173
|
+
// Map alt+0-9 to different pen types.
|
174
|
+
if (/^[0-9]$/.exec(event.key) && event.ctrlKey) {
|
175
|
+
const penTypeIdx = parseInt(event.key) - 1;
|
176
|
+
if (penTypeIdx >= 0 && penTypeIdx < this.penTypes.length) {
|
177
|
+
this.tool.setStrokeFactory(this.penTypes[penTypeIdx].factory);
|
178
|
+
return true;
|
179
|
+
}
|
180
|
+
}
|
181
|
+
|
182
|
+
return false;
|
183
|
+
}
|
168
184
|
}
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
2
|
import SelectionTool from '../../tools/SelectionTool/SelectionTool';
|
3
|
-
import { EditorEventType } from '../../types';
|
4
|
-
import { makeDeleteSelectionIcon, makeDuplicateSelectionIcon, makeResizeViewportIcon, makeSelectionIcon } from '../icons';
|
3
|
+
import { EditorEventType, KeyPressEvent } from '../../types';
|
5
4
|
import { ToolbarLocalization } from '../localization';
|
6
5
|
import ActionButtonWidget from './ActionButtonWidget';
|
7
6
|
import BaseToolWidget from './BaseToolWidget';
|
@@ -14,16 +13,15 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
14
13
|
|
15
14
|
const resizeButton = new ActionButtonWidget(
|
16
15
|
editor, localization,
|
17
|
-
makeResizeViewportIcon,
|
16
|
+
() => editor.icons.makeResizeViewportIcon(),
|
18
17
|
this.localizationTable.resizeImageToSelection,
|
19
18
|
() => {
|
20
|
-
|
21
|
-
this.editor.dispatch(this.editor.setImportExportRect(selection!.region));
|
19
|
+
this.resizeImageToSelection();
|
22
20
|
},
|
23
21
|
);
|
24
22
|
const deleteButton = new ActionButtonWidget(
|
25
23
|
editor, localization,
|
26
|
-
makeDeleteSelectionIcon,
|
24
|
+
() => editor.icons.makeDeleteSelectionIcon(),
|
27
25
|
this.localizationTable.deleteSelection,
|
28
26
|
() => {
|
29
27
|
const selection = this.tool.getSelection();
|
@@ -33,7 +31,7 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
33
31
|
);
|
34
32
|
const duplicateButton = new ActionButtonWidget(
|
35
33
|
editor, localization,
|
36
|
-
makeDuplicateSelectionIcon,
|
34
|
+
() => editor.icons.makeDuplicateSelectionIcon(),
|
37
35
|
this.localizationTable.duplicateSelection,
|
38
36
|
() => {
|
39
37
|
const selection = this.tool.getSelection();
|
@@ -67,11 +65,29 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
67
65
|
});
|
68
66
|
}
|
69
67
|
|
68
|
+
private resizeImageToSelection() {
|
69
|
+
const selection = this.tool.getSelection();
|
70
|
+
if (selection) {
|
71
|
+
this.editor.dispatch(this.editor.setImportExportRect(selection.region));
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
protected onKeyPress(event: KeyPressEvent): boolean {
|
76
|
+
// Resize image to selection:
|
77
|
+
// Other keys are handled directly by the selection tool.
|
78
|
+
if (event.ctrlKey && event.key === 'r') {
|
79
|
+
this.resizeImageToSelection();
|
80
|
+
return true;
|
81
|
+
}
|
82
|
+
|
83
|
+
return false;
|
84
|
+
}
|
85
|
+
|
70
86
|
protected getTitle(): string {
|
71
87
|
return this.localizationTable.select;
|
72
88
|
}
|
73
89
|
|
74
90
|
protected createIcon(): Element {
|
75
|
-
return makeSelectionIcon();
|
91
|
+
return this.editor.icons.makeSelectionIcon();
|
76
92
|
}
|
77
93
|
}
|
@@ -2,7 +2,6 @@ import Editor from '../../Editor';
|
|
2
2
|
import TextTool from '../../tools/TextTool';
|
3
3
|
import { EditorEventType } from '../../types';
|
4
4
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
5
|
-
import { makeTextIcon } from '../icons';
|
6
5
|
import { ToolbarLocalization } from '../localization';
|
7
6
|
import makeColorInput from '../makeColorInput';
|
8
7
|
import BaseToolWidget from './BaseToolWidget';
|
@@ -26,7 +25,7 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
26
25
|
|
27
26
|
protected createIcon(): Element {
|
28
27
|
const textStyle = this.tool.getTextStyle();
|
29
|
-
return makeTextIcon(textStyle);
|
28
|
+
return this.editor.icons.makeTextIcon(textStyle);
|
30
29
|
}
|
31
30
|
|
32
31
|
private static idCounter: number = 0;
|
package/src/tools/PanZoom.ts
CHANGED
@@ -182,10 +182,13 @@ export default class PanZoom extends BaseTool {
|
|
182
182
|
return true;
|
183
183
|
}
|
184
184
|
|
185
|
-
public onKeyPress({ key }: KeyPressEvent): boolean {
|
185
|
+
public onKeyPress({ key, ctrlKey, altKey }: KeyPressEvent): boolean {
|
186
186
|
if (!(this.mode & PanZoomMode.Keyboard)) {
|
187
187
|
return false;
|
188
188
|
}
|
189
|
+
if (ctrlKey || altKey) {
|
190
|
+
return false;
|
191
|
+
}
|
189
192
|
|
190
193
|
// No need to keep the same the transform for keyboard events.
|
191
194
|
this.transform = Viewport.transformBy(Mat33.identity);
|
@@ -100,4 +100,44 @@ describe('SelectionTool', () => {
|
|
100
100
|
editor.sendKeyboardEvent(InputEvtType.KeyUpEvent, 'a');
|
101
101
|
expect(editor.viewport.visibleRect.containsPoint(selection.region.center)).toBe(true);
|
102
102
|
});
|
103
|
+
|
104
|
+
it('shift+click should expand an existing selection', () => {
|
105
|
+
const { addTestStrokeCommand: stroke1Command } = createSquareStroke(50);
|
106
|
+
const { addTestStrokeCommand: stroke2Command } = createSquareStroke(500);
|
107
|
+
|
108
|
+
const editor = createEditor();
|
109
|
+
editor.dispatch(stroke1Command);
|
110
|
+
editor.dispatch(stroke2Command);
|
111
|
+
|
112
|
+
// Select the first stroke
|
113
|
+
const selectionTool = getSelectionTool(editor);
|
114
|
+
selectionTool.setEnabled(true);
|
115
|
+
|
116
|
+
// Select the smaller rectangle
|
117
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(40, 40));
|
118
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(100, 100));
|
119
|
+
|
120
|
+
expect(selectionTool.getSelectedObjects()).toHaveLength(1);
|
121
|
+
|
122
|
+
// Shift key down.
|
123
|
+
editor.sendKeyboardEvent(InputEvtType.KeyPressEvent, 'Shift');
|
124
|
+
|
125
|
+
// Select the larger stroke.
|
126
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(200, 200));
|
127
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(600, 600));
|
128
|
+
|
129
|
+
expect(selectionTool.getSelectedObjects()).toHaveLength(2);
|
130
|
+
|
131
|
+
editor.sendKeyboardEvent(InputEvtType.KeyUpEvent, 'Shift');
|
132
|
+
|
133
|
+
// Select the larger stroke without shift pressed
|
134
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(200, 200));
|
135
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(600, 600));
|
136
|
+
expect(selectionTool.getSelectedObjects()).toHaveLength(1);
|
137
|
+
|
138
|
+
// Select nothing
|
139
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(200, 200));
|
140
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(201, 201));
|
141
|
+
expect(selectionTool.getSelectedObjects()).toHaveLength(0);
|
142
|
+
});
|
103
143
|
});
|
@@ -23,6 +23,9 @@ export default class SelectionTool extends BaseTool {
|
|
23
23
|
private selectionBox: Selection|null;
|
24
24
|
private lastEvtTarget: EventTarget|null = null;
|
25
25
|
|
26
|
+
private expandingSelectionBox: boolean = false;
|
27
|
+
private shiftKeyPressed: boolean = false;
|
28
|
+
|
26
29
|
public constructor(private editor: Editor, description: string) {
|
27
30
|
super(editor.notifier, description);
|
28
31
|
|
@@ -50,8 +53,11 @@ export default class SelectionTool extends BaseTool {
|
|
50
53
|
this.selectionBox = new Selection(
|
51
54
|
selectionStartPos, this.editor
|
52
55
|
);
|
53
|
-
|
54
|
-
this.
|
56
|
+
|
57
|
+
if (!this.expandingSelectionBox) {
|
58
|
+
// Remove any previous selection rects
|
59
|
+
this.prevSelectionBox?.cancelSelection();
|
60
|
+
}
|
55
61
|
this.selectionBox.addTo(this.handleOverlay);
|
56
62
|
}
|
57
63
|
|
@@ -60,7 +66,11 @@ export default class SelectionTool extends BaseTool {
|
|
60
66
|
if (event.allPointers.length === 1 && event.current.isPrimary) {
|
61
67
|
if (this.lastEvtTarget && this.selectionBox?.onDragStart(event.current, this.lastEvtTarget)) {
|
62
68
|
this.selectionBoxHandlingEvt = true;
|
63
|
-
|
69
|
+
this.expandingSelectionBox = false;
|
70
|
+
}
|
71
|
+
else {
|
72
|
+
// Shift key: Combine the new and old selection boxes at the end of the gesture.
|
73
|
+
this.expandingSelectionBox = this.shiftKeyPressed;
|
64
74
|
this.makeSelectionBox(event.current.canvasPos);
|
65
75
|
}
|
66
76
|
|
@@ -99,6 +109,7 @@ export default class SelectionTool extends BaseTool {
|
|
99
109
|
}
|
100
110
|
}
|
101
111
|
|
112
|
+
// Called after a gestureCancel and a pointerUp
|
102
113
|
private onGestureEnd() {
|
103
114
|
this.lastEvtTarget = null;
|
104
115
|
|
@@ -127,7 +138,19 @@ export default class SelectionTool extends BaseTool {
|
|
127
138
|
if (!this.selectionBox) return;
|
128
139
|
|
129
140
|
this.selectionBox.setToPoint(event.current.canvasPos);
|
130
|
-
|
141
|
+
|
142
|
+
// Were we expanding the previous selection?
|
143
|
+
if (this.expandingSelectionBox && this.prevSelectionBox) {
|
144
|
+
// If so, finish expanding.
|
145
|
+
this.expandingSelectionBox = false;
|
146
|
+
this.selectionBox.resolveToObjects();
|
147
|
+
this.setSelection([
|
148
|
+
...this.selectionBox.getSelectedObjects(),
|
149
|
+
...this.prevSelectionBox.getSelectedObjects(),
|
150
|
+
]);
|
151
|
+
} else {
|
152
|
+
this.onGestureEnd();
|
153
|
+
}
|
131
154
|
}
|
132
155
|
|
133
156
|
public onGestureCancel(): void {
|
@@ -139,6 +162,8 @@ export default class SelectionTool extends BaseTool {
|
|
139
162
|
this.selectionBox = this.prevSelectionBox;
|
140
163
|
this.selectionBox?.addTo(this.handleOverlay);
|
141
164
|
}
|
165
|
+
|
166
|
+
this.expandingSelectionBox = false;
|
142
167
|
}
|
143
168
|
|
144
169
|
private static handleableKeys = [
|
@@ -150,6 +175,26 @@ export default class SelectionTool extends BaseTool {
|
|
150
175
|
'i', 'I', 'o', 'O',
|
151
176
|
];
|
152
177
|
public onKeyPress(event: KeyPressEvent): boolean {
|
178
|
+
if (this.selectionBox && event.ctrlKey && event.key === 'd') {
|
179
|
+
// Handle duplication on key up — we don't want to accidentally duplicate
|
180
|
+
// many times.
|
181
|
+
return true;
|
182
|
+
}
|
183
|
+
else if (event.key === 'a' && event.ctrlKey) {
|
184
|
+
// Handle ctrl+A on key up.
|
185
|
+
// Return early to prevent 'a' from moving the selection/view.
|
186
|
+
return true;
|
187
|
+
}
|
188
|
+
else if (event.ctrlKey) {
|
189
|
+
// Don't transform the selection with, for example, ctrl+i.
|
190
|
+
// Pass it to another tool, if apliccable.
|
191
|
+
return false;
|
192
|
+
}
|
193
|
+
else if (event.key === 'Shift') {
|
194
|
+
this.shiftKeyPressed = true;
|
195
|
+
return true;
|
196
|
+
}
|
197
|
+
|
153
198
|
let rotationSteps = 0;
|
154
199
|
let xTranslateSteps = 0;
|
155
200
|
let yTranslateSteps = 0;
|
@@ -245,6 +290,21 @@ export default class SelectionTool extends BaseTool {
|
|
245
290
|
}
|
246
291
|
|
247
292
|
public onKeyUp(evt: KeyUpEvent) {
|
293
|
+
if (evt.key === 'Shift') {
|
294
|
+
this.shiftKeyPressed = false;
|
295
|
+
return true;
|
296
|
+
}
|
297
|
+
else if (evt.ctrlKey) {
|
298
|
+
if (this.selectionBox && evt.key === 'd') {
|
299
|
+
this.editor.dispatch(this.selectionBox.duplicateSelectedObjects());
|
300
|
+
return true;
|
301
|
+
}
|
302
|
+
else if (evt.key === 'a') {
|
303
|
+
this.setSelection(this.editor.image.getAllElements());
|
304
|
+
return true;
|
305
|
+
}
|
306
|
+
}
|
307
|
+
|
248
308
|
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
|
249
309
|
this.selectionBox.finalizeTransform();
|
250
310
|
return true;
|
@@ -307,11 +367,20 @@ export default class SelectionTool extends BaseTool {
|
|
307
367
|
}
|
308
368
|
|
309
369
|
// Get the object responsible for displaying this' selection.
|
370
|
+
// @internal
|
310
371
|
public getSelection(): Selection|null {
|
311
372
|
return this.selectionBox;
|
312
373
|
}
|
313
374
|
|
375
|
+
public getSelectedObjects(): AbstractComponent[] {
|
376
|
+
return this.selectionBox?.getSelectedObjects() ?? [];
|
377
|
+
}
|
378
|
+
|
379
|
+
// Select the given `objects`. Any non-selectable objects in `objects` are ignored.
|
314
380
|
public setSelection(objects: AbstractComponent[]) {
|
381
|
+
// Only select selectable objects.
|
382
|
+
objects = objects.filter(obj => obj.isSelectable());
|
383
|
+
|
315
384
|
let bbox: Rect2|null = null;
|
316
385
|
for (const object of objects) {
|
317
386
|
if (bbox) {
|
@@ -13,6 +13,7 @@ import TextTool from './TextTool';
|
|
13
13
|
import PipetteTool from './PipetteTool';
|
14
14
|
import ToolSwitcherShortcut from './ToolSwitcherShortcut';
|
15
15
|
import PasteHandler from './PasteHandler';
|
16
|
+
import ToolbarShortcutHandler from './ToolbarShortcutHandler';
|
16
17
|
|
17
18
|
export default class ToolController {
|
18
19
|
private tools: BaseTool[];
|
@@ -38,6 +39,7 @@ export default class ToolController {
|
|
38
39
|
new Eraser(editor, localization.eraserTool),
|
39
40
|
new SelectionTool(editor, localization.selectionTool),
|
40
41
|
new TextTool(editor, localization.textTool, localization),
|
42
|
+
new PanZoom(editor, PanZoomMode.SinglePointerGestures, localization.anyDevicePanning)
|
41
43
|
];
|
42
44
|
this.tools = [
|
43
45
|
new PipetteTool(editor, localization.pipetteTool),
|
@@ -45,6 +47,7 @@ export default class ToolController {
|
|
45
47
|
...primaryTools,
|
46
48
|
keyboardPanZoomTool,
|
47
49
|
new UndoRedoShortcut(editor),
|
50
|
+
new ToolbarShortcutHandler(editor),
|
48
51
|
new ToolSwitcherShortcut(editor),
|
49
52
|
new PasteHandler(editor),
|
50
53
|
];
|
@@ -0,0 +1,34 @@
|
|
1
|
+
// Allows the toolbar to register keyboard events.
|
2
|
+
// @packageDocumentation
|
3
|
+
|
4
|
+
import Editor from '../Editor';
|
5
|
+
import { KeyPressEvent } from '../types';
|
6
|
+
import BaseTool from './BaseTool';
|
7
|
+
|
8
|
+
// Returns true if the event was handled, false otherwise.
|
9
|
+
type KeyPressListener = (event: KeyPressEvent)=>boolean;
|
10
|
+
|
11
|
+
export default class ToolbarShortcutHandler extends BaseTool {
|
12
|
+
private listeners: Set<KeyPressListener> = new Set([]);
|
13
|
+
public constructor(editor: Editor) {
|
14
|
+
super(editor.notifier, editor.localization.changeTool);
|
15
|
+
}
|
16
|
+
|
17
|
+
public registerListener(listener: KeyPressListener) {
|
18
|
+
this.listeners.add(listener);
|
19
|
+
}
|
20
|
+
|
21
|
+
public removeListener(listener: KeyPressListener) {
|
22
|
+
this.listeners.delete(listener);
|
23
|
+
}
|
24
|
+
|
25
|
+
public onKeyPress(event: KeyPressEvent): boolean {
|
26
|
+
for (const listener of this.listeners) {
|
27
|
+
if (listener(event)) {
|
28
|
+
return true;
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
return false;
|
33
|
+
}
|
34
|
+
}
|
@@ -19,6 +19,7 @@ describe('UndoRedoShortcut', () => {
|
|
19
19
|
editor.toolController.dispatchInputEvent({
|
20
20
|
kind: InputEvtType.KeyPressEvent,
|
21
21
|
ctrlKey: true,
|
22
|
+
altKey: false,
|
22
23
|
key: 'z',
|
23
24
|
});
|
24
25
|
|
@@ -35,6 +36,7 @@ describe('UndoRedoShortcut', () => {
|
|
35
36
|
editor.toolController.dispatchInputEvent({
|
36
37
|
kind: InputEvtType.KeyPressEvent,
|
37
38
|
ctrlKey: true,
|
39
|
+
altKey: false,
|
38
40
|
key: 'z',
|
39
41
|
});
|
40
42
|
|
@@ -44,6 +46,7 @@ describe('UndoRedoShortcut', () => {
|
|
44
46
|
editor.toolController.dispatchInputEvent({
|
45
47
|
kind: InputEvtType.KeyPressEvent,
|
46
48
|
ctrlKey: true,
|
49
|
+
altKey: false,
|
47
50
|
key: 'Z',
|
48
51
|
});
|
49
52
|
|
package/src/tools/lib.ts
CHANGED
@@ -17,3 +17,4 @@ export { default as SelectionTool } from './SelectionTool/SelectionTool';
|
|
17
17
|
export { default as EraserTool } from './Eraser';
|
18
18
|
export { default as PasteHandler } from './PasteHandler';
|
19
19
|
|
20
|
+
export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
|
@@ -15,6 +15,8 @@ export interface ToolLocalization {
|
|
15
15
|
changeTool: string;
|
16
16
|
pasteHandler: string;
|
17
17
|
|
18
|
+
anyDevicePanning: string;
|
19
|
+
|
18
20
|
copied: (count: number, description: string) => string;
|
19
21
|
pasted: (count: number, description: string) => string;
|
20
22
|
|
@@ -38,6 +40,8 @@ export const defaultToolLocalization: ToolLocalization = {
|
|
38
40
|
changeTool: 'Change tool',
|
39
41
|
pasteHandler: 'Copy paste handler',
|
40
42
|
|
43
|
+
anyDevicePanning: 'Any device panning',
|
44
|
+
|
41
45
|
copied: (count: number, description: string) => `Copied ${count} ${description}`,
|
42
46
|
pasted: (count: number, description: string) => `Pasted ${count} ${description}`,
|
43
47
|
|
package/src/types.ts
CHANGED
@@ -52,14 +52,25 @@ export interface WheelEvt {
|
|
52
52
|
|
53
53
|
export interface KeyPressEvent {
|
54
54
|
readonly kind: InputEvtType.KeyPressEvent;
|
55
|
+
|
56
|
+
// key, as given by an HTML `KeyboardEvent`
|
55
57
|
readonly key: string;
|
56
|
-
|
58
|
+
|
59
|
+
// If `ctrlKey` is undefined, that is equivalent to `ctrlKey = false`.
|
60
|
+
readonly ctrlKey: boolean|undefined;
|
61
|
+
|
62
|
+
// If falsey, the `alt` key is not pressed.
|
63
|
+
readonly altKey: boolean|undefined;
|
57
64
|
}
|
58
65
|
|
59
66
|
export interface KeyUpEvent {
|
60
67
|
readonly kind: InputEvtType.KeyUpEvent;
|
61
68
|
readonly key: string;
|
62
|
-
|
69
|
+
|
70
|
+
// As in `KeyPressEvent, if `ctrlKey` is undefined, that is equivalent to
|
71
|
+
// `ctrlKey = false`.
|
72
|
+
readonly ctrlKey: boolean|undefined;
|
73
|
+
readonly altKey: boolean|undefined;
|
63
74
|
}
|
64
75
|
|
65
76
|
export interface CopyEvent {
|
@@ -101,12 +112,6 @@ export type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt
|
|
101
112
|
export type EditorNotifier = EventDispatcher<EditorEventType, EditorEventDataType>;
|
102
113
|
|
103
114
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
115
|
export enum EditorEventType {
|
111
116
|
ToolEnabled,
|
112
117
|
ToolDisabled,
|