js-draw 0.3.1 → 0.4.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/.github/ISSUE_TEMPLATE/translation.md +4 -1
- package/CHANGELOG.md +16 -0
- package/README.md +1 -3
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +15 -1
- package/dist/src/Editor.js +221 -78
- package/dist/src/EditorImage.js +4 -1
- package/dist/src/Pointer.d.ts +1 -1
- package/dist/src/Pointer.js +8 -3
- 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 +2 -0
- package/dist/src/Viewport.js +26 -5
- 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/language/assertions.d.ts +1 -0
- package/dist/src/language/assertions.js +5 -0
- package/dist/src/math/LineSegment2.d.ts +2 -0
- package/dist/src/math/LineSegment2.js +3 -0
- package/dist/src/math/Mat33.d.ts +38 -2
- package/dist/src/math/Mat33.js +30 -1
- package/dist/src/math/Path.d.ts +1 -1
- package/dist/src/math/Path.js +10 -8
- package/dist/src/math/Vec3.d.ts +11 -1
- package/dist/src/math/Vec3.js +15 -0
- package/dist/src/math/rounding.d.ts +1 -0
- package/dist/src/math/rounding.js +13 -6
- 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/AbstractRenderer.js +2 -1
- 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/toolbar/HTMLToolbar.js +5 -4
- package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -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 +144 -0
- package/dist/src/tools/Pen.js +1 -1
- package/dist/src/tools/SelectionTool/Selection.d.ts +54 -0
- package/dist/src/tools/SelectionTool/Selection.js +337 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +35 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.js +75 -0
- package/dist/src/tools/SelectionTool/SelectionTool.d.ts +31 -0
- package/dist/src/tools/SelectionTool/SelectionTool.js +276 -0
- package/dist/src/tools/SelectionTool/TransformMode.d.ts +34 -0
- package/dist/src/tools/SelectionTool/TransformMode.js +98 -0
- package/dist/src/tools/SelectionTool/types.d.ts +9 -0
- package/dist/src/tools/SelectionTool/types.js +11 -0
- package/dist/src/tools/ToolController.js +37 -28
- package/dist/src/tools/lib.d.ts +2 -1
- package/dist/src/tools/lib.js +2 -1
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/dist/src/types.d.ts +14 -3
- package/dist/src/types.js +2 -0
- package/package.json +1 -1
- package/src/Editor.css +1 -0
- package/src/Editor.ts +275 -109
- package/src/EditorImage.ts +7 -1
- package/src/Pointer.ts +8 -3
- package/src/SVGLoader.ts +90 -36
- package/src/UndoRedoHistory.test.ts +33 -0
- package/src/UndoRedoHistory.ts +8 -0
- package/src/Viewport.ts +30 -6
- 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 +53 -11
- package/src/components/ImageComponent.ts +149 -0
- package/src/components/Text.ts +2 -6
- package/src/components/builders/FreehandLineBuilder.ts +2 -2
- package/src/components/lib.ts +7 -2
- package/src/components/localization.ts +4 -0
- package/src/language/assertions.ts +6 -0
- package/src/math/LineSegment2.test.ts +9 -0
- package/src/math/LineSegment2.ts +5 -0
- package/src/math/Mat33.test.ts +14 -0
- package/src/math/Mat33.ts +43 -2
- package/src/math/Path.toString.test.ts +12 -1
- package/src/math/Path.ts +11 -9
- package/src/math/Vec3.ts +22 -1
- package/src/math/rounding.test.ts +30 -5
- package/src/math/rounding.ts +16 -7
- package/src/rendering/localization.ts +6 -0
- package/src/rendering/renderers/AbstractRenderer.ts +19 -2
- 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/toolbar/HTMLToolbar.ts +5 -4
- package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
- package/src/tools/BaseTool.ts +9 -1
- package/src/tools/PasteHandler.ts +159 -0
- package/src/tools/Pen.ts +1 -1
- package/src/tools/SelectionTool/Selection.ts +455 -0
- package/src/tools/SelectionTool/SelectionHandle.ts +99 -0
- package/src/tools/SelectionTool/SelectionTool.css +22 -0
- package/src/tools/{SelectionTool.test.ts → SelectionTool/SelectionTool.test.ts} +21 -21
- package/src/tools/SelectionTool/SelectionTool.ts +335 -0
- package/src/tools/SelectionTool/TransformMode.ts +114 -0
- package/src/tools/SelectionTool/types.ts +11 -0
- package/src/tools/ToolController.ts +52 -45
- package/src/tools/lib.ts +2 -1
- package/src/tools/localization.ts +8 -0
- package/src/types.ts +17 -3
- package/dist/src/tools/SelectionTool.d.ts +0 -59
- package/dist/src/tools/SelectionTool.js +0 -589
- package/src/tools/SelectionTool.ts +0 -725
package/dist/src/Editor.d.ts
CHANGED
@@ -28,6 +28,8 @@ import Display, { RenderingMode } from './rendering/Display';
|
|
28
28
|
import Pointer from './Pointer';
|
29
29
|
import Rect2 from './math/Rect2';
|
30
30
|
import { EditorLocalization } from './localization';
|
31
|
+
declare type HTMLPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel';
|
32
|
+
declare type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent) => boolean;
|
31
33
|
export interface EditorSettings {
|
32
34
|
/** Defaults to `RenderingMode.CanvasRenderer` */
|
33
35
|
renderingMode: RenderingMode;
|
@@ -94,6 +96,7 @@ export declare class Editor {
|
|
94
96
|
private loadingWarning;
|
95
97
|
private accessibilityAnnounceArea;
|
96
98
|
private accessibilityControlArea;
|
99
|
+
private eventListenerTargets;
|
97
100
|
private settings;
|
98
101
|
/**
|
99
102
|
* @example
|
@@ -139,6 +142,16 @@ export declare class Editor {
|
|
139
142
|
*/
|
140
143
|
addToolbar(defaultLayout?: boolean): HTMLToolbar;
|
141
144
|
private registerListeners;
|
145
|
+
private pointers;
|
146
|
+
private getPointerList;
|
147
|
+
/**
|
148
|
+
* Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
|
149
|
+
* as the content of the editor.
|
150
|
+
*/
|
151
|
+
handleHTMLPointerEvent(eventType: 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel', evt: PointerEvent): boolean;
|
152
|
+
private isEventSink;
|
153
|
+
private handlePaste;
|
154
|
+
handlePointerEventsFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter): void;
|
142
155
|
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
143
156
|
handleKeyEventsFrom(elem: HTMLElement): void;
|
144
157
|
/** `apply` a command. `command` will be announced for accessibility. */
|
@@ -180,6 +193,7 @@ export declare class Editor {
|
|
180
193
|
remove: () => void;
|
181
194
|
};
|
182
195
|
addStyleSheet(content: string): HTMLStyleElement;
|
196
|
+
sendKeyboardEvent(eventType: InputEvtType.KeyPressEvent | InputEvtType.KeyUpEvent, key: string, ctrlKey?: boolean): void;
|
183
197
|
sendPenEvent(eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, point: Point2, allPointers?: Pointer[]): void;
|
184
198
|
toSVG(): SVGElement;
|
185
199
|
loadFrom(loader: ImageLoader): Promise<void>;
|
@@ -191,6 +205,6 @@ export declare class Editor {
|
|
191
205
|
* This is particularly useful when accessing a bundled version of the editor,
|
192
206
|
* where `SVGLoader.fromString` is unavailable.
|
193
207
|
*/
|
194
|
-
loadFromSVG(svgData: string): Promise<void>;
|
208
|
+
loadFromSVG(svgData: string, sanitize?: boolean): Promise<void>;
|
195
209
|
}
|
196
210
|
export default Editor;
|
package/dist/src/Editor.js
CHANGED
@@ -68,7 +68,9 @@ export class Editor {
|
|
68
68
|
*/
|
69
69
|
constructor(parent, settings = {}) {
|
70
70
|
var _a, _b, _c, _d;
|
71
|
+
this.eventListenerTargets = [];
|
71
72
|
this.previousAccessibilityAnnouncement = '';
|
73
|
+
this.pointers = {};
|
72
74
|
this.announceUndoCallback = (command) => {
|
73
75
|
this.announceForAccessibility(this.localization.undoAnnouncement(command.description(this, this.localization)));
|
74
76
|
};
|
@@ -181,81 +183,7 @@ export class Editor {
|
|
181
183
|
return toolbar;
|
182
184
|
}
|
183
185
|
registerListeners() {
|
184
|
-
|
185
|
-
const getPointerList = () => {
|
186
|
-
const nowTime = (new Date()).getTime();
|
187
|
-
const res = [];
|
188
|
-
for (const id in pointers) {
|
189
|
-
const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
|
190
|
-
if (pointers[id] && (nowTime - pointers[id].timeStamp) < maxUnupdatedTime) {
|
191
|
-
res.push(pointers[id]);
|
192
|
-
}
|
193
|
-
}
|
194
|
-
return res;
|
195
|
-
};
|
196
|
-
// May be required to prevent text selection on iOS/Safari:
|
197
|
-
// See https://stackoverflow.com/a/70992717/17055750
|
198
|
-
this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
|
199
|
-
this.renderingRegion.addEventListener('contextmenu', evt => {
|
200
|
-
// Don't show a context menu
|
201
|
-
evt.preventDefault();
|
202
|
-
});
|
203
|
-
this.renderingRegion.addEventListener('pointerdown', evt => {
|
204
|
-
const pointer = Pointer.ofEvent(evt, true, this.viewport);
|
205
|
-
pointers[pointer.id] = pointer;
|
206
|
-
this.renderingRegion.setPointerCapture(pointer.id);
|
207
|
-
const event = {
|
208
|
-
kind: InputEvtType.PointerDownEvt,
|
209
|
-
current: pointer,
|
210
|
-
allPointers: getPointerList(),
|
211
|
-
};
|
212
|
-
this.toolController.dispatchInputEvent(event);
|
213
|
-
return true;
|
214
|
-
});
|
215
|
-
this.renderingRegion.addEventListener('pointermove', evt => {
|
216
|
-
var _a, _b;
|
217
|
-
const pointer = Pointer.ofEvent(evt, (_b = (_a = pointers[evt.pointerId]) === null || _a === void 0 ? void 0 : _a.down) !== null && _b !== void 0 ? _b : false, this.viewport);
|
218
|
-
if (pointer.down) {
|
219
|
-
const prevData = pointers[pointer.id];
|
220
|
-
if (prevData) {
|
221
|
-
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
|
222
|
-
// If the pointer moved less than two pixels, don't send a new event.
|
223
|
-
if (distanceMoved < 2) {
|
224
|
-
return;
|
225
|
-
}
|
226
|
-
}
|
227
|
-
pointers[pointer.id] = pointer;
|
228
|
-
if (this.toolController.dispatchInputEvent({
|
229
|
-
kind: InputEvtType.PointerMoveEvt,
|
230
|
-
current: pointer,
|
231
|
-
allPointers: getPointerList(),
|
232
|
-
})) {
|
233
|
-
evt.preventDefault();
|
234
|
-
}
|
235
|
-
}
|
236
|
-
});
|
237
|
-
const pointerEnd = (evt) => {
|
238
|
-
const pointer = Pointer.ofEvent(evt, false, this.viewport);
|
239
|
-
if (!pointers[pointer.id]) {
|
240
|
-
return;
|
241
|
-
}
|
242
|
-
pointers[pointer.id] = pointer;
|
243
|
-
this.renderingRegion.releasePointerCapture(pointer.id);
|
244
|
-
if (this.toolController.dispatchInputEvent({
|
245
|
-
kind: InputEvtType.PointerUpEvt,
|
246
|
-
current: pointer,
|
247
|
-
allPointers: getPointerList(),
|
248
|
-
})) {
|
249
|
-
evt.preventDefault();
|
250
|
-
}
|
251
|
-
delete pointers[pointer.id];
|
252
|
-
};
|
253
|
-
this.renderingRegion.addEventListener('pointerup', evt => {
|
254
|
-
pointerEnd(evt);
|
255
|
-
});
|
256
|
-
this.renderingRegion.addEventListener('pointercancel', evt => {
|
257
|
-
pointerEnd(evt);
|
258
|
-
});
|
186
|
+
this.handlePointerEventsFrom(this.renderingRegion);
|
259
187
|
this.handleKeyEventsFrom(this.renderingRegion);
|
260
188
|
this.container.addEventListener('wheel', evt => {
|
261
189
|
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
@@ -281,7 +209,9 @@ export class Editor {
|
|
281
209
|
if (evt.ctrlKey) {
|
282
210
|
delta = Vec3.of(0, 0, evt.deltaY);
|
283
211
|
}
|
284
|
-
|
212
|
+
// Ensure that `pos` is relative to `this.container`
|
213
|
+
const bbox = this.container.getBoundingClientRect();
|
214
|
+
const pos = Vec2.of(evt.clientX, evt.clientY).minus(Vec2.of(bbox.left, bbox.top));
|
285
215
|
if (this.toolController.dispatchInputEvent({
|
286
216
|
kind: InputEvtType.WheelEvt,
|
287
217
|
delta,
|
@@ -305,6 +235,200 @@ export class Editor {
|
|
305
235
|
this.accessibilityControlArea.addEventListener('input', () => {
|
306
236
|
this.accessibilityControlArea.value = '';
|
307
237
|
});
|
238
|
+
document.addEventListener('copy', evt => {
|
239
|
+
if (!this.isEventSink(document.querySelector(':focus'))) {
|
240
|
+
return;
|
241
|
+
}
|
242
|
+
const clipboardData = evt.clipboardData;
|
243
|
+
if (this.toolController.dispatchInputEvent({
|
244
|
+
kind: InputEvtType.CopyEvent,
|
245
|
+
setData: (mime, data) => {
|
246
|
+
clipboardData === null || clipboardData === void 0 ? void 0 : clipboardData.setData(mime, data);
|
247
|
+
},
|
248
|
+
})) {
|
249
|
+
evt.preventDefault();
|
250
|
+
}
|
251
|
+
});
|
252
|
+
document.addEventListener('paste', evt => {
|
253
|
+
this.handlePaste(evt);
|
254
|
+
});
|
255
|
+
}
|
256
|
+
getPointerList() {
|
257
|
+
const nowTime = (new Date()).getTime();
|
258
|
+
const res = [];
|
259
|
+
for (const id in this.pointers) {
|
260
|
+
const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
|
261
|
+
if (this.pointers[id] && (nowTime - this.pointers[id].timeStamp) < maxUnupdatedTime) {
|
262
|
+
res.push(this.pointers[id]);
|
263
|
+
}
|
264
|
+
}
|
265
|
+
return res;
|
266
|
+
}
|
267
|
+
/**
|
268
|
+
* Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
|
269
|
+
* as the content of the editor.
|
270
|
+
*/
|
271
|
+
handleHTMLPointerEvent(eventType, evt) {
|
272
|
+
var _a, _b, _c;
|
273
|
+
const eventsRelativeTo = this.renderingRegion;
|
274
|
+
const eventTarget = (_a = evt.target) !== null && _a !== void 0 ? _a : this.renderingRegion;
|
275
|
+
if (eventType === 'pointerdown') {
|
276
|
+
const pointer = Pointer.ofEvent(evt, true, this.viewport, eventsRelativeTo);
|
277
|
+
this.pointers[pointer.id] = pointer;
|
278
|
+
eventTarget.setPointerCapture(pointer.id);
|
279
|
+
const event = {
|
280
|
+
kind: InputEvtType.PointerDownEvt,
|
281
|
+
current: pointer,
|
282
|
+
allPointers: this.getPointerList(),
|
283
|
+
};
|
284
|
+
this.toolController.dispatchInputEvent(event);
|
285
|
+
return true;
|
286
|
+
}
|
287
|
+
else if (eventType === 'pointermove') {
|
288
|
+
const pointer = Pointer.ofEvent(evt, (_c = (_b = this.pointers[evt.pointerId]) === null || _b === void 0 ? void 0 : _b.down) !== null && _c !== void 0 ? _c : false, this.viewport, eventsRelativeTo);
|
289
|
+
if (pointer.down) {
|
290
|
+
const prevData = this.pointers[pointer.id];
|
291
|
+
if (prevData) {
|
292
|
+
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
|
293
|
+
// If the pointer moved less than two pixels, don't send a new event.
|
294
|
+
if (distanceMoved < 2) {
|
295
|
+
return false;
|
296
|
+
}
|
297
|
+
}
|
298
|
+
this.pointers[pointer.id] = pointer;
|
299
|
+
if (this.toolController.dispatchInputEvent({
|
300
|
+
kind: InputEvtType.PointerMoveEvt,
|
301
|
+
current: pointer,
|
302
|
+
allPointers: this.getPointerList(),
|
303
|
+
})) {
|
304
|
+
evt.preventDefault();
|
305
|
+
}
|
306
|
+
}
|
307
|
+
return true;
|
308
|
+
}
|
309
|
+
else if (eventType === 'pointercancel' || eventType === 'pointerup') {
|
310
|
+
const pointer = Pointer.ofEvent(evt, false, this.viewport, eventsRelativeTo);
|
311
|
+
if (!this.pointers[pointer.id]) {
|
312
|
+
return false;
|
313
|
+
}
|
314
|
+
this.pointers[pointer.id] = pointer;
|
315
|
+
eventTarget.releasePointerCapture(pointer.id);
|
316
|
+
if (this.toolController.dispatchInputEvent({
|
317
|
+
kind: InputEvtType.PointerUpEvt,
|
318
|
+
current: pointer,
|
319
|
+
allPointers: this.getPointerList(),
|
320
|
+
})) {
|
321
|
+
evt.preventDefault();
|
322
|
+
}
|
323
|
+
delete this.pointers[pointer.id];
|
324
|
+
return true;
|
325
|
+
}
|
326
|
+
return eventType;
|
327
|
+
}
|
328
|
+
isEventSink(evtTarget) {
|
329
|
+
let currentElem = evtTarget;
|
330
|
+
while (currentElem !== null) {
|
331
|
+
for (const elem of this.eventListenerTargets) {
|
332
|
+
if (elem === currentElem) {
|
333
|
+
return true;
|
334
|
+
}
|
335
|
+
}
|
336
|
+
currentElem = currentElem.parentElement;
|
337
|
+
}
|
338
|
+
return false;
|
339
|
+
}
|
340
|
+
handlePaste(evt) {
|
341
|
+
var _a, _b;
|
342
|
+
return __awaiter(this, void 0, void 0, function* () {
|
343
|
+
const target = (_a = document.querySelector(':focus')) !== null && _a !== void 0 ? _a : evt.target;
|
344
|
+
if (!this.isEventSink(target)) {
|
345
|
+
return;
|
346
|
+
}
|
347
|
+
const clipboardData = (_b = evt.dataTransfer) !== null && _b !== void 0 ? _b : evt.clipboardData;
|
348
|
+
if (!clipboardData) {
|
349
|
+
return;
|
350
|
+
}
|
351
|
+
// Handle SVG files (prefer to PNG/JPEG)
|
352
|
+
for (const file of clipboardData.files) {
|
353
|
+
if (file.type.toLowerCase() === 'image/svg+xml') {
|
354
|
+
const text = yield file.text();
|
355
|
+
if (this.toolController.dispatchInputEvent({
|
356
|
+
kind: InputEvtType.PasteEvent,
|
357
|
+
mime: file.type,
|
358
|
+
data: text,
|
359
|
+
})) {
|
360
|
+
evt.preventDefault();
|
361
|
+
return;
|
362
|
+
}
|
363
|
+
}
|
364
|
+
}
|
365
|
+
// Handle image files.
|
366
|
+
for (const file of clipboardData.files) {
|
367
|
+
const fileType = file.type.toLowerCase();
|
368
|
+
if (fileType === 'image/png' || fileType === 'image/jpg') {
|
369
|
+
const reader = new FileReader();
|
370
|
+
this.showLoadingWarning(0);
|
371
|
+
try {
|
372
|
+
const data = yield new Promise((resolve, reject) => {
|
373
|
+
reader.onload = () => resolve(reader.result);
|
374
|
+
reader.onerror = reject;
|
375
|
+
reader.onabort = reject;
|
376
|
+
reader.onprogress = (evt) => {
|
377
|
+
this.showLoadingWarning(evt.loaded / evt.total);
|
378
|
+
};
|
379
|
+
reader.readAsDataURL(file);
|
380
|
+
});
|
381
|
+
if (data && this.toolController.dispatchInputEvent({
|
382
|
+
kind: InputEvtType.PasteEvent,
|
383
|
+
mime: fileType,
|
384
|
+
data: data,
|
385
|
+
})) {
|
386
|
+
evt.preventDefault();
|
387
|
+
this.hideLoadingWarning();
|
388
|
+
return;
|
389
|
+
}
|
390
|
+
}
|
391
|
+
catch (e) {
|
392
|
+
console.error('Error reading image:', e);
|
393
|
+
}
|
394
|
+
this.hideLoadingWarning();
|
395
|
+
}
|
396
|
+
}
|
397
|
+
// Supported MIMEs for text data, in order of preference
|
398
|
+
const supportedMIMEs = [
|
399
|
+
'image/svg+xml',
|
400
|
+
'text/plain',
|
401
|
+
];
|
402
|
+
for (const mime of supportedMIMEs) {
|
403
|
+
const data = clipboardData.getData(mime);
|
404
|
+
if (data && this.toolController.dispatchInputEvent({
|
405
|
+
kind: InputEvtType.PasteEvent,
|
406
|
+
mime,
|
407
|
+
data,
|
408
|
+
})) {
|
409
|
+
evt.preventDefault();
|
410
|
+
return;
|
411
|
+
}
|
412
|
+
}
|
413
|
+
});
|
414
|
+
}
|
415
|
+
handlePointerEventsFrom(elem, filter) {
|
416
|
+
// May be required to prevent text selection on iOS/Safari:
|
417
|
+
// See https://stackoverflow.com/a/70992717/17055750
|
418
|
+
elem.addEventListener('touchstart', evt => evt.preventDefault());
|
419
|
+
elem.addEventListener('contextmenu', evt => {
|
420
|
+
// Don't show a context menu
|
421
|
+
evt.preventDefault();
|
422
|
+
});
|
423
|
+
const eventNames = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'];
|
424
|
+
for (const eventName of eventNames) {
|
425
|
+
elem.addEventListener(eventName, evt => {
|
426
|
+
if (filter && !filter(eventName, evt)) {
|
427
|
+
return true;
|
428
|
+
}
|
429
|
+
return this.handleHTMLPointerEvent(eventName, evt);
|
430
|
+
});
|
431
|
+
}
|
308
432
|
}
|
309
433
|
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
310
434
|
handleKeyEventsFrom(elem) {
|
@@ -333,6 +457,15 @@ export class Editor {
|
|
333
457
|
evt.preventDefault();
|
334
458
|
}
|
335
459
|
});
|
460
|
+
// Allow drop.
|
461
|
+
elem.ondragover = evt => {
|
462
|
+
evt.preventDefault();
|
463
|
+
};
|
464
|
+
elem.ondrop = evt => {
|
465
|
+
evt.preventDefault();
|
466
|
+
this.handlePaste(evt);
|
467
|
+
};
|
468
|
+
this.eventListenerTargets.push(elem);
|
336
469
|
}
|
337
470
|
/** `apply` a command. `command` will be announced for accessibility. */
|
338
471
|
dispatch(command, addToHistory = true) {
|
@@ -376,6 +509,7 @@ export class Editor {
|
|
376
509
|
*/
|
377
510
|
asyncApplyOrUnapplyCommands(commands, apply, updateChunkSize) {
|
378
511
|
return __awaiter(this, void 0, void 0, function* () {
|
512
|
+
console.assert(updateChunkSize > 0);
|
379
513
|
this.display.setDraftMode(true);
|
380
514
|
for (let i = 0; i < commands.length; i += updateChunkSize) {
|
381
515
|
this.showLoadingWarning(i / commands.length);
|
@@ -462,6 +596,15 @@ export class Editor {
|
|
462
596
|
this.container.appendChild(styleSheet);
|
463
597
|
return styleSheet;
|
464
598
|
}
|
599
|
+
// Dispatch a keyboard event to the currently selected tool.
|
600
|
+
// Intended for unit testing
|
601
|
+
sendKeyboardEvent(eventType, key, ctrlKey = false) {
|
602
|
+
this.toolController.dispatchInputEvent({
|
603
|
+
kind: eventType,
|
604
|
+
key,
|
605
|
+
ctrlKey
|
606
|
+
});
|
607
|
+
}
|
465
608
|
// Dispatch a pen event to the currently selected tool.
|
466
609
|
// Intended primarially for unit tests.
|
467
610
|
sendPenEvent(eventType, point, allPointers) {
|
@@ -553,9 +696,9 @@ export class Editor {
|
|
553
696
|
* This is particularly useful when accessing a bundled version of the editor,
|
554
697
|
* where `SVGLoader.fromString` is unavailable.
|
555
698
|
*/
|
556
|
-
loadFromSVG(svgData) {
|
699
|
+
loadFromSVG(svgData, sanitize = false) {
|
557
700
|
return __awaiter(this, void 0, void 0, function* () {
|
558
|
-
const loader = SVGLoader.fromString(svgData);
|
701
|
+
const loader = SVGLoader.fromString(svgData, sanitize);
|
559
702
|
yield this.loadFrom(loader);
|
560
703
|
});
|
561
704
|
}
|
package/dist/src/EditorImage.js
CHANGED
@@ -69,6 +69,9 @@ EditorImage.AddElementCommand = (_a = class extends SerializableCommand {
|
|
69
69
|
super('add-element');
|
70
70
|
this.element = element;
|
71
71
|
this.applyByFlattening = applyByFlattening;
|
72
|
+
// Store the element's serialization --- .serializeToJSON may be called on this
|
73
|
+
// even when this is not at the top of the undo/redo stack.
|
74
|
+
this.serializedElem = element.serialize();
|
72
75
|
if (isNaN(element.getBBox().area)) {
|
73
76
|
throw new Error('Elements in the image cannot have NaN bounding boxes');
|
74
77
|
}
|
@@ -93,7 +96,7 @@ EditorImage.AddElementCommand = (_a = class extends SerializableCommand {
|
|
93
96
|
}
|
94
97
|
serializeToJSON() {
|
95
98
|
return {
|
96
|
-
elemData: this.
|
99
|
+
elemData: this.serializedElem,
|
97
100
|
};
|
98
101
|
}
|
99
102
|
},
|
package/dist/src/Pointer.d.ts
CHANGED
@@ -18,6 +18,6 @@ export default class Pointer {
|
|
18
18
|
readonly id: number;
|
19
19
|
readonly timeStamp: number;
|
20
20
|
private constructor();
|
21
|
-
static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport): Pointer;
|
21
|
+
static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer;
|
22
22
|
static ofCanvasPoint(canvasPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null): Pointer;
|
23
23
|
}
|
package/dist/src/Pointer.js
CHANGED
@@ -31,10 +31,15 @@ export default class Pointer {
|
|
31
31
|
this.id = id;
|
32
32
|
this.timeStamp = timeStamp;
|
33
33
|
}
|
34
|
-
// Creates a Pointer from a DOM event.
|
35
|
-
|
34
|
+
// Creates a Pointer from a DOM event. If `relativeTo` is given, (0, 0) in screen coordinates is
|
35
|
+
// considered the top left of `relativeTo`.
|
36
|
+
static ofEvent(evt, isDown, viewport, relativeTo) {
|
36
37
|
var _a, _b;
|
37
|
-
|
38
|
+
let screenPos = Vec2.of(evt.clientX, evt.clientY);
|
39
|
+
if (relativeTo) {
|
40
|
+
const bbox = relativeTo.getBoundingClientRect();
|
41
|
+
screenPos = screenPos.minus(Vec2.of(bbox.left, bbox.top));
|
42
|
+
}
|
38
43
|
const pointerTypeToDevice = {
|
39
44
|
'mouse': PointerDevice.PrimaryButtonMouse,
|
40
45
|
'pen': PointerDevice.Pen,
|
package/dist/src/SVGLoader.d.ts
CHANGED
@@ -12,6 +12,7 @@ export declare type SVGLoaderUnknownStyleAttribute = {
|
|
12
12
|
export default class SVGLoader implements ImageLoader {
|
13
13
|
private source;
|
14
14
|
private onFinish?;
|
15
|
+
private readonly storeUnknown;
|
15
16
|
private onAddComponent;
|
16
17
|
private onProgress;
|
17
18
|
private onDetermineExportRect;
|
@@ -23,13 +24,15 @@ export default class SVGLoader implements ImageLoader {
|
|
23
24
|
private strokeDataFromElem;
|
24
25
|
private attachUnrecognisedAttrs;
|
25
26
|
private addPath;
|
27
|
+
private getTransform;
|
26
28
|
private makeText;
|
27
29
|
private addText;
|
30
|
+
private addImage;
|
28
31
|
private addUnknownNode;
|
29
32
|
private updateViewBox;
|
30
33
|
private updateSVGAttrs;
|
31
34
|
private visit;
|
32
35
|
private getSourceAttrs;
|
33
36
|
start(onAddComponent: ComponentAddedListener, onProgress: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener | null): Promise<void>;
|
34
|
-
static fromString(text: string): SVGLoader;
|
37
|
+
static fromString(text: string, sanitize?: boolean): SVGLoader;
|
35
38
|
}
|
package/dist/src/SVGLoader.js
CHANGED
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
8
8
|
});
|
9
9
|
};
|
10
10
|
import Color4 from './Color4';
|
11
|
+
import ImageComponent from './components/ImageComponent';
|
11
12
|
import Stroke from './components/Stroke';
|
12
13
|
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
13
14
|
import Text from './components/Text';
|
@@ -22,9 +23,10 @@ export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
|
|
22
23
|
export const svgAttributesDataKey = 'svgAttrs';
|
23
24
|
export const svgStyleAttributesDataKey = 'svgStyleAttrs';
|
24
25
|
export default class SVGLoader {
|
25
|
-
constructor(source, onFinish) {
|
26
|
+
constructor(source, onFinish, storeUnknown = true) {
|
26
27
|
this.source = source;
|
27
28
|
this.onFinish = onFinish;
|
29
|
+
this.storeUnknown = storeUnknown;
|
28
30
|
this.onAddComponent = null;
|
29
31
|
this.onProgress = null;
|
30
32
|
this.onDetermineExportRect = null;
|
@@ -88,6 +90,9 @@ export default class SVGLoader {
|
|
88
90
|
return result;
|
89
91
|
}
|
90
92
|
attachUnrecognisedAttrs(elem, node, supportedAttrs, supportedStyleAttrs) {
|
93
|
+
if (!this.storeUnknown) {
|
94
|
+
return;
|
95
|
+
}
|
91
96
|
for (const attr of node.getAttributeNames()) {
|
92
97
|
if (supportedAttrs.has(attr) || (attr === 'style' && supportedStyleAttrs)) {
|
93
98
|
continue;
|
@@ -123,10 +128,45 @@ export default class SVGLoader {
|
|
123
128
|
}
|
124
129
|
catch (e) {
|
125
130
|
console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
|
126
|
-
|
131
|
+
if (this.storeUnknown) {
|
132
|
+
elem = new UnknownSVGObject(node);
|
133
|
+
}
|
134
|
+
else {
|
135
|
+
return;
|
136
|
+
}
|
127
137
|
}
|
128
138
|
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem);
|
129
139
|
}
|
140
|
+
// If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
|
141
|
+
// to prevent storing duplicate transform information when saving the component.
|
142
|
+
getTransform(elem, supportedAttrs, computedStyles) {
|
143
|
+
computedStyles !== null && computedStyles !== void 0 ? computedStyles : (computedStyles = window.getComputedStyle(elem));
|
144
|
+
let transformProperty = computedStyles.transform;
|
145
|
+
if (transformProperty === '' || transformProperty === 'none') {
|
146
|
+
transformProperty = elem.style.transform || 'none';
|
147
|
+
}
|
148
|
+
// Prefer the actual .style.transform
|
149
|
+
// to the computed stylesheet -- in some browsers, the computedStyles version
|
150
|
+
// can have lower precision.
|
151
|
+
let transform;
|
152
|
+
try {
|
153
|
+
transform = Mat33.fromCSSMatrix(elem.style.transform);
|
154
|
+
}
|
155
|
+
catch (_e) {
|
156
|
+
transform = Mat33.fromCSSMatrix(transformProperty);
|
157
|
+
}
|
158
|
+
const elemX = elem.getAttribute('x');
|
159
|
+
const elemY = elem.getAttribute('y');
|
160
|
+
if (elemX && elemY) {
|
161
|
+
const x = parseFloat(elemX);
|
162
|
+
const y = parseFloat(elemY);
|
163
|
+
if (!isNaN(x) && !isNaN(y)) {
|
164
|
+
supportedAttrs === null || supportedAttrs === void 0 ? void 0 : supportedAttrs.push('x', 'y');
|
165
|
+
transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
|
166
|
+
}
|
167
|
+
}
|
168
|
+
return transform;
|
169
|
+
}
|
130
170
|
makeText(elem) {
|
131
171
|
var _a;
|
132
172
|
const contentList = [];
|
@@ -167,31 +207,8 @@ export default class SVGLoader {
|
|
167
207
|
fill: Color4.fromString(computedStyles.fill)
|
168
208
|
},
|
169
209
|
};
|
170
|
-
let transformProperty = computedStyles.transform;
|
171
|
-
if (transformProperty === '' || transformProperty === 'none') {
|
172
|
-
transformProperty = elem.style.transform || 'none';
|
173
|
-
}
|
174
|
-
// Compute transform matrix. Prefer the actual .style.transform
|
175
|
-
// to the computed stylesheet -- in some browsers, the computedStyles version
|
176
|
-
// can have lower precision.
|
177
|
-
let transform;
|
178
|
-
try {
|
179
|
-
transform = Mat33.fromCSSMatrix(elem.style.transform);
|
180
|
-
}
|
181
|
-
catch (_e) {
|
182
|
-
transform = Mat33.fromCSSMatrix(transformProperty);
|
183
|
-
}
|
184
210
|
const supportedAttrs = [];
|
185
|
-
const
|
186
|
-
const elemY = elem.getAttribute('y');
|
187
|
-
if (elemX && elemY) {
|
188
|
-
const x = parseFloat(elemX);
|
189
|
-
const y = parseFloat(elemY);
|
190
|
-
if (!isNaN(x) && !isNaN(y)) {
|
191
|
-
supportedAttrs.push('x', 'y');
|
192
|
-
transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
|
193
|
-
}
|
194
|
-
}
|
211
|
+
const transform = this.getTransform(elem, supportedAttrs, computedStyles);
|
195
212
|
const result = new Text(contentList, transform, style);
|
196
213
|
this.attachUnrecognisedAttrs(result, elem, new Set(supportedAttrs), new Set(supportedStyleAttrs));
|
197
214
|
return result;
|
@@ -203,14 +220,34 @@ export default class SVGLoader {
|
|
203
220
|
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, textElem);
|
204
221
|
}
|
205
222
|
catch (e) {
|
206
|
-
console.error('Invalid text object in node', elem, '.
|
223
|
+
console.error('Invalid text object in node', elem, '. Continuing.... Error:', e);
|
207
224
|
this.addUnknownNode(elem);
|
208
225
|
}
|
209
226
|
}
|
227
|
+
addImage(elem) {
|
228
|
+
var _a, _b;
|
229
|
+
return __awaiter(this, void 0, void 0, function* () {
|
230
|
+
const image = new Image();
|
231
|
+
image.src = (_a = elem.getAttribute('xlink:href')) !== null && _a !== void 0 ? _a : elem.href.baseVal;
|
232
|
+
try {
|
233
|
+
const supportedAttrs = [];
|
234
|
+
const transform = this.getTransform(elem, supportedAttrs);
|
235
|
+
const imageElem = yield ImageComponent.fromImage(image, transform);
|
236
|
+
this.attachUnrecognisedAttrs(imageElem, elem, new Set(supportedAttrs), new Set(['transform']));
|
237
|
+
(_b = this.onAddComponent) === null || _b === void 0 ? void 0 : _b.call(this, imageElem);
|
238
|
+
}
|
239
|
+
catch (e) {
|
240
|
+
console.error('Error loading image:', e, '. Element: ', elem, '. Continuing...');
|
241
|
+
this.addUnknownNode(elem);
|
242
|
+
}
|
243
|
+
});
|
244
|
+
}
|
210
245
|
addUnknownNode(node) {
|
211
246
|
var _a;
|
212
|
-
|
213
|
-
|
247
|
+
if (this.storeUnknown) {
|
248
|
+
const component = new UnknownSVGObject(node);
|
249
|
+
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, component);
|
250
|
+
}
|
214
251
|
}
|
215
252
|
updateViewBox(node) {
|
216
253
|
var _a;
|
@@ -232,7 +269,9 @@ export default class SVGLoader {
|
|
232
269
|
}
|
233
270
|
updateSVGAttrs(node) {
|
234
271
|
var _a;
|
235
|
-
(
|
272
|
+
if (this.storeUnknown) {
|
273
|
+
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
|
274
|
+
}
|
236
275
|
}
|
237
276
|
visit(node) {
|
238
277
|
var _a;
|
@@ -250,6 +289,11 @@ export default class SVGLoader {
|
|
250
289
|
this.addText(node);
|
251
290
|
visitChildren = false;
|
252
291
|
break;
|
292
|
+
case 'image':
|
293
|
+
yield this.addImage(node);
|
294
|
+
// Images should not have children.
|
295
|
+
visitChildren = false;
|
296
|
+
break;
|
253
297
|
case 'svg':
|
254
298
|
this.updateViewBox(node);
|
255
299
|
this.updateSVGAttrs(node);
|
@@ -257,7 +301,7 @@ export default class SVGLoader {
|
|
257
301
|
default:
|
258
302
|
console.warn('Unknown SVG element,', node);
|
259
303
|
if (!(node instanceof SVGElement)) {
|
260
|
-
console.warn('Element', node, 'is not an SVGElement! Continuing anyway.');
|
304
|
+
console.warn('Element', node, 'is not an SVGElement!', this.storeUnknown ? 'Continuing anyway.' : 'Skipping.');
|
261
305
|
}
|
262
306
|
this.addUnknownNode(node);
|
263
307
|
return;
|
@@ -296,7 +340,8 @@ export default class SVGLoader {
|
|
296
340
|
});
|
297
341
|
}
|
298
342
|
// TODO: Handling unsafe data! Tripple-check that this is secure!
|
299
|
-
|
343
|
+
// @param sanitize - if `true`, don't store unknown attributes.
|
344
|
+
static fromString(text, sanitize = false) {
|
300
345
|
var _a, _b;
|
301
346
|
const sandbox = document.createElement('iframe');
|
302
347
|
sandbox.src = 'about:blank';
|
@@ -336,6 +381,6 @@ export default class SVGLoader {
|
|
336
381
|
return new SVGLoader(svgElem, () => {
|
337
382
|
svgElem.remove();
|
338
383
|
sandbox.remove();
|
339
|
-
});
|
384
|
+
}, !sanitize);
|
340
385
|
}
|
341
386
|
}
|
@@ -8,6 +8,7 @@ declare class UndoRedoHistory {
|
|
8
8
|
private announceUndoCallback;
|
9
9
|
private undoStack;
|
10
10
|
private redoStack;
|
11
|
+
private maxUndoRedoStackSize;
|
11
12
|
constructor(editor: Editor, announceRedoCallback: AnnounceRedoCallback, announceUndoCallback: AnnounceUndoCallback);
|
12
13
|
private fireUpdateEvent;
|
13
14
|
push(command: Command, apply?: boolean): void;
|