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/src/Editor.ts
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
/**
|
2
2
|
* The main entrypoint for the full editor.
|
3
|
-
*
|
3
|
+
*
|
4
4
|
* @example
|
5
5
|
* To create an editor with a toolbar,
|
6
6
|
* ```
|
7
7
|
* const editor = new Editor(document.body);
|
8
|
-
*
|
8
|
+
*
|
9
9
|
* const toolbar = editor.addToolbar();
|
10
10
|
* toolbar.addActionButton('Save', () => {
|
11
11
|
* const saveData = editor.toSVG().outerHTML;
|
12
12
|
* // Do something with saveData...
|
13
13
|
* });
|
14
14
|
* ```
|
15
|
-
*
|
15
|
+
*
|
16
16
|
* @packageDocumentation
|
17
17
|
*/
|
18
18
|
|
@@ -38,6 +38,9 @@ import Rect2 from './math/Rect2';
|
|
38
38
|
import { EditorLocalization } from './localization';
|
39
39
|
import getLocalizationTable from './localizations/getLocalizationTable';
|
40
40
|
|
41
|
+
type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
|
42
|
+
type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
|
43
|
+
|
41
44
|
export interface EditorSettings {
|
42
45
|
/** Defaults to `RenderingMode.CanvasRenderer` */
|
43
46
|
renderingMode: RenderingMode,
|
@@ -67,14 +70,14 @@ export class Editor {
|
|
67
70
|
|
68
71
|
/**
|
69
72
|
* Handles undo/redo.
|
70
|
-
*
|
73
|
+
*
|
71
74
|
* @example
|
72
75
|
* ```
|
73
76
|
* const editor = new Editor(document.body);
|
74
|
-
*
|
77
|
+
*
|
75
78
|
* // Do something undoable.
|
76
79
|
* // ...
|
77
|
-
*
|
80
|
+
*
|
78
81
|
* // Undo the last action
|
79
82
|
* editor.history.undo();
|
80
83
|
* ```
|
@@ -83,17 +86,17 @@ export class Editor {
|
|
83
86
|
|
84
87
|
/**
|
85
88
|
* Data structure for adding/removing/querying objects in the image.
|
86
|
-
*
|
89
|
+
*
|
87
90
|
* @example
|
88
91
|
* ```
|
89
92
|
* const editor = new Editor(document.body);
|
90
|
-
*
|
93
|
+
*
|
91
94
|
* // Create a path.
|
92
95
|
* const stroke = new Stroke([
|
93
96
|
* Path.fromString('M0,0 L30,30 z').toRenderable({ fill: Color4.black }),
|
94
97
|
* ]);
|
95
98
|
* const addElementCommand = editor.image.addElement(stroke);
|
96
|
-
*
|
99
|
+
*
|
97
100
|
* // Add the stroke to the editor
|
98
101
|
* editor.dispatch(addElementCommand);
|
99
102
|
* ```
|
@@ -118,6 +121,7 @@ export class Editor {
|
|
118
121
|
private loadingWarning: HTMLElement;
|
119
122
|
private accessibilityAnnounceArea: HTMLElement;
|
120
123
|
private accessibilityControlArea: HTMLTextAreaElement;
|
124
|
+
private eventListenerTargets: HTMLElement[] = [];
|
121
125
|
|
122
126
|
private settings: EditorSettings;
|
123
127
|
|
@@ -125,14 +129,14 @@ export class Editor {
|
|
125
129
|
* @example
|
126
130
|
* ```
|
127
131
|
* const container = document.body;
|
128
|
-
*
|
132
|
+
*
|
129
133
|
* // Create an editor
|
130
134
|
* const editor = new Editor(container, {
|
131
135
|
* // 2e-10 and 1e12 are the default values for minimum/maximum zoom.
|
132
136
|
* minZoom: 2e-10,
|
133
137
|
* maxZoom: 1e12,
|
134
138
|
* });
|
135
|
-
*
|
139
|
+
*
|
136
140
|
* // Add the default toolbar
|
137
141
|
* const toolbar = editor.addToolbar();
|
138
142
|
* toolbar.addActionButton({
|
@@ -223,7 +227,7 @@ export class Editor {
|
|
223
227
|
if (oldZoom <= this.settings.maxZoom && oldZoom >= this.settings.minZoom) {
|
224
228
|
resetTransform = evt.oldTransform;
|
225
229
|
}
|
226
|
-
|
230
|
+
|
227
231
|
this.viewport.resetTransform(resetTransform);
|
228
232
|
}
|
229
233
|
}
|
@@ -232,7 +236,7 @@ export class Editor {
|
|
232
236
|
|
233
237
|
/**
|
234
238
|
* @returns a reference to the editor's container.
|
235
|
-
*
|
239
|
+
*
|
236
240
|
* @example
|
237
241
|
* ```
|
238
242
|
* editor.getRootElement().style.height = '500px';
|
@@ -284,157 +288,292 @@ export class Editor {
|
|
284
288
|
}
|
285
289
|
|
286
290
|
private registerListeners() {
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
291
|
+
this.handlePointerEventsFrom(this.renderingRegion);
|
292
|
+
this.handleKeyEventsFrom(this.renderingRegion);
|
293
|
+
|
294
|
+
this.container.addEventListener('wheel', evt => {
|
295
|
+
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
296
|
+
|
297
|
+
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
298
|
+
// pinch-zooming.
|
299
|
+
if (!evt.ctrlKey) {
|
300
|
+
if (!this.settings.wheelEventsEnabled) {
|
301
|
+
return;
|
302
|
+
} else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
|
303
|
+
const focusedChild = this.container.querySelector(':focus');
|
304
|
+
|
305
|
+
if (!focusedChild) {
|
306
|
+
return;
|
307
|
+
}
|
296
308
|
}
|
297
309
|
}
|
298
|
-
return res;
|
299
|
-
};
|
300
310
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
311
|
+
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
312
|
+
delta = delta.times(15);
|
313
|
+
} else if (evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
314
|
+
delta = delta.times(100);
|
315
|
+
}
|
316
|
+
|
317
|
+
if (evt.ctrlKey) {
|
318
|
+
delta = Vec3.of(0, 0, evt.deltaY);
|
319
|
+
}
|
320
|
+
|
321
|
+
// Ensure that `pos` is relative to `this.container`
|
322
|
+
const bbox = this.container.getBoundingClientRect();
|
323
|
+
const pos = Vec2.of(evt.clientX, evt.clientY).minus(Vec2.of(bbox.left, bbox.top));
|
324
|
+
|
325
|
+
if (this.toolController.dispatchInputEvent({
|
326
|
+
kind: InputEvtType.WheelEvt,
|
327
|
+
delta,
|
328
|
+
screenPos: pos,
|
329
|
+
})) {
|
330
|
+
evt.preventDefault();
|
331
|
+
return true;
|
332
|
+
}
|
333
|
+
return false;
|
307
334
|
});
|
308
335
|
|
309
|
-
this.
|
310
|
-
|
311
|
-
|
336
|
+
this.notifier.on(EditorEventType.DisplayResized, _event => {
|
337
|
+
this.viewport.updateScreenSize(
|
338
|
+
Vec2.of(this.display.width, this.display.height)
|
339
|
+
);
|
340
|
+
});
|
312
341
|
|
313
|
-
|
342
|
+
window.addEventListener('resize', () => {
|
343
|
+
this.notifier.dispatch(EditorEventType.DisplayResized, {
|
344
|
+
kind: EditorEventType.DisplayResized,
|
345
|
+
newSize: Vec2.of(
|
346
|
+
this.display.width,
|
347
|
+
this.display.height
|
348
|
+
),
|
349
|
+
});
|
350
|
+
this.queueRerender();
|
351
|
+
});
|
352
|
+
|
353
|
+
this.accessibilityControlArea.addEventListener('input', () => {
|
354
|
+
this.accessibilityControlArea.value = '';
|
355
|
+
});
|
356
|
+
|
357
|
+
document.addEventListener('copy', evt => {
|
358
|
+
if (!this.isEventSink(document.querySelector(':focus'))) {
|
359
|
+
return;
|
360
|
+
}
|
361
|
+
|
362
|
+
const clipboardData = evt.clipboardData;
|
363
|
+
|
364
|
+
if (this.toolController.dispatchInputEvent({
|
365
|
+
kind: InputEvtType.CopyEvent,
|
366
|
+
setData: (mime, data) => {
|
367
|
+
clipboardData?.setData(mime, data);
|
368
|
+
},
|
369
|
+
})) {
|
370
|
+
evt.preventDefault();
|
371
|
+
}
|
372
|
+
});
|
373
|
+
|
374
|
+
document.addEventListener('paste', evt => {
|
375
|
+
this.handlePaste(evt);
|
376
|
+
});
|
377
|
+
}
|
378
|
+
|
379
|
+
private pointers: Record<number, Pointer> = {};
|
380
|
+
private getPointerList() {
|
381
|
+
const nowTime = (new Date()).getTime();
|
382
|
+
|
383
|
+
const res: Pointer[] = [];
|
384
|
+
for (const id in this.pointers) {
|
385
|
+
const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
|
386
|
+
if (this.pointers[id] && (nowTime - this.pointers[id].timeStamp) < maxUnupdatedTime) {
|
387
|
+
res.push(this.pointers[id]);
|
388
|
+
}
|
389
|
+
}
|
390
|
+
return res;
|
391
|
+
}
|
392
|
+
|
393
|
+
/**
|
394
|
+
* Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
|
395
|
+
* as the content of the editor.
|
396
|
+
*/
|
397
|
+
public handleHTMLPointerEvent(eventType: 'pointerdown'|'pointermove'|'pointerup'|'pointercancel', evt: PointerEvent): boolean {
|
398
|
+
const eventsRelativeTo = this.renderingRegion;
|
399
|
+
const eventTarget = (evt.target as HTMLElement|null) ?? this.renderingRegion;
|
400
|
+
|
401
|
+
if (eventType === 'pointerdown') {
|
402
|
+
const pointer = Pointer.ofEvent(evt, true, this.viewport, eventsRelativeTo);
|
403
|
+
this.pointers[pointer.id] = pointer;
|
404
|
+
|
405
|
+
eventTarget.setPointerCapture(pointer.id);
|
314
406
|
const event: PointerEvt = {
|
315
407
|
kind: InputEvtType.PointerDownEvt,
|
316
408
|
current: pointer,
|
317
|
-
allPointers: getPointerList(),
|
409
|
+
allPointers: this.getPointerList(),
|
318
410
|
};
|
319
411
|
this.toolController.dispatchInputEvent(event);
|
320
412
|
|
321
413
|
return true;
|
322
|
-
}
|
323
|
-
|
324
|
-
this.renderingRegion.addEventListener('pointermove', evt => {
|
414
|
+
}
|
415
|
+
else if (eventType === 'pointermove') {
|
325
416
|
const pointer = Pointer.ofEvent(
|
326
|
-
evt, pointers[evt.pointerId]?.down ?? false, this.viewport
|
417
|
+
evt, this.pointers[evt.pointerId]?.down ?? false, this.viewport, eventsRelativeTo
|
327
418
|
);
|
328
419
|
if (pointer.down) {
|
329
|
-
const prevData = pointers[pointer.id];
|
420
|
+
const prevData = this.pointers[pointer.id];
|
330
421
|
|
331
422
|
if (prevData) {
|
332
423
|
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
|
333
424
|
|
334
425
|
// If the pointer moved less than two pixels, don't send a new event.
|
335
426
|
if (distanceMoved < 2) {
|
336
|
-
return;
|
427
|
+
return false;
|
337
428
|
}
|
338
429
|
}
|
339
430
|
|
340
|
-
pointers[pointer.id] = pointer;
|
431
|
+
this.pointers[pointer.id] = pointer;
|
341
432
|
if (this.toolController.dispatchInputEvent({
|
342
433
|
kind: InputEvtType.PointerMoveEvt,
|
343
434
|
current: pointer,
|
344
|
-
allPointers: getPointerList(),
|
435
|
+
allPointers: this.getPointerList(),
|
345
436
|
})) {
|
346
437
|
evt.preventDefault();
|
347
438
|
}
|
348
439
|
}
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
const pointer = Pointer.ofEvent(evt, false, this.viewport);
|
353
|
-
if (!pointers[pointer.id]) {
|
354
|
-
return;
|
440
|
+
return true;
|
441
|
+
}
|
442
|
+
else if (eventType === 'pointercancel' || eventType === 'pointerup') {
|
443
|
+
const pointer = Pointer.ofEvent(evt, false, this.viewport, eventsRelativeTo);
|
444
|
+
if (!this.pointers[pointer.id]) {
|
445
|
+
return false;
|
355
446
|
}
|
356
447
|
|
357
|
-
pointers[pointer.id] = pointer;
|
358
|
-
|
448
|
+
this.pointers[pointer.id] = pointer;
|
449
|
+
eventTarget.releasePointerCapture(pointer.id);
|
359
450
|
if (this.toolController.dispatchInputEvent({
|
360
451
|
kind: InputEvtType.PointerUpEvt,
|
361
452
|
current: pointer,
|
362
|
-
allPointers: getPointerList(),
|
453
|
+
allPointers: this.getPointerList(),
|
363
454
|
})) {
|
364
455
|
evt.preventDefault();
|
365
456
|
}
|
366
|
-
delete pointers[pointer.id];
|
367
|
-
|
457
|
+
delete this.pointers[pointer.id];
|
458
|
+
return true;
|
459
|
+
}
|
368
460
|
|
369
|
-
|
370
|
-
|
371
|
-
});
|
461
|
+
return eventType;
|
462
|
+
}
|
372
463
|
|
373
|
-
|
374
|
-
|
375
|
-
|
464
|
+
private isEventSink(evtTarget: Element|EventTarget|null) {
|
465
|
+
let currentElem: Element|null = evtTarget as Element|null;
|
466
|
+
while (currentElem !== null) {
|
467
|
+
for (const elem of this.eventListenerTargets) {
|
468
|
+
if (elem === currentElem) {
|
469
|
+
return true;
|
470
|
+
}
|
471
|
+
}
|
376
472
|
|
377
|
-
|
473
|
+
currentElem = (currentElem as Element).parentElement;
|
474
|
+
}
|
475
|
+
return false;
|
476
|
+
}
|
378
477
|
|
379
|
-
|
380
|
-
|
478
|
+
private async handlePaste(evt: DragEvent|ClipboardEvent) {
|
479
|
+
const target = document.querySelector(':focus') ?? evt.target;
|
480
|
+
if (!this.isEventSink(target)) {
|
481
|
+
return;
|
482
|
+
}
|
381
483
|
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
484
|
+
const clipboardData: DataTransfer = (evt as any).dataTransfer ?? (evt as any).clipboardData;
|
485
|
+
if (!clipboardData) {
|
486
|
+
return;
|
487
|
+
}
|
488
|
+
|
489
|
+
// Handle SVG files (prefer to PNG/JPEG)
|
490
|
+
for (const file of clipboardData.files) {
|
491
|
+
if (file.type.toLowerCase() === 'image/svg+xml') {
|
492
|
+
const text = await file.text();
|
493
|
+
if (this.toolController.dispatchInputEvent({
|
494
|
+
kind: InputEvtType.PasteEvent,
|
495
|
+
mime: file.type,
|
496
|
+
data: text,
|
497
|
+
})) {
|
498
|
+
evt.preventDefault();
|
386
499
|
return;
|
387
|
-
}
|
388
|
-
|
500
|
+
}
|
501
|
+
}
|
502
|
+
}
|
389
503
|
|
390
|
-
|
504
|
+
// Handle image files.
|
505
|
+
for (const file of clipboardData.files) {
|
506
|
+
const fileType = file.type.toLowerCase();
|
507
|
+
if (fileType === 'image/png' || fileType === 'image/jpg') {
|
508
|
+
const reader = new FileReader();
|
509
|
+
|
510
|
+
this.showLoadingWarning(0);
|
511
|
+
try {
|
512
|
+
const data = await new Promise((resolve: (result: string|null)=>void, reject) => {
|
513
|
+
reader.onload = () => resolve(reader.result as string|null);
|
514
|
+
reader.onerror = reject;
|
515
|
+
reader.onabort = reject;
|
516
|
+
reader.onprogress = (evt) => {
|
517
|
+
this.showLoadingWarning(evt.loaded / evt.total);
|
518
|
+
};
|
519
|
+
|
520
|
+
reader.readAsDataURL(file);
|
521
|
+
});
|
522
|
+
if (data && this.toolController.dispatchInputEvent({
|
523
|
+
kind: InputEvtType.PasteEvent,
|
524
|
+
mime: fileType,
|
525
|
+
data: data,
|
526
|
+
})) {
|
527
|
+
evt.preventDefault();
|
528
|
+
this.hideLoadingWarning();
|
391
529
|
return;
|
392
530
|
}
|
531
|
+
} catch (e) {
|
532
|
+
console.error('Error reading image:', e);
|
393
533
|
}
|
534
|
+
this.hideLoadingWarning();
|
394
535
|
}
|
536
|
+
}
|
395
537
|
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
538
|
+
// Supported MIMEs for text data, in order of preference
|
539
|
+
const supportedMIMEs = [
|
540
|
+
'image/svg+xml',
|
541
|
+
'text/plain',
|
542
|
+
];
|
401
543
|
|
402
|
-
|
403
|
-
|
404
|
-
}
|
544
|
+
for (const mime of supportedMIMEs) {
|
545
|
+
const data = clipboardData.getData(mime);
|
405
546
|
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
screenPos: pos,
|
547
|
+
if (data && this.toolController.dispatchInputEvent({
|
548
|
+
kind: InputEvtType.PasteEvent,
|
549
|
+
mime,
|
550
|
+
data,
|
411
551
|
})) {
|
412
552
|
evt.preventDefault();
|
413
|
-
return
|
553
|
+
return;
|
414
554
|
}
|
415
|
-
|
416
|
-
|
555
|
+
}
|
556
|
+
}
|
417
557
|
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
558
|
+
public handlePointerEventsFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter) {
|
559
|
+
// May be required to prevent text selection on iOS/Safari:
|
560
|
+
// See https://stackoverflow.com/a/70992717/17055750
|
561
|
+
elem.addEventListener('touchstart', evt => evt.preventDefault());
|
562
|
+
elem.addEventListener('contextmenu', evt => {
|
563
|
+
// Don't show a context menu
|
564
|
+
evt.preventDefault();
|
422
565
|
});
|
423
566
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
),
|
431
|
-
});
|
432
|
-
this.queueRerender();
|
433
|
-
});
|
567
|
+
const eventNames: HTMLPointerEventType[] = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'];
|
568
|
+
for (const eventName of eventNames) {
|
569
|
+
elem.addEventListener(eventName, evt => {
|
570
|
+
if (filter && !filter(eventName, evt)) {
|
571
|
+
return true;
|
572
|
+
}
|
434
573
|
|
435
|
-
|
436
|
-
|
437
|
-
}
|
574
|
+
return this.handleHTMLPointerEvent(eventName, evt);
|
575
|
+
});
|
576
|
+
}
|
438
577
|
}
|
439
578
|
|
440
579
|
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
@@ -451,7 +590,7 @@ export class Editor {
|
|
451
590
|
evt.preventDefault();
|
452
591
|
} else if (evt.key === 'Escape') {
|
453
592
|
this.renderingRegion.blur();
|
454
|
-
}
|
593
|
+
}
|
455
594
|
});
|
456
595
|
|
457
596
|
elem.addEventListener('keyup', evt => {
|
@@ -463,6 +602,18 @@ export class Editor {
|
|
463
602
|
evt.preventDefault();
|
464
603
|
}
|
465
604
|
});
|
605
|
+
|
606
|
+
// Allow drop.
|
607
|
+
elem.ondragover = evt => {
|
608
|
+
evt.preventDefault();
|
609
|
+
};
|
610
|
+
|
611
|
+
elem.ondrop = evt => {
|
612
|
+
evt.preventDefault();
|
613
|
+
this.handlePaste(evt);
|
614
|
+
};
|
615
|
+
|
616
|
+
this.eventListenerTargets.push(elem);
|
466
617
|
}
|
467
618
|
|
468
619
|
/** `apply` a command. `command` will be announced for accessibility. */
|
@@ -481,11 +632,11 @@ export class Editor {
|
|
481
632
|
* Dispatches a command without announcing it. By default, does not add to history.
|
482
633
|
* Use this to show finalized commands that don't need to have `announceForAccessibility`
|
483
634
|
* called.
|
484
|
-
*
|
635
|
+
*
|
485
636
|
* Prefer `command.apply(editor)` for incomplete commands. `dispatchNoAnnounce` may allow
|
486
637
|
* clients to listen for the application of commands (e.g. `SerializableCommand`s so they can
|
487
638
|
* be sent across the network), while `apply` does not.
|
488
|
-
*
|
639
|
+
*
|
489
640
|
* @example
|
490
641
|
* ```
|
491
642
|
* const addToHistory = false;
|
@@ -509,6 +660,7 @@ export class Editor {
|
|
509
660
|
public async asyncApplyOrUnapplyCommands(
|
510
661
|
commands: Command[], apply: boolean, updateChunkSize: number
|
511
662
|
) {
|
663
|
+
console.assert(updateChunkSize > 0);
|
512
664
|
this.display.setDraftMode(true);
|
513
665
|
for (let i = 0; i < commands.length; i += updateChunkSize) {
|
514
666
|
this.showLoadingWarning(i / commands.length);
|
@@ -626,6 +778,20 @@ export class Editor {
|
|
626
778
|
return styleSheet;
|
627
779
|
}
|
628
780
|
|
781
|
+
// Dispatch a keyboard event to the currently selected tool.
|
782
|
+
// Intended for unit testing
|
783
|
+
public sendKeyboardEvent(
|
784
|
+
eventType: InputEvtType.KeyPressEvent|InputEvtType.KeyUpEvent,
|
785
|
+
key: string,
|
786
|
+
ctrlKey: boolean = false
|
787
|
+
) {
|
788
|
+
this.toolController.dispatchInputEvent({
|
789
|
+
kind: eventType,
|
790
|
+
key,
|
791
|
+
ctrlKey
|
792
|
+
});
|
793
|
+
}
|
794
|
+
|
629
795
|
// Dispatch a pen event to the currently selected tool.
|
630
796
|
// Intended primarially for unit tests.
|
631
797
|
public sendPenEvent(
|
@@ -735,12 +901,12 @@ export class Editor {
|
|
735
901
|
|
736
902
|
/**
|
737
903
|
* Alias for loadFrom(SVGLoader.fromString).
|
738
|
-
*
|
904
|
+
*
|
739
905
|
* This is particularly useful when accessing a bundled version of the editor,
|
740
906
|
* where `SVGLoader.fromString` is unavailable.
|
741
907
|
*/
|
742
|
-
public async loadFromSVG(svgData: string) {
|
743
|
-
const loader = SVGLoader.fromString(svgData);
|
908
|
+
public async loadFromSVG(svgData: string, sanitize: boolean = false) {
|
909
|
+
const loader = SVGLoader.fromString(svgData, sanitize);
|
744
910
|
await this.loadFrom(loader);
|
745
911
|
}
|
746
912
|
}
|
package/src/EditorImage.ts
CHANGED
@@ -81,6 +81,8 @@ export default class EditorImage {
|
|
81
81
|
|
82
82
|
// A Command that can access private [EditorImage] functionality
|
83
83
|
private static AddElementCommand = class extends SerializableCommand {
|
84
|
+
private serializedElem: any;
|
85
|
+
|
84
86
|
// If [applyByFlattening], then the rendered content of this element
|
85
87
|
// is present on the display's wet ink canvas. As such, no re-render is necessary
|
86
88
|
// the first time this command is applied (the surfaces are joined instead).
|
@@ -90,6 +92,10 @@ export default class EditorImage {
|
|
90
92
|
) {
|
91
93
|
super('add-element');
|
92
94
|
|
95
|
+
// Store the element's serialization --- .serializeToJSON may be called on this
|
96
|
+
// even when this is not at the top of the undo/redo stack.
|
97
|
+
this.serializedElem = element.serialize();
|
98
|
+
|
93
99
|
if (isNaN(element.getBBox().area)) {
|
94
100
|
throw new Error('Elements in the image cannot have NaN bounding boxes');
|
95
101
|
}
|
@@ -118,7 +124,7 @@ export default class EditorImage {
|
|
118
124
|
|
119
125
|
protected serializeToJSON() {
|
120
126
|
return {
|
121
|
-
elemData: this.
|
127
|
+
elemData: this.serializedElem,
|
122
128
|
};
|
123
129
|
}
|
124
130
|
|
package/src/Pointer.ts
CHANGED
@@ -36,9 +36,14 @@ export default class Pointer {
|
|
36
36
|
) {
|
37
37
|
}
|
38
38
|
|
39
|
-
// Creates a Pointer from a DOM event.
|
40
|
-
|
41
|
-
|
39
|
+
// Creates a Pointer from a DOM event. If `relativeTo` is given, (0, 0) in screen coordinates is
|
40
|
+
// considered the top left of `relativeTo`.
|
41
|
+
public static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer {
|
42
|
+
let screenPos = Vec2.of(evt.clientX, evt.clientY);
|
43
|
+
if (relativeTo) {
|
44
|
+
const bbox = relativeTo.getBoundingClientRect();
|
45
|
+
screenPos = screenPos.minus(Vec2.of(bbox.left, bbox.top));
|
46
|
+
}
|
42
47
|
|
43
48
|
const pointerTypeToDevice: Record<string, PointerDevice> = {
|
44
49
|
'mouse': PointerDevice.PrimaryButtonMouse,
|