js-draw 0.0.6 → 0.0.9
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 +7 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.js +1 -0
- package/dist/src/EditorImage.d.ts +2 -2
- package/dist/src/SVGLoader.d.ts +2 -0
- package/dist/src/SVGLoader.js +12 -0
- package/dist/src/UndoRedoHistory.d.ts +2 -0
- package/dist/src/UndoRedoHistory.js +6 -0
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +15 -0
- package/dist/src/components/SVGGlobalAttributesObject.js +29 -0
- package/dist/src/rendering/SVGRenderer.d.ts +2 -0
- package/dist/src/rendering/SVGRenderer.js +25 -0
- package/dist/src/testing/createEditor.d.ts +3 -0
- package/dist/src/testing/createEditor.js +3 -0
- package/dist/src/tools/BaseTool.d.ts +4 -4
- package/dist/src/tools/BaseTool.js +4 -0
- package/dist/src/tools/ToolController.d.ts +2 -1
- package/dist/src/tools/ToolController.js +3 -0
- package/dist/src/tools/UndoRedoShortcut.d.ts +10 -0
- package/dist/src/tools/UndoRedoShortcut.js +23 -0
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/dist/src/types.d.ts +1 -0
- package/package.json +2 -2
- package/src/Editor.ts +1 -0
- package/src/EditorImage.test.ts +1 -4
- package/src/SVGLoader.ts +13 -0
- package/src/UndoRedoHistory.ts +8 -0
- package/src/components/SVGGlobalAttributesObject.ts +39 -0
- package/src/rendering/SVGRenderer.ts +27 -0
- package/src/testing/createEditor.ts +4 -0
- package/src/tools/BaseTool.ts +5 -4
- package/src/tools/ToolController.ts +3 -0
- package/src/tools/UndoRedoShortcut.test.ts +53 -0
- package/src/tools/UndoRedoShortcut.ts +28 -0
- package/src/tools/localization.ts +2 -0
- package/src/types.ts +1 -0
package/dist/src/Editor.js
CHANGED
@@ -15,8 +15,8 @@ export default class EditorImage {
|
|
15
15
|
getElementsIntersectingRegion(region: Rect2): AbstractComponent[];
|
16
16
|
static AddElementCommand: {
|
17
17
|
new (element: AbstractComponent, applyByFlattening?: boolean): {
|
18
|
-
readonly "__#
|
19
|
-
"__#
|
18
|
+
readonly "__#679@#element": AbstractComponent;
|
19
|
+
"__#679@#applyByFlattening": boolean;
|
20
20
|
apply(editor: Editor): void;
|
21
21
|
unapply(editor: Editor): void;
|
22
22
|
description(localization: EditorLocalization): string;
|
package/dist/src/SVGLoader.d.ts
CHANGED
@@ -15,7 +15,9 @@ export default class SVGLoader implements ImageLoader {
|
|
15
15
|
private addPath;
|
16
16
|
private addUnknownNode;
|
17
17
|
private updateViewBox;
|
18
|
+
private updateSVGAttrs;
|
18
19
|
private visit;
|
20
|
+
private getSourceAttrs;
|
19
21
|
start(onAddComponent: ComponentAddedListener, onProgress: OnProgressListener): Promise<Rect2>;
|
20
22
|
static fromString(text: string): SVGLoader;
|
21
23
|
}
|
package/dist/src/SVGLoader.js
CHANGED
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
9
9
|
};
|
10
10
|
import Color4 from './Color4';
|
11
11
|
import Stroke from './components/Stroke';
|
12
|
+
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
12
13
|
import UnknownSVGObject from './components/UnknownSVGObject';
|
13
14
|
import Path from './geometry/Path';
|
14
15
|
import Rect2 from './geometry/Rect2';
|
@@ -113,6 +114,10 @@ export default class SVGLoader {
|
|
113
114
|
}
|
114
115
|
this.rootViewBox = new Rect2(x, y, width, height);
|
115
116
|
}
|
117
|
+
updateSVGAttrs(node) {
|
118
|
+
var _a;
|
119
|
+
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
|
120
|
+
}
|
116
121
|
visit(node) {
|
117
122
|
var _a;
|
118
123
|
return __awaiter(this, void 0, void 0, function* () {
|
@@ -126,6 +131,7 @@ export default class SVGLoader {
|
|
126
131
|
break;
|
127
132
|
case 'svg':
|
128
133
|
this.updateViewBox(node);
|
134
|
+
this.updateSVGAttrs(node);
|
129
135
|
break;
|
130
136
|
default:
|
131
137
|
console.warn('Unknown SVG element,', node);
|
@@ -142,6 +148,12 @@ export default class SVGLoader {
|
|
142
148
|
yield ((_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, this.processedCount, this.totalToProcess));
|
143
149
|
});
|
144
150
|
}
|
151
|
+
// Get SVG element attributes (e.g. xlink=...)
|
152
|
+
getSourceAttrs(node) {
|
153
|
+
return node.getAttributeNames().map(attr => {
|
154
|
+
return [attr, node.getAttribute(attr)];
|
155
|
+
});
|
156
|
+
}
|
145
157
|
start(onAddComponent, onProgress) {
|
146
158
|
var _a;
|
147
159
|
return __awaiter(this, void 0, void 0, function* () {
|
package/dist/src/Viewport.d.ts
CHANGED
@@ -10,7 +10,7 @@ export declare class Viewport {
|
|
10
10
|
private notifier;
|
11
11
|
static ViewportTransform: {
|
12
12
|
new (transform: Mat33): {
|
13
|
-
readonly "__#
|
13
|
+
readonly "__#678@#inverseTransform": Mat33;
|
14
14
|
readonly transform: Mat33;
|
15
15
|
apply(editor: Editor): void;
|
16
16
|
unapply(editor: Editor): void;
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import LineSegment2 from '../geometry/LineSegment2';
|
2
|
+
import Mat33 from '../geometry/Mat33';
|
3
|
+
import Rect2 from '../geometry/Rect2';
|
4
|
+
import AbstractRenderer from '../rendering/AbstractRenderer';
|
5
|
+
import AbstractComponent from './AbstractComponent';
|
6
|
+
import { ImageComponentLocalization } from './localization';
|
7
|
+
export default class SVGGlobalAttributesObject extends AbstractComponent {
|
8
|
+
private readonly attrs;
|
9
|
+
protected contentBBox: Rect2;
|
10
|
+
constructor(attrs: Array<[string, string | null]>);
|
11
|
+
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
12
|
+
intersects(_lineSegment: LineSegment2): boolean;
|
13
|
+
protected applyTransformation(_affineTransfm: Mat33): void;
|
14
|
+
description(localization: ImageComponentLocalization): string;
|
15
|
+
}
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import Rect2 from '../geometry/Rect2';
|
2
|
+
import SVGRenderer from '../rendering/SVGRenderer';
|
3
|
+
import AbstractComponent from './AbstractComponent';
|
4
|
+
// Stores global SVG attributes (e.g. namespace identifiers.)
|
5
|
+
export default class SVGGlobalAttributesObject extends AbstractComponent {
|
6
|
+
constructor(attrs) {
|
7
|
+
super();
|
8
|
+
this.attrs = attrs;
|
9
|
+
this.contentBBox = Rect2.empty;
|
10
|
+
}
|
11
|
+
render(canvas, _visibleRect) {
|
12
|
+
if (!(canvas instanceof SVGRenderer)) {
|
13
|
+
// Don't draw unrenderable objects if we can't
|
14
|
+
return;
|
15
|
+
}
|
16
|
+
console.log('Rendering to SVG.', this.attrs);
|
17
|
+
for (const [attr, value] of this.attrs) {
|
18
|
+
canvas.setRootSVGAttribute(attr, value);
|
19
|
+
}
|
20
|
+
}
|
21
|
+
intersects(_lineSegment) {
|
22
|
+
return false;
|
23
|
+
}
|
24
|
+
applyTransformation(_affineTransfm) {
|
25
|
+
}
|
26
|
+
description(localization) {
|
27
|
+
return localization.svgObject;
|
28
|
+
}
|
29
|
+
}
|
@@ -10,7 +10,9 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
10
10
|
private lastPath;
|
11
11
|
private lastPathStart;
|
12
12
|
private mainGroup;
|
13
|
+
private overwrittenAttrs;
|
13
14
|
constructor(elem: SVGSVGElement, viewport: Viewport);
|
15
|
+
setRootSVGAttribute(name: string, value: string | null): void;
|
14
16
|
displaySize(): Vec2;
|
15
17
|
clear(): void;
|
16
18
|
protected beginPath(startPoint: Point2): void;
|
@@ -6,13 +6,38 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
6
6
|
constructor(elem, viewport) {
|
7
7
|
super(viewport);
|
8
8
|
this.elem = elem;
|
9
|
+
this.overwrittenAttrs = {};
|
9
10
|
this.clear();
|
10
11
|
}
|
12
|
+
// Sets an attribute on the root SVG element.
|
13
|
+
setRootSVGAttribute(name, value) {
|
14
|
+
// Make the original value of the attribute restorable on clear
|
15
|
+
if (!(name in this.overwrittenAttrs)) {
|
16
|
+
this.overwrittenAttrs[name] = this.elem.getAttribute(name);
|
17
|
+
}
|
18
|
+
if (value !== null) {
|
19
|
+
this.elem.setAttribute(name, value);
|
20
|
+
}
|
21
|
+
else {
|
22
|
+
this.elem.removeAttribute(name);
|
23
|
+
}
|
24
|
+
}
|
11
25
|
displaySize() {
|
12
26
|
return Vec2.of(this.elem.clientWidth, this.elem.clientHeight);
|
13
27
|
}
|
14
28
|
clear() {
|
15
29
|
this.mainGroup = document.createElementNS(svgNameSpace, 'g');
|
30
|
+
// Restore all alltributes
|
31
|
+
for (const attrName in this.overwrittenAttrs) {
|
32
|
+
const value = this.overwrittenAttrs[attrName];
|
33
|
+
if (value) {
|
34
|
+
this.elem.setAttribute(attrName, value);
|
35
|
+
}
|
36
|
+
else {
|
37
|
+
this.elem.removeAttribute(attrName);
|
38
|
+
}
|
39
|
+
}
|
40
|
+
this.overwrittenAttrs = {};
|
16
41
|
// Remove all children
|
17
42
|
this.elem.replaceChildren(this.mainGroup);
|
18
43
|
}
|
@@ -6,10 +6,10 @@ export default abstract class BaseTool implements PointerEvtListener {
|
|
6
6
|
readonly description: string;
|
7
7
|
private enabled;
|
8
8
|
private group;
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
onPointerDown(_event: PointerEvt): boolean;
|
10
|
+
onPointerMove(_event: PointerEvt): void;
|
11
|
+
onPointerUp(_event: PointerEvt): void;
|
12
|
+
onGestureCancel(): void;
|
13
13
|
abstract readonly kind: ToolType;
|
14
14
|
protected constructor(notifier: EditorNotifier, description: string);
|
15
15
|
onWheel(_event: WheelEvt): boolean;
|
@@ -5,6 +5,7 @@ import ToolEnabledGroup from './ToolEnabledGroup';
|
|
5
5
|
import Eraser from './Eraser';
|
6
6
|
import SelectionTool from './SelectionTool';
|
7
7
|
import Color4 from '../Color4';
|
8
|
+
import UndoRedoShortcut from './UndoRedoShortcut';
|
8
9
|
export var ToolType;
|
9
10
|
(function (ToolType) {
|
10
11
|
ToolType[ToolType["TouchPanZoom"] = 0] = "TouchPanZoom";
|
@@ -12,6 +13,7 @@ export var ToolType;
|
|
12
13
|
ToolType[ToolType["Selection"] = 2] = "Selection";
|
13
14
|
ToolType[ToolType["Eraser"] = 3] = "Eraser";
|
14
15
|
ToolType[ToolType["PanZoom"] = 4] = "PanZoom";
|
16
|
+
ToolType[ToolType["UndoRedoShortcut"] = 5] = "UndoRedoShortcut";
|
15
17
|
})(ToolType || (ToolType = {}));
|
16
18
|
export default class ToolController {
|
17
19
|
constructor(editor, localization) {
|
@@ -31,6 +33,7 @@ export default class ToolController {
|
|
31
33
|
touchPanZoom,
|
32
34
|
...primaryTools,
|
33
35
|
new PanZoom(editor, PanZoomMode.TwoFingerGestures | PanZoomMode.AnyDevice, localization.twoFingerPanZoomTool),
|
36
|
+
new UndoRedoShortcut(editor),
|
34
37
|
];
|
35
38
|
primaryTools.forEach(tool => tool.setToolGroup(primaryToolEnabledGroup));
|
36
39
|
touchPanZoom.setEnabled(false);
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { KeyPressEvent } from '../types';
|
3
|
+
import BaseTool from './BaseTool';
|
4
|
+
import { ToolType } from './ToolController';
|
5
|
+
export default class UndoRedoShortcut extends BaseTool {
|
6
|
+
private editor;
|
7
|
+
kind: ToolType.UndoRedoShortcut;
|
8
|
+
constructor(editor: Editor);
|
9
|
+
onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean;
|
10
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import BaseTool from './BaseTool';
|
2
|
+
import { ToolType } from './ToolController';
|
3
|
+
export default class UndoRedoShortcut extends BaseTool {
|
4
|
+
constructor(editor) {
|
5
|
+
super(editor.notifier, editor.localization.undoRedoTool);
|
6
|
+
this.editor = editor;
|
7
|
+
this.kind = ToolType.UndoRedoShortcut;
|
8
|
+
}
|
9
|
+
// Activate undo/redo
|
10
|
+
onKeyPress({ key, ctrlKey }) {
|
11
|
+
if (ctrlKey) {
|
12
|
+
if (key === 'z') {
|
13
|
+
this.editor.history.undo();
|
14
|
+
return true;
|
15
|
+
}
|
16
|
+
else if (key === 'Z') {
|
17
|
+
this.editor.history.redo();
|
18
|
+
return true;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
return false;
|
22
|
+
}
|
23
|
+
}
|
@@ -4,6 +4,7 @@ export const defaultToolLocalization = {
|
|
4
4
|
eraserTool: 'Eraser',
|
5
5
|
touchPanTool: 'Touch Panning',
|
6
6
|
twoFingerPanZoomTool: 'Panning and Zooming',
|
7
|
+
undoRedoTool: 'Undo/Redo',
|
7
8
|
toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
|
8
9
|
toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,
|
9
10
|
};
|
package/dist/src/types.d.ts
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.9",
|
4
4
|
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
|
5
5
|
"main": "dist/src/Editor.js",
|
6
6
|
"types": "dist/src/Editor.d.ts",
|
@@ -48,7 +48,7 @@
|
|
48
48
|
"linter-precommit": "eslint --fix --ext .js --ext .ts",
|
49
49
|
"lint-staged": "lint-staged",
|
50
50
|
"_postinstall": "husky install",
|
51
|
-
"prepack": "pinst --disable",
|
51
|
+
"prepack": "yarn build && pinst --disable",
|
52
52
|
"postpack": "pinst --enable"
|
53
53
|
},
|
54
54
|
"dependencies": {
|
package/src/Editor.ts
CHANGED
package/src/EditorImage.test.ts
CHANGED
@@ -5,12 +5,9 @@ import Stroke from './components/Stroke';
|
|
5
5
|
import { Vec2 } from './geometry/Vec2';
|
6
6
|
import Path, { PathCommandType } from './geometry/Path';
|
7
7
|
import Color4 from './Color4';
|
8
|
-
import Editor from './Editor';
|
9
|
-
import { RenderingMode } from './Display';
|
10
8
|
import DummyRenderer from './rendering/DummyRenderer';
|
11
9
|
import { RenderingStyle } from './rendering/AbstractRenderer';
|
12
|
-
|
13
|
-
const createEditor = () => new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
|
10
|
+
import createEditor from './testing/createEditor';
|
14
11
|
|
15
12
|
describe('EditorImage', () => {
|
16
13
|
const testStroke = new Stroke([
|
package/src/SVGLoader.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import Color4 from './Color4';
|
2
2
|
import AbstractComponent from './components/AbstractComponent';
|
3
3
|
import Stroke from './components/Stroke';
|
4
|
+
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
4
5
|
import UnknownSVGObject from './components/UnknownSVGObject';
|
5
6
|
import Path from './geometry/Path';
|
6
7
|
import Rect2 from './geometry/Rect2';
|
@@ -127,6 +128,10 @@ export default class SVGLoader implements ImageLoader {
|
|
127
128
|
this.rootViewBox = new Rect2(x, y, width, height);
|
128
129
|
}
|
129
130
|
|
131
|
+
private updateSVGAttrs(node: SVGSVGElement) {
|
132
|
+
this.onAddComponent?.(new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
|
133
|
+
}
|
134
|
+
|
130
135
|
private async visit(node: Element) {
|
131
136
|
this.totalToProcess += node.childElementCount;
|
132
137
|
|
@@ -139,6 +144,7 @@ export default class SVGLoader implements ImageLoader {
|
|
139
144
|
break;
|
140
145
|
case 'svg':
|
141
146
|
this.updateViewBox(node as SVGSVGElement);
|
147
|
+
this.updateSVGAttrs(node as SVGSVGElement);
|
142
148
|
break;
|
143
149
|
default:
|
144
150
|
console.warn('Unknown SVG element,', node);
|
@@ -158,6 +164,13 @@ export default class SVGLoader implements ImageLoader {
|
|
158
164
|
await this.onProgress?.(this.processedCount, this.totalToProcess);
|
159
165
|
}
|
160
166
|
|
167
|
+
// Get SVG element attributes (e.g. xlink=...)
|
168
|
+
private getSourceAttrs(node: SVGSVGElement): Array<[string, string|null]> {
|
169
|
+
return node.getAttributeNames().map(attr => {
|
170
|
+
return [ attr, node.getAttribute(attr) ];
|
171
|
+
});
|
172
|
+
}
|
173
|
+
|
161
174
|
public async start(
|
162
175
|
onAddComponent: ComponentAddedListener, onProgress: OnProgressListener
|
163
176
|
): Promise<Rect2> {
|
package/src/UndoRedoHistory.ts
CHANGED
@@ -56,6 +56,14 @@ class UndoRedoHistory {
|
|
56
56
|
}
|
57
57
|
this.fireUpdateEvent();
|
58
58
|
}
|
59
|
+
|
60
|
+
public get undoStackSize(): number {
|
61
|
+
return this.undoStack.length;
|
62
|
+
}
|
63
|
+
|
64
|
+
public get redoStackSize(): number {
|
65
|
+
return this.redoStack.length;
|
66
|
+
}
|
59
67
|
}
|
60
68
|
|
61
69
|
export default UndoRedoHistory;
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import LineSegment2 from '../geometry/LineSegment2';
|
2
|
+
import Mat33 from '../geometry/Mat33';
|
3
|
+
import Rect2 from '../geometry/Rect2';
|
4
|
+
import AbstractRenderer from '../rendering/AbstractRenderer';
|
5
|
+
import SVGRenderer from '../rendering/SVGRenderer';
|
6
|
+
import AbstractComponent from './AbstractComponent';
|
7
|
+
import { ImageComponentLocalization } from './localization';
|
8
|
+
|
9
|
+
// Stores global SVG attributes (e.g. namespace identifiers.)
|
10
|
+
export default class SVGGlobalAttributesObject extends AbstractComponent {
|
11
|
+
protected contentBBox: Rect2;
|
12
|
+
public constructor(private readonly attrs: Array<[string, string|null]>) {
|
13
|
+
super();
|
14
|
+
this.contentBBox = Rect2.empty;
|
15
|
+
}
|
16
|
+
|
17
|
+
public render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
|
18
|
+
if (!(canvas instanceof SVGRenderer)) {
|
19
|
+
// Don't draw unrenderable objects if we can't
|
20
|
+
return;
|
21
|
+
}
|
22
|
+
|
23
|
+
console.log('Rendering to SVG.', this.attrs);
|
24
|
+
for (const [ attr, value ] of this.attrs) {
|
25
|
+
canvas.setRootSVGAttribute(attr, value);
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
public intersects(_lineSegment: LineSegment2): boolean {
|
30
|
+
return false;
|
31
|
+
}
|
32
|
+
|
33
|
+
protected applyTransformation(_affineTransfm: Mat33): void {
|
34
|
+
}
|
35
|
+
|
36
|
+
public description(localization: ImageComponentLocalization): string {
|
37
|
+
return localization.svgObject;
|
38
|
+
}
|
39
|
+
}
|
@@ -15,12 +15,27 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
15
15
|
private lastPathStart: Point2|null;
|
16
16
|
|
17
17
|
private mainGroup: SVGGElement;
|
18
|
+
private overwrittenAttrs: Record<string, string|null> = {};
|
18
19
|
|
19
20
|
public constructor(private elem: SVGSVGElement, viewport: Viewport) {
|
20
21
|
super(viewport);
|
21
22
|
this.clear();
|
22
23
|
}
|
23
24
|
|
25
|
+
// Sets an attribute on the root SVG element.
|
26
|
+
public setRootSVGAttribute(name: string, value: string|null) {
|
27
|
+
// Make the original value of the attribute restorable on clear
|
28
|
+
if (!(name in this.overwrittenAttrs)) {
|
29
|
+
this.overwrittenAttrs[name] = this.elem.getAttribute(name);
|
30
|
+
}
|
31
|
+
|
32
|
+
if (value !== null) {
|
33
|
+
this.elem.setAttribute(name, value);
|
34
|
+
} else {
|
35
|
+
this.elem.removeAttribute(name);
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
24
39
|
public displaySize(): Vec2 {
|
25
40
|
return Vec2.of(this.elem.clientWidth, this.elem.clientHeight);
|
26
41
|
}
|
@@ -28,6 +43,18 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
28
43
|
public clear() {
|
29
44
|
this.mainGroup = document.createElementNS(svgNameSpace, 'g');
|
30
45
|
|
46
|
+
// Restore all alltributes
|
47
|
+
for (const attrName in this.overwrittenAttrs) {
|
48
|
+
const value = this.overwrittenAttrs[attrName];
|
49
|
+
|
50
|
+
if (value) {
|
51
|
+
this.elem.setAttribute(attrName, value);
|
52
|
+
} else {
|
53
|
+
this.elem.removeAttribute(attrName);
|
54
|
+
}
|
55
|
+
}
|
56
|
+
this.overwrittenAttrs = {};
|
57
|
+
|
31
58
|
// Remove all children
|
32
59
|
this.elem.replaceChildren(this.mainGroup);
|
33
60
|
}
|
package/src/tools/BaseTool.ts
CHANGED
@@ -6,10 +6,11 @@ export default abstract class BaseTool implements PointerEvtListener {
|
|
6
6
|
private enabled: boolean = true;
|
7
7
|
private group: ToolEnabledGroup|null = null;
|
8
8
|
|
9
|
-
public
|
10
|
-
public
|
11
|
-
public
|
12
|
-
public
|
9
|
+
public onPointerDown(_event: PointerEvt): boolean { return false; }
|
10
|
+
public onPointerMove(_event: PointerEvt) { }
|
11
|
+
public onPointerUp(_event: PointerEvt) { }
|
12
|
+
public onGestureCancel() { }
|
13
|
+
|
13
14
|
public abstract readonly kind: ToolType;
|
14
15
|
|
15
16
|
protected constructor(private notifier: EditorNotifier, public readonly description: string) {
|
@@ -8,6 +8,7 @@ import Eraser from './Eraser';
|
|
8
8
|
import SelectionTool from './SelectionTool';
|
9
9
|
import Color4 from '../Color4';
|
10
10
|
import { ToolLocalization } from './localization';
|
11
|
+
import UndoRedoShortcut from './UndoRedoShortcut';
|
11
12
|
|
12
13
|
export enum ToolType {
|
13
14
|
TouchPanZoom,
|
@@ -15,6 +16,7 @@ export enum ToolType {
|
|
15
16
|
Selection,
|
16
17
|
Eraser,
|
17
18
|
PanZoom,
|
19
|
+
UndoRedoShortcut,
|
18
20
|
}
|
19
21
|
|
20
22
|
export default class ToolController {
|
@@ -40,6 +42,7 @@ export default class ToolController {
|
|
40
42
|
touchPanZoom,
|
41
43
|
...primaryTools,
|
42
44
|
new PanZoom(editor, PanZoomMode.TwoFingerGestures | PanZoomMode.AnyDevice, localization.twoFingerPanZoomTool),
|
45
|
+
new UndoRedoShortcut(editor),
|
43
46
|
];
|
44
47
|
primaryTools.forEach(tool => tool.setToolGroup(primaryToolEnabledGroup));
|
45
48
|
touchPanZoom.setEnabled(false);
|
@@ -0,0 +1,53 @@
|
|
1
|
+
/* @jest-environment jsdom */
|
2
|
+
|
3
|
+
import Color4 from '../Color4';
|
4
|
+
import Stroke from '../components/Stroke';
|
5
|
+
import EditorImage from '../EditorImage';
|
6
|
+
import Path from '../geometry/Path';
|
7
|
+
import createEditor from '../testing/createEditor';
|
8
|
+
import { InputEvtType } from '../types';
|
9
|
+
|
10
|
+
describe('UndoRedoShortcut', () => {
|
11
|
+
const testStroke = new Stroke([Path.fromString('M0,0L10,10').toRenderable({ fill: Color4.red })]);
|
12
|
+
const addTestStrokeCommand = new EditorImage.AddElementCommand(testStroke);
|
13
|
+
|
14
|
+
it('ctrl+z should undo', () => {
|
15
|
+
const editor = createEditor();
|
16
|
+
editor.dispatch(addTestStrokeCommand);
|
17
|
+
expect(editor.history.undoStackSize).toBe(1);
|
18
|
+
|
19
|
+
editor.toolController.dispatchInputEvent({
|
20
|
+
kind: InputEvtType.KeyPressEvent,
|
21
|
+
ctrlKey: true,
|
22
|
+
key: 'z',
|
23
|
+
});
|
24
|
+
|
25
|
+
expect(editor.history.undoStackSize).toBe(0);
|
26
|
+
expect(editor.history.redoStackSize).toBe(1);
|
27
|
+
});
|
28
|
+
|
29
|
+
it('ctrl+shift+Z should re-do', () => {
|
30
|
+
const editor = createEditor();
|
31
|
+
editor.dispatch(addTestStrokeCommand);
|
32
|
+
editor.dispatch(addTestStrokeCommand);
|
33
|
+
expect(editor.history.undoStackSize).toBe(2);
|
34
|
+
|
35
|
+
editor.toolController.dispatchInputEvent({
|
36
|
+
kind: InputEvtType.KeyPressEvent,
|
37
|
+
ctrlKey: true,
|
38
|
+
key: 'z',
|
39
|
+
});
|
40
|
+
|
41
|
+
expect(editor.history.undoStackSize).toBe(1);
|
42
|
+
expect(editor.history.redoStackSize).toBe(1);
|
43
|
+
|
44
|
+
editor.toolController.dispatchInputEvent({
|
45
|
+
kind: InputEvtType.KeyPressEvent,
|
46
|
+
ctrlKey: true,
|
47
|
+
key: 'Z',
|
48
|
+
});
|
49
|
+
|
50
|
+
expect(editor.history.undoStackSize).toBe(2);
|
51
|
+
expect(editor.history.redoStackSize).toBe(0);
|
52
|
+
});
|
53
|
+
});
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { KeyPressEvent } from '../types';
|
3
|
+
import BaseTool from './BaseTool';
|
4
|
+
import { ToolType } from './ToolController';
|
5
|
+
|
6
|
+
|
7
|
+
export default class UndoRedoShortcut extends BaseTool {
|
8
|
+
public kind: ToolType.UndoRedoShortcut = ToolType.UndoRedoShortcut;
|
9
|
+
|
10
|
+
public constructor(private editor: Editor) {
|
11
|
+
super(editor.notifier, editor.localization.undoRedoTool);
|
12
|
+
}
|
13
|
+
|
14
|
+
// Activate undo/redo
|
15
|
+
public onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean {
|
16
|
+
if (ctrlKey) {
|
17
|
+
if (key === 'z') {
|
18
|
+
this.editor.history.undo();
|
19
|
+
return true;
|
20
|
+
} else if (key === 'Z') {
|
21
|
+
this.editor.history.redo();
|
22
|
+
return true;
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
return false;
|
27
|
+
}
|
28
|
+
}
|