js-draw 0.3.1 → 0.3.2
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/.github/ISSUE_TEMPLATE/translation.md +4 -1
- package/CHANGELOG.md +8 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +4 -1
- package/dist/src/Editor.js +117 -2
- package/dist/src/EditorImage.js +4 -1
- package/dist/src/SVGLoader.d.ts +4 -1
- package/dist/src/SVGLoader.js +78 -33
- package/dist/src/UndoRedoHistory.d.ts +1 -0
- package/dist/src/UndoRedoHistory.js +6 -0
- package/dist/src/Viewport.d.ts +1 -0
- package/dist/src/Viewport.js +12 -4
- package/dist/src/commands/lib.d.ts +2 -1
- package/dist/src/commands/lib.js +2 -1
- package/dist/src/commands/localization.d.ts +1 -0
- package/dist/src/commands/localization.js +1 -0
- package/dist/src/commands/uniteCommands.d.ts +4 -0
- package/dist/src/commands/uniteCommands.js +105 -0
- package/dist/src/components/AbstractComponent.d.ts +2 -0
- package/dist/src/components/AbstractComponent.js +41 -5
- package/dist/src/components/ImageComponent.d.ts +27 -0
- package/dist/src/components/ImageComponent.js +129 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +2 -2
- package/dist/src/components/lib.d.ts +4 -2
- package/dist/src/components/lib.js +4 -2
- package/dist/src/components/localization.d.ts +2 -0
- package/dist/src/components/localization.js +2 -0
- package/dist/src/math/LineSegment2.d.ts +2 -0
- package/dist/src/math/LineSegment2.js +3 -0
- package/dist/src/rendering/localization.d.ts +3 -0
- package/dist/src/rendering/localization.js +3 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -0
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +5 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +45 -20
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
- package/dist/src/tools/BaseTool.d.ts +3 -1
- package/dist/src/tools/BaseTool.js +6 -0
- package/dist/src/tools/PasteHandler.d.ts +16 -0
- package/dist/src/tools/PasteHandler.js +142 -0
- package/dist/src/tools/SelectionTool.d.ts +7 -1
- package/dist/src/tools/SelectionTool.js +63 -5
- package/dist/src/tools/ToolController.js +36 -27
- package/dist/src/tools/lib.d.ts +1 -0
- package/dist/src/tools/lib.js +1 -0
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/dist/src/types.d.ts +13 -2
- package/dist/src/types.js +2 -0
- package/package.json +1 -1
- package/src/Editor.ts +131 -2
- package/src/EditorImage.ts +7 -1
- package/src/SVGLoader.ts +90 -36
- package/src/UndoRedoHistory.test.ts +33 -0
- package/src/UndoRedoHistory.ts +8 -0
- package/src/Viewport.ts +13 -4
- package/src/commands/lib.ts +2 -0
- package/src/commands/localization.ts +2 -0
- package/src/commands/uniteCommands.test.ts +23 -0
- package/src/commands/uniteCommands.ts +121 -0
- package/src/components/AbstractComponent.ts +55 -9
- package/src/components/ImageComponent.ts +153 -0
- package/src/components/builders/FreehandLineBuilder.ts +2 -2
- package/src/components/lib.ts +7 -2
- package/src/components/localization.ts +4 -0
- package/src/math/LineSegment2.test.ts +9 -0
- package/src/math/LineSegment2.ts +5 -0
- package/src/rendering/localization.ts +6 -0
- package/src/rendering/renderers/AbstractRenderer.ts +16 -0
- package/src/rendering/renderers/CanvasRenderer.ts +10 -1
- package/src/rendering/renderers/DummyRenderer.ts +6 -1
- package/src/rendering/renderers/SVGRenderer.ts +50 -21
- package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
- package/src/tools/BaseTool.ts +9 -1
- package/src/tools/PasteHandler.ts +156 -0
- package/src/tools/SelectionTool.ts +80 -8
- package/src/tools/ToolController.ts +51 -44
- package/src/tools/lib.ts +1 -0
- package/src/tools/localization.ts +8 -0
- package/src/types.ts +16 -2
@@ -0,0 +1,16 @@
|
|
1
|
+
/**
|
2
|
+
* A tool that handles paste events.
|
3
|
+
* @packageDocumentation
|
4
|
+
*/
|
5
|
+
import Editor from '../Editor';
|
6
|
+
import { PasteEvent } from '../types';
|
7
|
+
import BaseTool from './BaseTool';
|
8
|
+
export default class PasteHandler extends BaseTool {
|
9
|
+
private editor;
|
10
|
+
constructor(editor: Editor);
|
11
|
+
onPaste(event: PasteEvent): boolean;
|
12
|
+
private addComponentsFromPaste;
|
13
|
+
private doSVGPaste;
|
14
|
+
private doTextPaste;
|
15
|
+
private doImagePaste;
|
16
|
+
}
|
@@ -0,0 +1,142 @@
|
|
1
|
+
/**
|
2
|
+
* A tool that handles paste events.
|
3
|
+
* @packageDocumentation
|
4
|
+
*/
|
5
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
6
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
7
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
8
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
9
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
10
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
11
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
12
|
+
});
|
13
|
+
};
|
14
|
+
import { TextComponent } from '../components/lib';
|
15
|
+
import { uniteCommands } from '../commands/lib';
|
16
|
+
import SVGLoader from '../SVGLoader';
|
17
|
+
import { Mat33, Vec2 } from '../math/lib';
|
18
|
+
import BaseTool from './BaseTool';
|
19
|
+
import EditorImage from '../EditorImage';
|
20
|
+
import SelectionTool from './SelectionTool';
|
21
|
+
import TextTool from './TextTool';
|
22
|
+
import Color4 from '../Color4';
|
23
|
+
import ImageComponent from '../components/ImageComponent';
|
24
|
+
// { @inheritDoc PasteHandler! }
|
25
|
+
export default class PasteHandler extends BaseTool {
|
26
|
+
constructor(editor) {
|
27
|
+
super(editor.notifier, editor.localization.pasteHandler);
|
28
|
+
this.editor = editor;
|
29
|
+
}
|
30
|
+
onPaste(event) {
|
31
|
+
const mime = event.mime.toLowerCase();
|
32
|
+
if (mime === 'image/svg+xml') {
|
33
|
+
void this.doSVGPaste(event.data);
|
34
|
+
return true;
|
35
|
+
}
|
36
|
+
else if (mime === 'text/plain') {
|
37
|
+
void this.doTextPaste(event.data);
|
38
|
+
return true;
|
39
|
+
}
|
40
|
+
else if (mime === 'image/png' || mime === 'image/jpeg') {
|
41
|
+
void this.doImagePaste(event.data);
|
42
|
+
return true;
|
43
|
+
}
|
44
|
+
return false;
|
45
|
+
}
|
46
|
+
addComponentsFromPaste(components) {
|
47
|
+
return __awaiter(this, void 0, void 0, function* () {
|
48
|
+
let bbox = null;
|
49
|
+
for (const component of components) {
|
50
|
+
if (bbox) {
|
51
|
+
bbox = bbox.union(component.getBBox());
|
52
|
+
}
|
53
|
+
else {
|
54
|
+
bbox = component.getBBox();
|
55
|
+
}
|
56
|
+
}
|
57
|
+
if (!bbox) {
|
58
|
+
return;
|
59
|
+
}
|
60
|
+
// Find a transform that scales/moves bbox onto the screen.
|
61
|
+
const visibleRect = this.editor.viewport.visibleRect;
|
62
|
+
const scaleRatioX = visibleRect.width / bbox.width;
|
63
|
+
const scaleRatioY = visibleRect.height / bbox.height;
|
64
|
+
let scaleRatio = scaleRatioX;
|
65
|
+
if (bbox.width * scaleRatio > visibleRect.width || bbox.height * scaleRatio > visibleRect.height) {
|
66
|
+
scaleRatio = scaleRatioY;
|
67
|
+
}
|
68
|
+
scaleRatio *= 2 / 3;
|
69
|
+
const transfm = Mat33.translation(visibleRect.center.minus(bbox.center)).rightMul(Mat33.scaling2D(scaleRatio, bbox.center));
|
70
|
+
const commands = [];
|
71
|
+
for (const component of components) {
|
72
|
+
// To allow deserialization, we need to add first, then transform.
|
73
|
+
commands.push(EditorImage.addElement(component));
|
74
|
+
commands.push(component.transformBy(transfm));
|
75
|
+
}
|
76
|
+
const applyChunkSize = 100;
|
77
|
+
this.editor.dispatch(uniteCommands(commands, applyChunkSize), true);
|
78
|
+
for (const selectionTool of this.editor.toolController.getMatchingTools(SelectionTool)) {
|
79
|
+
selectionTool.setEnabled(true);
|
80
|
+
selectionTool.setSelection(components);
|
81
|
+
}
|
82
|
+
});
|
83
|
+
}
|
84
|
+
doSVGPaste(data) {
|
85
|
+
return __awaiter(this, void 0, void 0, function* () {
|
86
|
+
const sanitize = true;
|
87
|
+
const loader = SVGLoader.fromString(data, sanitize);
|
88
|
+
const components = [];
|
89
|
+
yield loader.start((component) => {
|
90
|
+
components.push(component);
|
91
|
+
}, (_countProcessed, _totalToProcess) => null);
|
92
|
+
yield this.addComponentsFromPaste(components);
|
93
|
+
});
|
94
|
+
}
|
95
|
+
doTextPaste(text) {
|
96
|
+
var _a, _b;
|
97
|
+
return __awaiter(this, void 0, void 0, function* () {
|
98
|
+
const textTools = this.editor.toolController.getMatchingTools(TextTool);
|
99
|
+
textTools.sort((a, b) => {
|
100
|
+
if (!a.isEnabled() && b.isEnabled()) {
|
101
|
+
return -1;
|
102
|
+
}
|
103
|
+
if (!b.isEnabled() && a.isEnabled()) {
|
104
|
+
return 1;
|
105
|
+
}
|
106
|
+
return 0;
|
107
|
+
});
|
108
|
+
const defaultTextStyle = { size: 12, fontFamily: 'sans', renderingStyle: { fill: Color4.red } };
|
109
|
+
const pastedTextStyle = (_b = (_a = textTools[0]) === null || _a === void 0 ? void 0 : _a.getTextStyle()) !== null && _b !== void 0 ? _b : defaultTextStyle;
|
110
|
+
const lines = text.split('\n');
|
111
|
+
let lastComponent = null;
|
112
|
+
const components = [];
|
113
|
+
for (const line of lines) {
|
114
|
+
let position = Vec2.zero;
|
115
|
+
if (lastComponent) {
|
116
|
+
const lineMargin = Math.floor(pastedTextStyle.size);
|
117
|
+
position = lastComponent.getBBox().bottomLeft.plus(Vec2.unitY.times(lineMargin));
|
118
|
+
}
|
119
|
+
const component = new TextComponent([line], Mat33.translation(position), pastedTextStyle);
|
120
|
+
components.push(component);
|
121
|
+
lastComponent = component;
|
122
|
+
}
|
123
|
+
if (components.length === 1) {
|
124
|
+
yield this.addComponentsFromPaste([components[0]]);
|
125
|
+
}
|
126
|
+
else {
|
127
|
+
// Wrap the existing `TextComponent`s --- dragging one component should drag all.
|
128
|
+
yield this.addComponentsFromPaste([
|
129
|
+
new TextComponent(components, Mat33.identity, pastedTextStyle)
|
130
|
+
]);
|
131
|
+
}
|
132
|
+
});
|
133
|
+
}
|
134
|
+
doImagePaste(dataURL) {
|
135
|
+
return __awaiter(this, void 0, void 0, function* () {
|
136
|
+
const image = new Image();
|
137
|
+
image.src = dataURL;
|
138
|
+
const component = yield ImageComponent.fromImage(image, Mat33.identity);
|
139
|
+
yield this.addComponentsFromPaste([component]);
|
140
|
+
});
|
141
|
+
}
|
142
|
+
}
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import Command from '../commands/Command';
|
2
|
+
import AbstractComponent from '../components/AbstractComponent';
|
2
3
|
import Editor from '../Editor';
|
3
4
|
import Mat33 from '../math/Mat33';
|
4
5
|
import Rect2 from '../math/Rect2';
|
5
6
|
import { Point2, Vec2 } from '../math/Vec2';
|
6
|
-
import { KeyPressEvent, KeyUpEvent, PointerEvt } from '../types';
|
7
|
+
import { CopyEvent, KeyPressEvent, KeyUpEvent, PointerEvt } from '../types';
|
7
8
|
import BaseTool from './BaseTool';
|
8
9
|
declare class Selection {
|
9
10
|
startPoint: Point2;
|
@@ -27,6 +28,8 @@ declare class Selection {
|
|
27
28
|
appendBackgroundBoxTo(elem: HTMLElement): void;
|
28
29
|
setToPoint(point: Point2): void;
|
29
30
|
cancelSelection(): void;
|
31
|
+
setSelectedObjects(objects: AbstractComponent[], bbox: Rect2): void;
|
32
|
+
getSelectedObjects(): AbstractComponent[];
|
30
33
|
resolveToObjects(): boolean;
|
31
34
|
recomputeRegion(): boolean;
|
32
35
|
getMinCanvasSize(): number;
|
@@ -43,6 +46,7 @@ export default class SelectionTool extends BaseTool {
|
|
43
46
|
private prevSelectionBox;
|
44
47
|
private selectionBox;
|
45
48
|
constructor(editor: Editor, description: string);
|
49
|
+
private makeSelectionBox;
|
46
50
|
onPointerDown(event: PointerEvt): boolean;
|
47
51
|
onPointerMove(event: PointerEvt): void;
|
48
52
|
private onGestureEnd;
|
@@ -52,8 +56,10 @@ export default class SelectionTool extends BaseTool {
|
|
52
56
|
private static handleableKeys;
|
53
57
|
onKeyPress(event: KeyPressEvent): boolean;
|
54
58
|
onKeyUp(evt: KeyUpEvent): boolean;
|
59
|
+
onCopy(event: CopyEvent): boolean;
|
55
60
|
setEnabled(enabled: boolean): void;
|
56
61
|
getSelection(): Selection | null;
|
62
|
+
setSelection(objects: AbstractComponent[]): void;
|
57
63
|
clearSelection(): void;
|
58
64
|
}
|
59
65
|
export {};
|
@@ -20,6 +20,7 @@ import { EditorEventType } from '../types';
|
|
20
20
|
import Viewport from '../Viewport';
|
21
21
|
import BaseTool from './BaseTool';
|
22
22
|
import SerializableCommand from '../commands/SerializableCommand';
|
23
|
+
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
23
24
|
const handleScreenSize = 30;
|
24
25
|
const styles = `
|
25
26
|
.handleOverlay {
|
@@ -262,6 +263,14 @@ class Selection {
|
|
262
263
|
}
|
263
264
|
this.region = Rect2.empty;
|
264
265
|
}
|
266
|
+
setSelectedObjects(objects, bbox) {
|
267
|
+
this.region = bbox;
|
268
|
+
this.selectedElems = objects;
|
269
|
+
this.updateUI();
|
270
|
+
}
|
271
|
+
getSelectedObjects() {
|
272
|
+
return this.selectedElems;
|
273
|
+
}
|
265
274
|
// Find the objects corresponding to this in the document,
|
266
275
|
// select them.
|
267
276
|
// Returns false iff nothing was selected.
|
@@ -427,13 +436,16 @@ export default class SelectionTool extends BaseTool {
|
|
427
436
|
});
|
428
437
|
this.editor.handleKeyEventsFrom(this.handleOverlay);
|
429
438
|
}
|
439
|
+
makeSelectionBox(selectionStartPos) {
|
440
|
+
this.prevSelectionBox = this.selectionBox;
|
441
|
+
this.selectionBox = new Selection(selectionStartPos, this.editor);
|
442
|
+
// Remove any previous selection rects
|
443
|
+
this.handleOverlay.replaceChildren();
|
444
|
+
this.selectionBox.appendBackgroundBoxTo(this.handleOverlay);
|
445
|
+
}
|
430
446
|
onPointerDown(event) {
|
431
447
|
if (event.allPointers.length === 1 && event.current.isPrimary) {
|
432
|
-
this.
|
433
|
-
this.selectionBox = new Selection(event.current.canvasPos, this.editor);
|
434
|
-
// Remove any previous selection rects
|
435
|
-
this.handleOverlay.replaceChildren();
|
436
|
-
this.selectionBox.appendBackgroundBoxTo(this.handleOverlay);
|
448
|
+
this.makeSelectionBox(event.current.canvasPos);
|
437
449
|
return true;
|
438
450
|
}
|
439
451
|
return false;
|
@@ -542,6 +554,11 @@ export default class SelectionTool extends BaseTool {
|
|
542
554
|
Math.max(0.5, scaledSize.x / region.size.x), Math.max(0.5, scaledSize.y / region.size.y)), region.topLeft).rightMul(Mat33.zRotation(rotationSteps * rotateStepSize, region.center)).rightMul(Mat33.translation(Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize)));
|
543
555
|
this.selectionBox.transformPreview(transform);
|
544
556
|
}
|
557
|
+
if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
|
558
|
+
this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
|
559
|
+
this.clearSelection();
|
560
|
+
handled = true;
|
561
|
+
}
|
545
562
|
return handled;
|
546
563
|
}
|
547
564
|
onKeyUp(evt) {
|
@@ -551,6 +568,28 @@ export default class SelectionTool extends BaseTool {
|
|
551
568
|
}
|
552
569
|
return false;
|
553
570
|
}
|
571
|
+
onCopy(event) {
|
572
|
+
if (!this.selectionBox) {
|
573
|
+
return false;
|
574
|
+
}
|
575
|
+
const selectedElems = this.selectionBox.getSelectedObjects();
|
576
|
+
const bbox = this.selectionBox.region;
|
577
|
+
if (selectedElems.length === 0) {
|
578
|
+
return false;
|
579
|
+
}
|
580
|
+
const exportViewport = new Viewport(this.editor.notifier);
|
581
|
+
exportViewport.updateScreenSize(Vec2.of(bbox.w, bbox.h));
|
582
|
+
exportViewport.resetTransform(Mat33.translation(bbox.topLeft));
|
583
|
+
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
584
|
+
const exportElem = document.createElementNS(svgNameSpace, 'svg');
|
585
|
+
const sanitize = true;
|
586
|
+
const renderer = new SVGRenderer(exportElem, exportViewport, sanitize);
|
587
|
+
for (const elem of selectedElems) {
|
588
|
+
elem.render(renderer);
|
589
|
+
}
|
590
|
+
event.setData('image/svg+xml', exportElem.outerHTML);
|
591
|
+
return true;
|
592
|
+
}
|
554
593
|
setEnabled(enabled) {
|
555
594
|
super.setEnabled(enabled);
|
556
595
|
// Clear the selection
|
@@ -569,6 +608,25 @@ export default class SelectionTool extends BaseTool {
|
|
569
608
|
getSelection() {
|
570
609
|
return this.selectionBox;
|
571
610
|
}
|
611
|
+
setSelection(objects) {
|
612
|
+
let bbox = null;
|
613
|
+
for (const object of objects) {
|
614
|
+
if (bbox) {
|
615
|
+
bbox = bbox.union(object.getBBox());
|
616
|
+
}
|
617
|
+
else {
|
618
|
+
bbox = object.getBBox();
|
619
|
+
}
|
620
|
+
}
|
621
|
+
if (!bbox) {
|
622
|
+
return;
|
623
|
+
}
|
624
|
+
this.clearSelection();
|
625
|
+
if (!this.selectionBox) {
|
626
|
+
this.makeSelectionBox(bbox.topLeft);
|
627
|
+
}
|
628
|
+
this.selectionBox.setSelectedObjects(objects, bbox);
|
629
|
+
}
|
572
630
|
clearSelection() {
|
573
631
|
this.handleOverlay.replaceChildren();
|
574
632
|
this.prevSelectionBox = this.selectionBox;
|
@@ -9,6 +9,7 @@ import UndoRedoShortcut from './UndoRedoShortcut';
|
|
9
9
|
import TextTool from './TextTool';
|
10
10
|
import PipetteTool from './PipetteTool';
|
11
11
|
import ToolSwitcherShortcut from './ToolSwitcherShortcut';
|
12
|
+
import PasteHandler from './PasteHandler';
|
12
13
|
export default class ToolController {
|
13
14
|
/** @internal */
|
14
15
|
constructor(editor, localization) {
|
@@ -35,6 +36,7 @@ export default class ToolController {
|
|
35
36
|
keyboardPanZoomTool,
|
36
37
|
new UndoRedoShortcut(editor),
|
37
38
|
new ToolSwitcherShortcut(editor),
|
39
|
+
new PasteHandler(editor),
|
38
40
|
];
|
39
41
|
primaryTools.forEach(tool => tool.setToolGroup(primaryToolGroup));
|
40
42
|
panZoomTool.setEnabled(true);
|
@@ -97,42 +99,49 @@ export default class ToolController {
|
|
97
99
|
this.activeTool = null;
|
98
100
|
handled = true;
|
99
101
|
}
|
100
|
-
else if (event.kind === InputEvtType.
|
101
|
-
|
102
|
-
|
103
|
-
|
102
|
+
else if (event.kind === InputEvtType.PointerMoveEvt) {
|
103
|
+
if (this.activeTool !== null) {
|
104
|
+
this.activeTool.onPointerMove(event);
|
105
|
+
handled = true;
|
106
|
+
}
|
107
|
+
}
|
108
|
+
else if (event.kind === InputEvtType.GestureCancelEvt) {
|
109
|
+
if (this.activeTool !== null) {
|
110
|
+
this.activeTool.onGestureCancel();
|
111
|
+
this.activeTool = null;
|
112
|
+
}
|
113
|
+
}
|
114
|
+
else {
|
115
|
+
let allCasesHandledGuard;
|
104
116
|
for (const tool of this.tools) {
|
105
117
|
if (!tool.isEnabled()) {
|
106
118
|
continue;
|
107
119
|
}
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
120
|
+
switch (event.kind) {
|
121
|
+
case InputEvtType.KeyPressEvent:
|
122
|
+
handled = tool.onKeyPress(event);
|
123
|
+
break;
|
124
|
+
case InputEvtType.KeyUpEvent:
|
125
|
+
handled = tool.onKeyUp(event);
|
126
|
+
break;
|
127
|
+
case InputEvtType.WheelEvt:
|
128
|
+
handled = tool.onWheel(event);
|
129
|
+
break;
|
130
|
+
case InputEvtType.CopyEvent:
|
131
|
+
handled = tool.onCopy(event);
|
132
|
+
break;
|
133
|
+
case InputEvtType.PasteEvent:
|
134
|
+
handled = tool.onPaste(event);
|
135
|
+
break;
|
136
|
+
default:
|
137
|
+
allCasesHandledGuard = event;
|
138
|
+
return allCasesHandledGuard;
|
139
|
+
}
|
112
140
|
if (handled) {
|
113
141
|
break;
|
114
142
|
}
|
115
143
|
}
|
116
144
|
}
|
117
|
-
else if (this.activeTool !== null) {
|
118
|
-
let allCasesHandledGuard;
|
119
|
-
switch (event.kind) {
|
120
|
-
case InputEvtType.PointerMoveEvt:
|
121
|
-
this.activeTool.onPointerMove(event);
|
122
|
-
break;
|
123
|
-
case InputEvtType.GestureCancelEvt:
|
124
|
-
this.activeTool.onGestureCancel();
|
125
|
-
this.activeTool = null;
|
126
|
-
break;
|
127
|
-
default:
|
128
|
-
allCasesHandledGuard = event;
|
129
|
-
return allCasesHandledGuard;
|
130
|
-
}
|
131
|
-
handled = true;
|
132
|
-
}
|
133
|
-
else {
|
134
|
-
handled = false;
|
135
|
-
}
|
136
145
|
return handled;
|
137
146
|
}
|
138
147
|
getMatchingTools(type) {
|
package/dist/src/tools/lib.d.ts
CHANGED
@@ -11,3 +11,4 @@ export { default as PenTool, PenStyle } from './Pen';
|
|
11
11
|
export { default as TextTool } from './TextTool';
|
12
12
|
export { default as SelectionTool } from './SelectionTool';
|
13
13
|
export { default as EraserTool } from './Eraser';
|
14
|
+
export { default as PasteHandler } from './PasteHandler';
|
package/dist/src/tools/lib.js
CHANGED
@@ -11,3 +11,4 @@ export { default as PenTool } from './Pen';
|
|
11
11
|
export { default as TextTool } from './TextTool';
|
12
12
|
export { default as SelectionTool } from './SelectionTool';
|
13
13
|
export { default as EraserTool } from './Eraser';
|
14
|
+
export { default as PasteHandler } from './PasteHandler';
|
@@ -11,6 +11,9 @@ export interface ToolLocalization {
|
|
11
11
|
textTool: string;
|
12
12
|
enterTextToInsert: string;
|
13
13
|
changeTool: string;
|
14
|
+
pasteHandler: string;
|
15
|
+
copied: (count: number, description: string) => string;
|
16
|
+
pasted: (count: number, description: string) => string;
|
14
17
|
toolEnabledAnnouncement: (toolName: string) => string;
|
15
18
|
toolDisabledAnnouncement: (toolName: string) => string;
|
16
19
|
}
|
@@ -11,6 +11,9 @@ export const defaultToolLocalization = {
|
|
11
11
|
textTool: 'Text',
|
12
12
|
enterTextToInsert: 'Text to insert',
|
13
13
|
changeTool: 'Change tool',
|
14
|
+
pasteHandler: 'Copy paste handler',
|
15
|
+
copied: (count, description) => `Copied ${count} ${description}`,
|
16
|
+
pasted: (count, description) => `Pasted ${count} ${description}`,
|
14
17
|
toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
|
15
18
|
toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,
|
16
19
|
};
|
package/dist/src/types.d.ts
CHANGED
@@ -22,7 +22,9 @@ export declare enum InputEvtType {
|
|
22
22
|
GestureCancelEvt = 3,
|
23
23
|
WheelEvt = 4,
|
24
24
|
KeyPressEvent = 5,
|
25
|
-
KeyUpEvent = 6
|
25
|
+
KeyUpEvent = 6,
|
26
|
+
CopyEvent = 7,
|
27
|
+
PasteEvent = 8
|
26
28
|
}
|
27
29
|
export interface WheelEvt {
|
28
30
|
readonly kind: InputEvtType.WheelEvt;
|
@@ -39,6 +41,15 @@ export interface KeyUpEvent {
|
|
39
41
|
readonly key: string;
|
40
42
|
readonly ctrlKey: boolean;
|
41
43
|
}
|
44
|
+
export interface CopyEvent {
|
45
|
+
readonly kind: InputEvtType.CopyEvent;
|
46
|
+
setData(mime: string, data: string): void;
|
47
|
+
}
|
48
|
+
export interface PasteEvent {
|
49
|
+
readonly kind: InputEvtType.PasteEvent;
|
50
|
+
readonly data: string;
|
51
|
+
readonly mime: string;
|
52
|
+
}
|
42
53
|
export interface GestureCancelEvt {
|
43
54
|
readonly kind: InputEvtType.GestureCancelEvt;
|
44
55
|
}
|
@@ -56,7 +67,7 @@ export interface PointerUpEvt extends PointerEvtBase {
|
|
56
67
|
readonly kind: InputEvtType.PointerUpEvt;
|
57
68
|
}
|
58
69
|
export declare type PointerEvt = PointerDownEvt | PointerMoveEvt | PointerUpEvt;
|
59
|
-
export declare type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt;
|
70
|
+
export declare type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt | CopyEvent | PasteEvent;
|
60
71
|
export declare type EditorNotifier = EventDispatcher<EditorEventType, EditorEventDataType>;
|
61
72
|
export declare enum EditorEventType {
|
62
73
|
ToolEnabled = 0,
|
package/dist/src/types.js
CHANGED
@@ -8,6 +8,8 @@ export var InputEvtType;
|
|
8
8
|
InputEvtType[InputEvtType["WheelEvt"] = 4] = "WheelEvt";
|
9
9
|
InputEvtType[InputEvtType["KeyPressEvent"] = 5] = "KeyPressEvent";
|
10
10
|
InputEvtType[InputEvtType["KeyUpEvent"] = 6] = "KeyUpEvent";
|
11
|
+
InputEvtType[InputEvtType["CopyEvent"] = 7] = "CopyEvent";
|
12
|
+
InputEvtType[InputEvtType["PasteEvent"] = 8] = "PasteEvent";
|
11
13
|
})(InputEvtType || (InputEvtType = {}));
|
12
14
|
export var EditorEventType;
|
13
15
|
(function (EditorEventType) {
|
package/package.json
CHANGED
package/src/Editor.ts
CHANGED
@@ -118,6 +118,7 @@ export class Editor {
|
|
118
118
|
private loadingWarning: HTMLElement;
|
119
119
|
private accessibilityAnnounceArea: HTMLElement;
|
120
120
|
private accessibilityControlArea: HTMLTextAreaElement;
|
121
|
+
private eventListenerTargets: HTMLElement[] = [];
|
121
122
|
|
122
123
|
private settings: EditorSettings;
|
123
124
|
|
@@ -435,6 +436,121 @@ export class Editor {
|
|
435
436
|
this.accessibilityControlArea.addEventListener('input', () => {
|
436
437
|
this.accessibilityControlArea.value = '';
|
437
438
|
});
|
439
|
+
|
440
|
+
document.addEventListener('copy', evt => {
|
441
|
+
if (!this.isEventSink(document.querySelector(':focus'))) {
|
442
|
+
return;
|
443
|
+
}
|
444
|
+
|
445
|
+
const clipboardData = evt.clipboardData;
|
446
|
+
|
447
|
+
if (this.toolController.dispatchInputEvent({
|
448
|
+
kind: InputEvtType.CopyEvent,
|
449
|
+
setData: (mime, data) => {
|
450
|
+
clipboardData?.setData(mime, data);
|
451
|
+
},
|
452
|
+
})) {
|
453
|
+
evt.preventDefault();
|
454
|
+
}
|
455
|
+
});
|
456
|
+
|
457
|
+
document.addEventListener('paste', evt => {
|
458
|
+
this.handlePaste(evt);
|
459
|
+
});
|
460
|
+
}
|
461
|
+
|
462
|
+
private isEventSink(evtTarget: Element|EventTarget|null) {
|
463
|
+
let currentElem: Element|null = evtTarget as Element|null;
|
464
|
+
while (currentElem !== null) {
|
465
|
+
for (const elem of this.eventListenerTargets) {
|
466
|
+
if (elem === currentElem) {
|
467
|
+
return true;
|
468
|
+
}
|
469
|
+
}
|
470
|
+
|
471
|
+
currentElem = (currentElem as Element).parentElement;
|
472
|
+
}
|
473
|
+
return false;
|
474
|
+
}
|
475
|
+
|
476
|
+
private async handlePaste(evt: DragEvent|ClipboardEvent) {
|
477
|
+
const target = document.querySelector(':focus') ?? evt.target;
|
478
|
+
if (!this.isEventSink(target)) {
|
479
|
+
return;
|
480
|
+
}
|
481
|
+
|
482
|
+
const clipboardData: DataTransfer = (evt as any).dataTransfer ?? (evt as any).clipboardData;
|
483
|
+
if (!clipboardData) {
|
484
|
+
return;
|
485
|
+
}
|
486
|
+
|
487
|
+
// Handle SVG files (prefer to PNG/JPEG)
|
488
|
+
for (const file of clipboardData.files) {
|
489
|
+
if (file.type.toLowerCase() === 'image/svg+xml') {
|
490
|
+
const text = await file.text();
|
491
|
+
if (this.toolController.dispatchInputEvent({
|
492
|
+
kind: InputEvtType.PasteEvent,
|
493
|
+
mime: file.type,
|
494
|
+
data: text,
|
495
|
+
})) {
|
496
|
+
evt.preventDefault();
|
497
|
+
return;
|
498
|
+
}
|
499
|
+
}
|
500
|
+
}
|
501
|
+
|
502
|
+
// Handle image files.
|
503
|
+
for (const file of clipboardData.files) {
|
504
|
+
const fileType = file.type.toLowerCase();
|
505
|
+
if (fileType === 'image/png' || fileType === 'image/jpg') {
|
506
|
+
const reader = new FileReader();
|
507
|
+
|
508
|
+
this.showLoadingWarning(0);
|
509
|
+
try {
|
510
|
+
const data = await new Promise((resolve: (result: string|null)=>void, reject) => {
|
511
|
+
reader.onload = () => resolve(reader.result as string|null);
|
512
|
+
reader.onerror = reject;
|
513
|
+
reader.onabort = reject;
|
514
|
+
reader.onprogress = (evt) => {
|
515
|
+
this.showLoadingWarning(evt.loaded / evt.total);
|
516
|
+
};
|
517
|
+
|
518
|
+
reader.readAsDataURL(file);
|
519
|
+
});
|
520
|
+
if (data && this.toolController.dispatchInputEvent({
|
521
|
+
kind: InputEvtType.PasteEvent,
|
522
|
+
mime: fileType,
|
523
|
+
data: data,
|
524
|
+
})) {
|
525
|
+
evt.preventDefault();
|
526
|
+
this.hideLoadingWarning();
|
527
|
+
return;
|
528
|
+
}
|
529
|
+
} catch (e) {
|
530
|
+
console.error('Error reading image:', e);
|
531
|
+
}
|
532
|
+
this.hideLoadingWarning();
|
533
|
+
}
|
534
|
+
}
|
535
|
+
|
536
|
+
// Supported MIMEs for text data, in order of preference
|
537
|
+
const supportedMIMEs = [
|
538
|
+
'image/svg+xml',
|
539
|
+
'text/plain',
|
540
|
+
];
|
541
|
+
|
542
|
+
for (const mime of supportedMIMEs) {
|
543
|
+
const data = clipboardData.getData(mime);
|
544
|
+
|
545
|
+
if (data && this.toolController.dispatchInputEvent({
|
546
|
+
kind: InputEvtType.PasteEvent,
|
547
|
+
mime,
|
548
|
+
data,
|
549
|
+
})) {
|
550
|
+
evt.preventDefault();
|
551
|
+
return;
|
552
|
+
}
|
553
|
+
}
|
438
554
|
}
|
439
555
|
|
440
556
|
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
@@ -463,6 +579,18 @@ export class Editor {
|
|
463
579
|
evt.preventDefault();
|
464
580
|
}
|
465
581
|
});
|
582
|
+
|
583
|
+
// Allow drop.
|
584
|
+
elem.ondragover = evt => {
|
585
|
+
evt.preventDefault();
|
586
|
+
};
|
587
|
+
|
588
|
+
elem.ondrop = evt => {
|
589
|
+
evt.preventDefault();
|
590
|
+
this.handlePaste(evt);
|
591
|
+
};
|
592
|
+
|
593
|
+
this.eventListenerTargets.push(elem);
|
466
594
|
}
|
467
595
|
|
468
596
|
/** `apply` a command. `command` will be announced for accessibility. */
|
@@ -509,6 +637,7 @@ export class Editor {
|
|
509
637
|
public async asyncApplyOrUnapplyCommands(
|
510
638
|
commands: Command[], apply: boolean, updateChunkSize: number
|
511
639
|
) {
|
640
|
+
console.assert(updateChunkSize > 0);
|
512
641
|
this.display.setDraftMode(true);
|
513
642
|
for (let i = 0; i < commands.length; i += updateChunkSize) {
|
514
643
|
this.showLoadingWarning(i / commands.length);
|
@@ -739,8 +868,8 @@ export class Editor {
|
|
739
868
|
* This is particularly useful when accessing a bundled version of the editor,
|
740
869
|
* where `SVGLoader.fromString` is unavailable.
|
741
870
|
*/
|
742
|
-
public async loadFromSVG(svgData: string) {
|
743
|
-
const loader = SVGLoader.fromString(svgData);
|
871
|
+
public async loadFromSVG(svgData: string, sanitize: boolean = false) {
|
872
|
+
const loader = SVGLoader.fromString(svgData, sanitize);
|
744
873
|
await this.loadFrom(loader);
|
745
874
|
}
|
746
875
|
}
|