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
@@ -4,7 +4,6 @@ import { makeLineBuilder } from '../../components/builders/LineBuilder';
|
|
4
4
|
import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../../components/builders/RectangleBuilder';
|
5
5
|
import { EditorEventType } from '../../types';
|
6
6
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
7
|
-
import { makeIconFromFactory, makePenIcon } from '../icons';
|
8
7
|
import makeColorInput from '../makeColorInput';
|
9
8
|
import BaseToolWidget from './BaseToolWidget';
|
10
9
|
export default class PenToolWidget extends BaseToolWidget {
|
@@ -55,11 +54,11 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
55
54
|
// Use a square-root scale to prevent the pen's tip from overflowing.
|
56
55
|
const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
|
57
56
|
const color = this.tool.getColor();
|
58
|
-
return makePenIcon(scale, color.toHexString());
|
57
|
+
return this.editor.icons.makePenIcon(scale, color.toHexString());
|
59
58
|
}
|
60
59
|
else {
|
61
60
|
const strokeFactory = this.tool.getStrokeFactory();
|
62
|
-
return makeIconFromFactory(this.tool, strokeFactory);
|
61
|
+
return this.editor.icons.makeIconFromFactory(this.tool, strokeFactory);
|
63
62
|
}
|
64
63
|
}
|
65
64
|
fillDropdown(dropdown) {
|
@@ -127,5 +126,19 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
127
126
|
dropdown.replaceChildren(container);
|
128
127
|
return true;
|
129
128
|
}
|
129
|
+
onKeyPress(event) {
|
130
|
+
if (!this.isSelected()) {
|
131
|
+
return false;
|
132
|
+
}
|
133
|
+
// Map alt+0-9 to different pen types.
|
134
|
+
if (/^[0-9]$/.exec(event.key) && event.ctrlKey) {
|
135
|
+
const penTypeIdx = parseInt(event.key) - 1;
|
136
|
+
if (penTypeIdx >= 0 && penTypeIdx < this.penTypes.length) {
|
137
|
+
this.tool.setStrokeFactory(this.penTypes[penTypeIdx].factory);
|
138
|
+
return true;
|
139
|
+
}
|
140
|
+
}
|
141
|
+
return false;
|
142
|
+
}
|
130
143
|
}
|
131
144
|
PenToolWidget.idCounter = 0;
|
@@ -1,10 +1,13 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
2
|
import SelectionTool from '../../tools/SelectionTool/SelectionTool';
|
3
|
+
import { KeyPressEvent } from '../../types';
|
3
4
|
import { ToolbarLocalization } from '../localization';
|
4
5
|
import BaseToolWidget from './BaseToolWidget';
|
5
6
|
export default class SelectionToolWidget extends BaseToolWidget {
|
6
7
|
private tool;
|
7
8
|
constructor(editor: Editor, tool: SelectionTool, localization: ToolbarLocalization);
|
9
|
+
private resizeImageToSelection;
|
10
|
+
protected onKeyPress(event: KeyPressEvent): boolean;
|
8
11
|
protected getTitle(): string;
|
9
12
|
protected createIcon(): Element;
|
10
13
|
}
|
@@ -1,21 +1,19 @@
|
|
1
1
|
import { EditorEventType } from '../../types';
|
2
|
-
import { makeDeleteSelectionIcon, makeDuplicateSelectionIcon, makeResizeViewportIcon, makeSelectionIcon } from '../icons';
|
3
2
|
import ActionButtonWidget from './ActionButtonWidget';
|
4
3
|
import BaseToolWidget from './BaseToolWidget';
|
5
4
|
export default class SelectionToolWidget extends BaseToolWidget {
|
6
5
|
constructor(editor, tool, localization) {
|
7
6
|
super(editor, tool, localization);
|
8
7
|
this.tool = tool;
|
9
|
-
const resizeButton = new ActionButtonWidget(editor, localization, makeResizeViewportIcon, this.localizationTable.resizeImageToSelection, () => {
|
10
|
-
|
11
|
-
this.editor.dispatch(this.editor.setImportExportRect(selection.region));
|
8
|
+
const resizeButton = new ActionButtonWidget(editor, localization, () => editor.icons.makeResizeViewportIcon(), this.localizationTable.resizeImageToSelection, () => {
|
9
|
+
this.resizeImageToSelection();
|
12
10
|
});
|
13
|
-
const deleteButton = new ActionButtonWidget(editor, localization, makeDeleteSelectionIcon, this.localizationTable.deleteSelection, () => {
|
11
|
+
const deleteButton = new ActionButtonWidget(editor, localization, () => editor.icons.makeDeleteSelectionIcon(), this.localizationTable.deleteSelection, () => {
|
14
12
|
const selection = this.tool.getSelection();
|
15
13
|
this.editor.dispatch(selection.deleteSelectedObjects());
|
16
14
|
this.tool.clearSelection();
|
17
15
|
});
|
18
|
-
const duplicateButton = new ActionButtonWidget(editor, localization, makeDuplicateSelectionIcon, this.localizationTable.duplicateSelection, () => {
|
16
|
+
const duplicateButton = new ActionButtonWidget(editor, localization, () => editor.icons.makeDuplicateSelectionIcon(), this.localizationTable.duplicateSelection, () => {
|
19
17
|
const selection = this.tool.getSelection();
|
20
18
|
this.editor.dispatch(selection.duplicateSelectedObjects());
|
21
19
|
});
|
@@ -40,10 +38,25 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
40
38
|
}
|
41
39
|
});
|
42
40
|
}
|
41
|
+
resizeImageToSelection() {
|
42
|
+
const selection = this.tool.getSelection();
|
43
|
+
if (selection) {
|
44
|
+
this.editor.dispatch(this.editor.setImportExportRect(selection.region));
|
45
|
+
}
|
46
|
+
}
|
47
|
+
onKeyPress(event) {
|
48
|
+
// Resize image to selection:
|
49
|
+
// Other keys are handled directly by the selection tool.
|
50
|
+
if (event.ctrlKey && event.key === 'r') {
|
51
|
+
this.resizeImageToSelection();
|
52
|
+
return true;
|
53
|
+
}
|
54
|
+
return false;
|
55
|
+
}
|
43
56
|
getTitle() {
|
44
57
|
return this.localizationTable.select;
|
45
58
|
}
|
46
59
|
createIcon() {
|
47
|
-
return makeSelectionIcon();
|
60
|
+
return this.editor.icons.makeSelectionIcon();
|
48
61
|
}
|
49
62
|
}
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { EditorEventType } from '../../types';
|
2
2
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
3
|
-
import { makeTextIcon } from '../icons';
|
4
3
|
import makeColorInput from '../makeColorInput';
|
5
4
|
import BaseToolWidget from './BaseToolWidget';
|
6
5
|
export default class TextToolWidget extends BaseToolWidget {
|
@@ -21,7 +20,7 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
21
20
|
}
|
22
21
|
createIcon() {
|
23
22
|
const textStyle = this.tool.getTextStyle();
|
24
|
-
return makeTextIcon(textStyle);
|
23
|
+
return this.editor.icons.makeTextIcon(textStyle);
|
25
24
|
}
|
26
25
|
fillDropdown(dropdown) {
|
27
26
|
const fontRow = document.createElement('div');
|
@@ -35,7 +35,7 @@ export default class PanZoom extends BaseTool {
|
|
35
35
|
onGestureCancel(): void;
|
36
36
|
private updateTransform;
|
37
37
|
onWheel({ delta, screenPos }: WheelEvt): boolean;
|
38
|
-
onKeyPress({ key }: KeyPressEvent): boolean;
|
38
|
+
onKeyPress({ key, ctrlKey, altKey }: KeyPressEvent): boolean;
|
39
39
|
setMode(mode: PanZoomMode): void;
|
40
40
|
getMode(): PanZoomMode;
|
41
41
|
}
|
@@ -134,10 +134,13 @@ export default class PanZoom extends BaseTool {
|
|
134
134
|
this.updateTransform(transformUpdate, true);
|
135
135
|
return true;
|
136
136
|
}
|
137
|
-
onKeyPress({ key }) {
|
137
|
+
onKeyPress({ key, ctrlKey, altKey }) {
|
138
138
|
if (!(this.mode & PanZoomMode.Keyboard)) {
|
139
139
|
return false;
|
140
140
|
}
|
141
|
+
if (ctrlKey || altKey) {
|
142
|
+
return false;
|
143
|
+
}
|
141
144
|
// No need to keep the same the transform for keyboard events.
|
142
145
|
this.transform = Viewport.transformBy(Mat33.identity);
|
143
146
|
let translation = Vec2.zero;
|
@@ -10,6 +10,8 @@ export default class SelectionTool extends BaseTool {
|
|
10
10
|
private prevSelectionBox;
|
11
11
|
private selectionBox;
|
12
12
|
private lastEvtTarget;
|
13
|
+
private expandingSelectionBox;
|
14
|
+
private shiftKeyPressed;
|
13
15
|
constructor(editor: Editor, description: string);
|
14
16
|
private makeSelectionBox;
|
15
17
|
private selectionBoxHandlingEvt;
|
@@ -26,6 +28,7 @@ export default class SelectionTool extends BaseTool {
|
|
26
28
|
onCopy(event: CopyEvent): boolean;
|
27
29
|
setEnabled(enabled: boolean): void;
|
28
30
|
getSelection(): Selection | null;
|
31
|
+
getSelectedObjects(): AbstractComponent[];
|
29
32
|
setSelection(objects: AbstractComponent[]): void;
|
30
33
|
clearSelection(): void;
|
31
34
|
}
|
@@ -16,6 +16,8 @@ export default class SelectionTool extends BaseTool {
|
|
16
16
|
super(editor.notifier, description);
|
17
17
|
this.editor = editor;
|
18
18
|
this.lastEvtTarget = null;
|
19
|
+
this.expandingSelectionBox = false;
|
20
|
+
this.shiftKeyPressed = false;
|
19
21
|
this.selectionBoxHandlingEvt = false;
|
20
22
|
this.handleOverlay = document.createElement('div');
|
21
23
|
editor.createHTMLOverlay(this.handleOverlay);
|
@@ -34,10 +36,13 @@ export default class SelectionTool extends BaseTool {
|
|
34
36
|
});
|
35
37
|
}
|
36
38
|
makeSelectionBox(selectionStartPos) {
|
39
|
+
var _a;
|
37
40
|
this.prevSelectionBox = this.selectionBox;
|
38
41
|
this.selectionBox = new Selection(selectionStartPos, this.editor);
|
39
|
-
|
40
|
-
|
42
|
+
if (!this.expandingSelectionBox) {
|
43
|
+
// Remove any previous selection rects
|
44
|
+
(_a = this.prevSelectionBox) === null || _a === void 0 ? void 0 : _a.cancelSelection();
|
45
|
+
}
|
41
46
|
this.selectionBox.addTo(this.handleOverlay);
|
42
47
|
}
|
43
48
|
onPointerDown(event) {
|
@@ -45,8 +50,11 @@ export default class SelectionTool extends BaseTool {
|
|
45
50
|
if (event.allPointers.length === 1 && event.current.isPrimary) {
|
46
51
|
if (this.lastEvtTarget && ((_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.onDragStart(event.current, this.lastEvtTarget))) {
|
47
52
|
this.selectionBoxHandlingEvt = true;
|
53
|
+
this.expandingSelectionBox = false;
|
48
54
|
}
|
49
55
|
else {
|
56
|
+
// Shift key: Combine the new and old selection boxes at the end of the gesture.
|
57
|
+
this.expandingSelectionBox = this.shiftKeyPressed;
|
50
58
|
this.makeSelectionBox(event.current.canvasPos);
|
51
59
|
}
|
52
60
|
return true;
|
@@ -81,6 +89,7 @@ export default class SelectionTool extends BaseTool {
|
|
81
89
|
this.selectionBox = null;
|
82
90
|
}
|
83
91
|
}
|
92
|
+
// Called after a gestureCancel and a pointerUp
|
84
93
|
onGestureEnd() {
|
85
94
|
this.lastEvtTarget = null;
|
86
95
|
if (!this.selectionBox)
|
@@ -105,7 +114,19 @@ export default class SelectionTool extends BaseTool {
|
|
105
114
|
if (!this.selectionBox)
|
106
115
|
return;
|
107
116
|
this.selectionBox.setToPoint(event.current.canvasPos);
|
108
|
-
|
117
|
+
// Were we expanding the previous selection?
|
118
|
+
if (this.expandingSelectionBox && this.prevSelectionBox) {
|
119
|
+
// If so, finish expanding.
|
120
|
+
this.expandingSelectionBox = false;
|
121
|
+
this.selectionBox.resolveToObjects();
|
122
|
+
this.setSelection([
|
123
|
+
...this.selectionBox.getSelectedObjects(),
|
124
|
+
...this.prevSelectionBox.getSelectedObjects(),
|
125
|
+
]);
|
126
|
+
}
|
127
|
+
else {
|
128
|
+
this.onGestureEnd();
|
129
|
+
}
|
109
130
|
}
|
110
131
|
onGestureCancel() {
|
111
132
|
var _a, _b, _c;
|
@@ -118,8 +139,28 @@ export default class SelectionTool extends BaseTool {
|
|
118
139
|
this.selectionBox = this.prevSelectionBox;
|
119
140
|
(_c = this.selectionBox) === null || _c === void 0 ? void 0 : _c.addTo(this.handleOverlay);
|
120
141
|
}
|
142
|
+
this.expandingSelectionBox = false;
|
121
143
|
}
|
122
144
|
onKeyPress(event) {
|
145
|
+
if (this.selectionBox && event.ctrlKey && event.key === 'd') {
|
146
|
+
// Handle duplication on key up — we don't want to accidentally duplicate
|
147
|
+
// many times.
|
148
|
+
return true;
|
149
|
+
}
|
150
|
+
else if (event.key === 'a' && event.ctrlKey) {
|
151
|
+
// Handle ctrl+A on key up.
|
152
|
+
// Return early to prevent 'a' from moving the selection/view.
|
153
|
+
return true;
|
154
|
+
}
|
155
|
+
else if (event.ctrlKey) {
|
156
|
+
// Don't transform the selection with, for example, ctrl+i.
|
157
|
+
// Pass it to another tool, if apliccable.
|
158
|
+
return false;
|
159
|
+
}
|
160
|
+
else if (event.key === 'Shift') {
|
161
|
+
this.shiftKeyPressed = true;
|
162
|
+
return true;
|
163
|
+
}
|
123
164
|
let rotationSteps = 0;
|
124
165
|
let xTranslateSteps = 0;
|
125
166
|
let yTranslateSteps = 0;
|
@@ -194,6 +235,20 @@ export default class SelectionTool extends BaseTool {
|
|
194
235
|
return handled;
|
195
236
|
}
|
196
237
|
onKeyUp(evt) {
|
238
|
+
if (evt.key === 'Shift') {
|
239
|
+
this.shiftKeyPressed = false;
|
240
|
+
return true;
|
241
|
+
}
|
242
|
+
else if (evt.ctrlKey) {
|
243
|
+
if (this.selectionBox && evt.key === 'd') {
|
244
|
+
this.editor.dispatch(this.selectionBox.duplicateSelectedObjects());
|
245
|
+
return true;
|
246
|
+
}
|
247
|
+
else if (evt.key === 'a') {
|
248
|
+
this.setSelection(this.editor.image.getAllElements());
|
249
|
+
return true;
|
250
|
+
}
|
251
|
+
}
|
197
252
|
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
|
198
253
|
this.selectionBox.finalizeTransform();
|
199
254
|
return true;
|
@@ -244,10 +299,18 @@ export default class SelectionTool extends BaseTool {
|
|
244
299
|
}
|
245
300
|
}
|
246
301
|
// Get the object responsible for displaying this' selection.
|
302
|
+
// @internal
|
247
303
|
getSelection() {
|
248
304
|
return this.selectionBox;
|
249
305
|
}
|
306
|
+
getSelectedObjects() {
|
307
|
+
var _a, _b;
|
308
|
+
return (_b = (_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.getSelectedObjects()) !== null && _b !== void 0 ? _b : [];
|
309
|
+
}
|
310
|
+
// Select the given `objects`. Any non-selectable objects in `objects` are ignored.
|
250
311
|
setSelection(objects) {
|
312
|
+
// Only select selectable objects.
|
313
|
+
objects = objects.filter(obj => obj.isSelectable());
|
251
314
|
let bbox = null;
|
252
315
|
for (const object of objects) {
|
253
316
|
if (bbox) {
|
@@ -10,6 +10,7 @@ import TextTool from './TextTool';
|
|
10
10
|
import PipetteTool from './PipetteTool';
|
11
11
|
import ToolSwitcherShortcut from './ToolSwitcherShortcut';
|
12
12
|
import PasteHandler from './PasteHandler';
|
13
|
+
import ToolbarShortcutHandler from './ToolbarShortcutHandler';
|
13
14
|
export default class ToolController {
|
14
15
|
/** @internal */
|
15
16
|
constructor(editor, localization) {
|
@@ -28,6 +29,7 @@ export default class ToolController {
|
|
28
29
|
new Eraser(editor, localization.eraserTool),
|
29
30
|
new SelectionTool(editor, localization.selectionTool),
|
30
31
|
new TextTool(editor, localization.textTool, localization),
|
32
|
+
new PanZoom(editor, PanZoomMode.SinglePointerGestures, localization.anyDevicePanning)
|
31
33
|
];
|
32
34
|
this.tools = [
|
33
35
|
new PipetteTool(editor, localization.pipetteTool),
|
@@ -35,6 +37,7 @@ export default class ToolController {
|
|
35
37
|
...primaryTools,
|
36
38
|
keyboardPanZoomTool,
|
37
39
|
new UndoRedoShortcut(editor),
|
40
|
+
new ToolbarShortcutHandler(editor),
|
38
41
|
new ToolSwitcherShortcut(editor),
|
39
42
|
new PasteHandler(editor),
|
40
43
|
];
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { KeyPressEvent } from '../types';
|
3
|
+
import BaseTool from './BaseTool';
|
4
|
+
declare type KeyPressListener = (event: KeyPressEvent) => boolean;
|
5
|
+
export default class ToolbarShortcutHandler extends BaseTool {
|
6
|
+
private listeners;
|
7
|
+
constructor(editor: Editor);
|
8
|
+
registerListener(listener: KeyPressListener): void;
|
9
|
+
removeListener(listener: KeyPressListener): void;
|
10
|
+
onKeyPress(event: KeyPressEvent): boolean;
|
11
|
+
}
|
12
|
+
export {};
|
@@ -0,0 +1,23 @@
|
|
1
|
+
// Allows the toolbar to register keyboard events.
|
2
|
+
// @packageDocumentation
|
3
|
+
import BaseTool from './BaseTool';
|
4
|
+
export default class ToolbarShortcutHandler extends BaseTool {
|
5
|
+
constructor(editor) {
|
6
|
+
super(editor.notifier, editor.localization.changeTool);
|
7
|
+
this.listeners = new Set([]);
|
8
|
+
}
|
9
|
+
registerListener(listener) {
|
10
|
+
this.listeners.add(listener);
|
11
|
+
}
|
12
|
+
removeListener(listener) {
|
13
|
+
this.listeners.delete(listener);
|
14
|
+
}
|
15
|
+
onKeyPress(event) {
|
16
|
+
for (const listener of this.listeners) {
|
17
|
+
if (listener(event)) {
|
18
|
+
return true;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
return false;
|
22
|
+
}
|
23
|
+
}
|
package/dist/src/tools/lib.d.ts
CHANGED
@@ -12,3 +12,4 @@ export { default as TextTool } from './TextTool';
|
|
12
12
|
export { default as SelectionTool } from './SelectionTool/SelectionTool';
|
13
13
|
export { default as EraserTool } from './Eraser';
|
14
14
|
export { default as PasteHandler } from './PasteHandler';
|
15
|
+
export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
|
package/dist/src/tools/lib.js
CHANGED
@@ -12,3 +12,4 @@ export { default as TextTool } from './TextTool';
|
|
12
12
|
export { default as SelectionTool } from './SelectionTool/SelectionTool';
|
13
13
|
export { default as EraserTool } from './Eraser';
|
14
14
|
export { default as PasteHandler } from './PasteHandler';
|
15
|
+
export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
|
@@ -12,6 +12,7 @@ export interface ToolLocalization {
|
|
12
12
|
enterTextToInsert: string;
|
13
13
|
changeTool: string;
|
14
14
|
pasteHandler: string;
|
15
|
+
anyDevicePanning: string;
|
15
16
|
copied: (count: number, description: string) => string;
|
16
17
|
pasted: (count: number, description: string) => string;
|
17
18
|
toolEnabledAnnouncement: (toolName: string) => string;
|
@@ -12,6 +12,7 @@ export const defaultToolLocalization = {
|
|
12
12
|
enterTextToInsert: 'Text to insert',
|
13
13
|
changeTool: 'Change tool',
|
14
14
|
pasteHandler: 'Copy paste handler',
|
15
|
+
anyDevicePanning: 'Any device panning',
|
15
16
|
copied: (count, description) => `Copied ${count} ${description}`,
|
16
17
|
pasted: (count, description) => `Pasted ${count} ${description}`,
|
17
18
|
toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
|
package/dist/src/types.d.ts
CHANGED
@@ -34,12 +34,14 @@ export interface WheelEvt {
|
|
34
34
|
export interface KeyPressEvent {
|
35
35
|
readonly kind: InputEvtType.KeyPressEvent;
|
36
36
|
readonly key: string;
|
37
|
-
readonly ctrlKey: boolean;
|
37
|
+
readonly ctrlKey: boolean | undefined;
|
38
|
+
readonly altKey: boolean | undefined;
|
38
39
|
}
|
39
40
|
export interface KeyUpEvent {
|
40
41
|
readonly kind: InputEvtType.KeyUpEvent;
|
41
42
|
readonly key: string;
|
42
|
-
readonly ctrlKey: boolean;
|
43
|
+
readonly ctrlKey: boolean | undefined;
|
44
|
+
readonly altKey: boolean | undefined;
|
43
45
|
}
|
44
46
|
export interface CopyEvent {
|
45
47
|
readonly kind: InputEvtType.CopyEvent;
|
package/package.json
CHANGED
package/src/Editor.ts
CHANGED
@@ -37,6 +37,7 @@ import Mat33 from './math/Mat33';
|
|
37
37
|
import Rect2 from './math/Rect2';
|
38
38
|
import { EditorLocalization } from './localization';
|
39
39
|
import getLocalizationTable from './localizations/getLocalizationTable';
|
40
|
+
import IconProvider from './toolbar/IconProvider';
|
40
41
|
|
41
42
|
type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
|
42
43
|
type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
|
@@ -58,6 +59,8 @@ export interface EditorSettings {
|
|
58
59
|
/** Minimum zoom fraction (e.g. 0.5 → 50% zoom). */
|
59
60
|
minZoom: number,
|
60
61
|
maxZoom: number,
|
62
|
+
|
63
|
+
iconProvider: IconProvider,
|
61
64
|
}
|
62
65
|
|
63
66
|
// { @inheritDoc Editor! }
|
@@ -101,22 +104,23 @@ export class Editor {
|
|
101
104
|
* editor.dispatch(addElementCommand);
|
102
105
|
* ```
|
103
106
|
*/
|
104
|
-
public image: EditorImage;
|
107
|
+
public readonly image: EditorImage;
|
105
108
|
|
106
109
|
/** Viewport for the exported/imported image. */
|
107
110
|
private importExportViewport: Viewport;
|
108
111
|
|
109
112
|
/** @internal */
|
110
|
-
public localization: EditorLocalization;
|
113
|
+
public readonly localization: EditorLocalization;
|
111
114
|
|
112
|
-
public
|
113
|
-
public
|
115
|
+
public readonly icons: IconProvider;
|
116
|
+
public readonly viewport: Viewport;
|
117
|
+
public readonly toolController: ToolController;
|
114
118
|
|
115
119
|
/**
|
116
120
|
* Global event dispatcher/subscriber.
|
117
121
|
* @see {@link types.EditorEventType}
|
118
122
|
*/
|
119
|
-
public notifier: EditorNotifier;
|
123
|
+
public readonly notifier: EditorNotifier;
|
120
124
|
|
121
125
|
private loadingWarning: HTMLElement;
|
122
126
|
private accessibilityAnnounceArea: HTMLElement;
|
@@ -164,7 +168,9 @@ export class Editor {
|
|
164
168
|
localization: this.localization,
|
165
169
|
minZoom: settings.minZoom ?? 2e-10,
|
166
170
|
maxZoom: settings.maxZoom ?? 1e12,
|
171
|
+
iconProvider: settings.iconProvider ?? new IconProvider(),
|
167
172
|
};
|
173
|
+
this.icons = this.settings.iconProvider;
|
168
174
|
|
169
175
|
this.container = document.createElement('div');
|
170
176
|
this.renderingRegion = document.createElement('div');
|
@@ -586,6 +592,7 @@ export class Editor {
|
|
586
592
|
kind: InputEvtType.KeyPressEvent,
|
587
593
|
key: evt.key,
|
588
594
|
ctrlKey: evt.ctrlKey,
|
595
|
+
altKey: evt.altKey,
|
589
596
|
})) {
|
590
597
|
evt.preventDefault();
|
591
598
|
} else if (evt.key === 'Escape') {
|
@@ -598,6 +605,7 @@ export class Editor {
|
|
598
605
|
kind: InputEvtType.KeyUpEvent,
|
599
606
|
key: evt.key,
|
600
607
|
ctrlKey: evt.ctrlKey,
|
608
|
+
altKey: evt.altKey,
|
601
609
|
})) {
|
602
610
|
evt.preventDefault();
|
603
611
|
}
|
@@ -783,12 +791,14 @@ export class Editor {
|
|
783
791
|
public sendKeyboardEvent(
|
784
792
|
eventType: InputEvtType.KeyPressEvent|InputEvtType.KeyUpEvent,
|
785
793
|
key: string,
|
786
|
-
ctrlKey: boolean = false
|
794
|
+
ctrlKey: boolean = false,
|
795
|
+
altKey: boolean = false,
|
787
796
|
) {
|
788
797
|
this.toolController.dispatchInputEvent({
|
789
798
|
kind: eventType,
|
790
799
|
key,
|
791
|
-
ctrlKey
|
800
|
+
ctrlKey,
|
801
|
+
altKey,
|
792
802
|
});
|
793
803
|
}
|
794
804
|
|
package/src/EditorImage.ts
CHANGED
@@ -54,6 +54,15 @@ export default class EditorImage {
|
|
54
54
|
}
|
55
55
|
}
|
56
56
|
|
57
|
+
/** @returns all elements in the image, sorted by z-index. This can be slow for large images. */
|
58
|
+
public getAllElements() {
|
59
|
+
const leaves = this.root.getLeaves();
|
60
|
+
sortLeavesByZIndex(leaves);
|
61
|
+
|
62
|
+
return leaves.map(leaf => leaf.getContent()!);
|
63
|
+
}
|
64
|
+
|
65
|
+
/** @returns a list of `AbstractComponent`s intersecting `region`, sorted by z-index. */
|
57
66
|
public getElementsIntersectingRegion(region: Rect2): AbstractComponent[] {
|
58
67
|
const leaves = this.root.getLeavesIntersectingRegion(region);
|
59
68
|
sortLeavesByZIndex(leaves);
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import { Rect2, TextComponent, Vec2 } from './lib';
|
2
|
+
import SVGLoader from './SVGLoader';
|
3
|
+
import createEditor from './testing/createEditor';
|
4
|
+
|
5
|
+
describe('SVGLoader', () => {
|
6
|
+
it('should correctly load x/y-positioned text nodes', async () => {
|
7
|
+
const editor = createEditor();
|
8
|
+
await editor.loadFrom(SVGLoader.fromString(`
|
9
|
+
<svg>
|
10
|
+
<text>Testing...</text>
|
11
|
+
<text y=100>Test 2...</text>
|
12
|
+
<text x=100>Test 3...</text>
|
13
|
+
<text x=100 y=100>Test 3...</text>
|
14
|
+
|
15
|
+
<!-- Transform matrix: translate by (100,0) -->
|
16
|
+
<text style='transform: matrix(1,0,0,1,100,0);'>Test 3...</text>
|
17
|
+
</svg>
|
18
|
+
`, true));
|
19
|
+
const elems = editor.image
|
20
|
+
.getElementsIntersectingRegion(new Rect2(-1000, -1000, 10000, 10000))
|
21
|
+
.filter(elem => elem instanceof TextComponent);
|
22
|
+
expect(elems).toHaveLength(5);
|
23
|
+
const topLefts = elems.map(elem => elem.getBBox().topLeft);
|
24
|
+
|
25
|
+
// Top-left of Testing... should be (0, 0) ± 10 pixels (objects are aligned based on baseline)
|
26
|
+
expect(topLefts[0]).objEq(Vec2.of(0, 0), 10);
|
27
|
+
|
28
|
+
expect(topLefts[1].y - topLefts[0].y).toBe(100);
|
29
|
+
expect(topLefts[1].x - topLefts[0].x).toBe(0);
|
30
|
+
|
31
|
+
expect(topLefts[2].y - topLefts[0].y).toBe(0);
|
32
|
+
expect(topLefts[2].x - topLefts[0].x).toBe(100);
|
33
|
+
|
34
|
+
expect(topLefts[4].x - topLefts[0].x).toBe(100);
|
35
|
+
expect(topLefts[4].y - topLefts[0].y).toBe(0);
|
36
|
+
});
|
37
|
+
});
|
package/src/SVGLoader.ts
CHANGED
@@ -124,7 +124,7 @@ export default class SVGLoader implements ImageLoader {
|
|
124
124
|
);
|
125
125
|
}
|
126
126
|
|
127
|
-
if (supportedStyleAttrs) {
|
127
|
+
if (supportedStyleAttrs && node.style) {
|
128
128
|
for (const attr of node.style) {
|
129
129
|
if (attr === '' || !attr) {
|
130
130
|
continue;
|
@@ -198,9 +198,9 @@ export default class SVGLoader implements ImageLoader {
|
|
198
198
|
|
199
199
|
const elemX = elem.getAttribute('x');
|
200
200
|
const elemY = elem.getAttribute('y');
|
201
|
-
if (elemX
|
202
|
-
const x = parseFloat(elemX);
|
203
|
-
const y = parseFloat(elemY);
|
201
|
+
if (elemX || elemY) {
|
202
|
+
const x = parseFloat(elemX ?? '0');
|
203
|
+
const y = parseFloat(elemY ?? '0');
|
204
204
|
if (!isNaN(x) && !isNaN(y)) {
|
205
205
|
supportedAttrs?.push('x', 'y');
|
206
206
|
transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
|
@@ -245,7 +245,7 @@ export default class SVGLoader implements ImageLoader {
|
|
245
245
|
size: fontSize,
|
246
246
|
fontFamily: computedStyles.fontFamily || elem.style.fontFamily || 'sans-serif',
|
247
247
|
renderingStyle: {
|
248
|
-
fill: Color4.fromString(computedStyles.fill)
|
248
|
+
fill: Color4.fromString(computedStyles.fill || elem.style.fill || '#000')
|
249
249
|
},
|
250
250
|
};
|
251
251
|
|
@@ -406,7 +406,6 @@ export default class SVGLoader implements ImageLoader {
|
|
406
406
|
this.onFinish?.();
|
407
407
|
}
|
408
408
|
|
409
|
-
// TODO: Handling unsafe data! Tripple-check that this is secure!
|
410
409
|
// @param sanitize - if `true`, don't store unknown attributes.
|
411
410
|
public static fromString(text: string, sanitize: boolean = false): SVGLoader {
|
412
411
|
const sandbox = document.createElement('iframe');
|
@@ -89,6 +89,11 @@ export default abstract class AbstractComponent {
|
|
89
89
|
return new AbstractComponent.TransformElementCommand(affineTransfm, this);
|
90
90
|
}
|
91
91
|
|
92
|
+
// @returns true iff this component can be selected (e.g. by the selection tool.)
|
93
|
+
public isSelectable(): boolean {
|
94
|
+
return true;
|
95
|
+
}
|
96
|
+
|
92
97
|
private static transformElementCommandId = 'transform-element';
|
93
98
|
|
94
99
|
private static UnresolvedTransformElementCommand = class extends SerializableCommand {
|
@@ -43,6 +43,10 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
|
|
43
43
|
protected applyTransformation(_affineTransfm: Mat33): void {
|
44
44
|
}
|
45
45
|
|
46
|
+
public isSelectable() {
|
47
|
+
return false;
|
48
|
+
}
|
49
|
+
|
46
50
|
protected createClone() {
|
47
51
|
return new SVGGlobalAttributesObject(this.attrs);
|
48
52
|
}
|
@@ -1,21 +1,8 @@
|
|
1
1
|
import Color4 from '../Color4';
|
2
2
|
import Mat33 from '../math/Mat33';
|
3
|
-
import Rect2 from '../math/Rect2';
|
4
3
|
import AbstractComponent from './AbstractComponent';
|
5
4
|
import Text, { TextStyle } from './Text';
|
6
5
|
|
7
|
-
const estimateTextBounds = (text: string, style: TextStyle): Rect2 => {
|
8
|
-
const widthEst = text.length * style.size;
|
9
|
-
const heightEst = style.size;
|
10
|
-
|
11
|
-
// Text is drawn with (0, 0) as its baseline. As such, the majority of the text's height should
|
12
|
-
// be above (0, 0).
|
13
|
-
return new Rect2(0, -heightEst * 2/3, widthEst, heightEst);
|
14
|
-
};
|
15
|
-
|
16
|
-
// Don't use the default Canvas-based text bounding code. The canvas-based code may not work
|
17
|
-
// with jsdom.
|
18
|
-
AbstractComponent.registerComponent('text', (data: string) => Text.deserializeFromString(data, estimateTextBounds));
|
19
6
|
|
20
7
|
describe('Text', () => {
|
21
8
|
it('should be serializable', () => {
|
@@ -24,9 +11,7 @@ describe('Text', () => {
|
|
24
11
|
fontFamily: 'serif',
|
25
12
|
renderingStyle: { fill: Color4.black },
|
26
13
|
};
|
27
|
-
const text = new Text(
|
28
|
-
[ 'Foo' ], Mat33.identity, style, estimateTextBounds
|
29
|
-
);
|
14
|
+
const text = new Text([ 'Foo' ], Mat33.identity, style);
|
30
15
|
const serialized = text.serialize();
|
31
16
|
const deserialized = AbstractComponent.deserialize(serialized) as Text;
|
32
17
|
expect(deserialized.getBBox()).objEq(text.getBBox());
|