js-draw 0.1.11 → 0.2.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/.eslintrc.js +1 -0
- package/.firebaserc +5 -0
- package/.github/workflows/firebase-hosting-merge.yml +25 -0
- package/.github/workflows/firebase-hosting-pull-request.yml +22 -0
- package/.github/workflows/github-pages.yml +52 -0
- package/CHANGELOG.md +13 -0
- package/README.md +11 -6
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +19 -0
- package/dist/src/Color4.js +24 -3
- package/dist/src/Editor.d.ts +133 -4
- package/dist/src/Editor.js +124 -27
- package/dist/src/EditorImage.d.ts +8 -3
- package/dist/src/EditorImage.js +42 -26
- package/dist/src/EventDispatcher.d.ts +18 -0
- package/dist/src/EventDispatcher.js +19 -4
- package/dist/src/Pointer.d.ts +1 -1
- package/dist/src/Pointer.js +4 -3
- package/dist/src/SVGLoader.d.ts +1 -1
- package/dist/src/SVGLoader.js +14 -6
- package/dist/src/UndoRedoHistory.js +15 -2
- package/dist/src/Viewport.d.ts +8 -25
- package/dist/src/Viewport.js +18 -10
- package/dist/src/bundle/bundled.d.ts +1 -2
- package/dist/src/bundle/bundled.js +1 -2
- package/dist/src/commands/Command.d.ts +2 -2
- package/dist/src/commands/Command.js +4 -4
- package/dist/src/commands/Duplicate.d.ts +2 -2
- package/dist/src/commands/Duplicate.js +4 -5
- package/dist/src/commands/Erase.d.ts +2 -2
- package/dist/src/commands/Erase.js +7 -6
- package/dist/src/commands/SerializableCommand.d.ts +4 -5
- package/dist/src/commands/SerializableCommand.js +12 -4
- package/dist/src/commands/invertCommand.d.ts +4 -0
- package/dist/src/commands/invertCommand.js +44 -0
- package/dist/src/commands/lib.d.ts +6 -0
- package/dist/src/commands/lib.js +6 -0
- package/dist/src/commands/localization.d.ts +2 -1
- package/dist/src/commands/localization.js +1 -0
- package/dist/src/components/AbstractComponent.d.ts +16 -11
- package/dist/src/components/AbstractComponent.js +28 -17
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +4 -4
- package/dist/src/components/SVGGlobalAttributesObject.js +8 -2
- package/dist/src/components/Stroke.d.ts +16 -6
- package/dist/src/components/Stroke.js +12 -9
- package/dist/src/components/Text.d.ts +5 -5
- package/dist/src/components/Text.js +9 -9
- package/dist/src/components/UnknownSVGObject.d.ts +4 -4
- package/dist/src/components/UnknownSVGObject.js +7 -2
- package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
- package/dist/src/components/builders/ArrowBuilder.js +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +8 -3
- package/dist/src/components/builders/FreehandLineBuilder.js +142 -71
- package/dist/src/components/builders/LineBuilder.d.ts +1 -1
- package/dist/src/components/builders/LineBuilder.js +1 -1
- package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
- package/dist/src/components/builders/RectangleBuilder.js +3 -3
- package/dist/src/components/builders/types.d.ts +1 -1
- package/dist/src/components/lib.d.ts +4 -0
- package/dist/src/components/lib.js +4 -0
- package/dist/src/lib.d.ts +25 -0
- package/dist/src/lib.js +25 -0
- package/dist/src/localization.d.ts +1 -0
- package/dist/src/localization.js +5 -1
- package/dist/src/localizations/es.js +1 -1
- package/dist/src/{geometry → math}/LineSegment2.d.ts +0 -0
- package/dist/src/{geometry → math}/LineSegment2.js +0 -0
- package/dist/src/math/Mat33.d.ts +78 -0
- package/dist/src/{geometry → math}/Mat33.js +48 -20
- package/dist/src/{geometry → math}/Path.d.ts +2 -1
- package/dist/src/{geometry → math}/Path.js +59 -52
- package/dist/src/{geometry → math}/Rect2.d.ts +2 -2
- package/dist/src/{geometry → math}/Rect2.js +0 -0
- package/dist/src/{geometry → math}/Vec2.d.ts +0 -0
- package/dist/src/{geometry → math}/Vec2.js +0 -0
- package/dist/src/math/Vec3.d.ts +96 -0
- package/dist/src/{geometry → math}/Vec3.js +63 -15
- package/dist/src/math/lib.d.ts +7 -0
- package/dist/src/math/lib.js +7 -0
- package/dist/src/math/rounding.d.ts +3 -0
- package/dist/src/math/rounding.js +121 -0
- package/dist/src/rendering/Display.d.ts +47 -1
- package/dist/src/rendering/Display.js +60 -15
- package/dist/src/rendering/caching/CacheRecord.d.ts +3 -2
- package/dist/src/rendering/caching/CacheRecord.js +4 -1
- package/dist/src/rendering/caching/CacheRecordManager.d.ts +5 -4
- package/dist/src/rendering/caching/CacheRecordManager.js +16 -4
- package/dist/src/rendering/caching/RenderingCache.d.ts +2 -3
- package/dist/src/rendering/caching/RenderingCache.js +10 -11
- package/dist/src/rendering/caching/RenderingCacheNode.d.ts +2 -1
- package/dist/src/rendering/caching/RenderingCacheNode.js +18 -7
- package/dist/src/rendering/caching/testUtils.js +1 -1
- package/dist/src/rendering/caching/types.d.ts +2 -4
- package/dist/src/rendering/localization.d.ts +2 -0
- package/dist/src/rendering/localization.js +2 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +4 -4
- package/dist/src/rendering/renderers/AbstractRenderer.js +2 -2
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +4 -4
- package/dist/src/rendering/renderers/CanvasRenderer.js +2 -2
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +4 -4
- package/dist/src/rendering/renderers/DummyRenderer.js +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -3
- package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +5 -3
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +13 -3
- package/dist/src/toolbar/HTMLToolbar.js +1 -0
- package/dist/src/toolbar/icons.d.ts +3 -0
- package/dist/src/toolbar/icons.js +142 -132
- package/dist/src/toolbar/localization.d.ts +2 -1
- package/dist/src/toolbar/localization.js +2 -1
- package/dist/src/toolbar/makeColorInput.js +3 -2
- package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +13 -0
- package/dist/src/toolbar/widgets/ActionButtonWidget.js +21 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +2 -0
- package/dist/src/toolbar/widgets/HandToolWidget.js +3 -3
- package/dist/src/toolbar/widgets/PenWidget.js +1 -0
- package/dist/src/toolbar/widgets/SelectionWidget.d.ts +0 -1
- package/dist/src/toolbar/widgets/SelectionWidget.js +23 -30
- package/dist/src/tools/Eraser.js +1 -1
- package/dist/src/tools/PanZoom.d.ts +1 -1
- package/dist/src/tools/PanZoom.js +24 -14
- package/dist/src/tools/Pen.d.ts +1 -2
- package/dist/src/tools/Pen.js +8 -1
- package/dist/src/tools/PipetteTool.js +1 -0
- package/dist/src/tools/SelectionTool.d.ts +3 -3
- package/dist/src/tools/SelectionTool.js +51 -28
- package/dist/src/tools/TextTool.js +1 -1
- package/dist/src/types.d.ts +21 -10
- package/dist/src/types.js +7 -5
- package/firebase.json +16 -0
- package/package.json +118 -101
- package/src/Color4.ts +23 -2
- package/src/Editor.ts +181 -37
- package/src/EditorImage.test.ts +2 -4
- package/src/EditorImage.ts +46 -28
- package/src/EventDispatcher.ts +21 -6
- package/src/Pointer.ts +4 -3
- package/src/SVGLoader.ts +14 -6
- package/src/UndoRedoHistory.ts +18 -2
- package/src/Viewport.ts +23 -18
- package/src/bundle/bundled.ts +1 -2
- package/src/commands/Command.ts +5 -5
- package/src/commands/Duplicate.ts +4 -5
- package/src/commands/Erase.ts +7 -6
- package/src/commands/SerializableCommand.ts +17 -9
- package/src/commands/invertCommand.ts +51 -0
- package/src/commands/lib.ts +14 -0
- package/src/commands/localization.ts +3 -1
- package/src/components/AbstractComponent.ts +35 -24
- package/src/components/SVGGlobalAttributesObject.ts +11 -4
- package/src/components/Stroke.test.ts +4 -6
- package/src/components/Stroke.ts +15 -11
- package/src/components/Text.test.ts +2 -2
- package/src/components/Text.ts +9 -10
- package/src/components/UnknownSVGObject.ts +10 -4
- package/src/components/builders/ArrowBuilder.ts +2 -2
- package/src/components/builders/FreehandLineBuilder.ts +190 -80
- package/src/components/builders/LineBuilder.ts +2 -2
- package/src/components/builders/RectangleBuilder.ts +3 -3
- package/src/components/builders/types.ts +1 -1
- package/src/components/lib.ts +9 -0
- package/src/lib.ts +28 -0
- package/src/localization.ts +6 -0
- package/src/localizations/es.ts +2 -1
- package/src/{geometry → math}/LineSegment2.test.ts +0 -0
- package/src/{geometry → math}/LineSegment2.ts +0 -0
- package/src/{geometry → math}/Mat33.test.ts +0 -0
- package/src/{geometry → math}/Mat33.ts +48 -20
- package/src/{geometry → math}/Path.fromString.test.ts +0 -0
- package/src/{geometry → math}/Path.test.ts +0 -0
- package/src/{geometry → math}/Path.toString.test.ts +11 -2
- package/src/{geometry → math}/Path.ts +61 -58
- package/src/{geometry → math}/Rect2.test.ts +0 -0
- package/src/{geometry → math}/Rect2.ts +2 -2
- package/src/{geometry → math}/Vec2.test.ts +0 -0
- package/src/{geometry → math}/Vec2.ts +0 -0
- package/src/{geometry → math}/Vec3.test.ts +0 -0
- package/src/{geometry → math}/Vec3.ts +64 -16
- package/src/math/lib.ts +15 -0
- package/src/math/rounding.test.ts +40 -0
- package/src/math/rounding.ts +147 -0
- package/src/rendering/Display.ts +63 -15
- package/src/rendering/caching/CacheRecord.test.ts +3 -3
- package/src/rendering/caching/CacheRecord.ts +6 -2
- package/src/rendering/caching/CacheRecordManager.ts +34 -8
- package/src/rendering/caching/RenderingCache.test.ts +3 -3
- package/src/rendering/caching/RenderingCache.ts +11 -16
- package/src/rendering/caching/RenderingCacheNode.ts +23 -7
- package/src/rendering/caching/testUtils.ts +1 -1
- package/src/rendering/caching/types.ts +2 -7
- package/src/rendering/localization.ts +4 -0
- package/src/rendering/renderers/AbstractRenderer.ts +4 -4
- package/src/rendering/renderers/CanvasRenderer.ts +5 -5
- package/src/rendering/renderers/DummyRenderer.test.ts +2 -2
- package/src/rendering/renderers/DummyRenderer.ts +4 -4
- package/src/rendering/renderers/SVGRenderer.ts +10 -4
- package/src/rendering/renderers/TextOnlyRenderer.ts +17 -6
- package/src/toolbar/HTMLToolbar.ts +1 -0
- package/src/toolbar/icons.ts +157 -137
- package/src/toolbar/localization.ts +4 -2
- package/src/toolbar/makeColorInput.ts +3 -2
- package/src/toolbar/toolbar.css +1 -1
- package/src/toolbar/widgets/ActionButtonWidget.ts +31 -0
- package/src/toolbar/widgets/BaseWidget.ts +2 -0
- package/src/toolbar/widgets/HandToolWidget.ts +3 -3
- package/src/toolbar/widgets/PenWidget.ts +2 -0
- package/src/toolbar/widgets/SelectionWidget.ts +46 -41
- package/src/tools/Eraser.ts +2 -2
- package/src/tools/PanZoom.ts +28 -17
- package/src/tools/Pen.ts +11 -2
- package/src/tools/PipetteTool.ts +2 -0
- package/src/tools/SelectionTool.test.ts +2 -4
- package/src/tools/SelectionTool.ts +52 -24
- package/src/tools/TextTool.ts +2 -2
- package/src/tools/UndoRedoShortcut.test.ts +1 -1
- package/src/types.ts +23 -7
- package/tsconfig.json +4 -1
- package/typedoc.json +20 -0
- package/dist/src/geometry/Mat33.d.ts +0 -32
- package/dist/src/geometry/Vec3.d.ts +0 -34
package/src/Editor.ts
CHANGED
@@ -1,3 +1,21 @@
|
|
1
|
+
/**
|
2
|
+
* The main entrypoint for the full editor.
|
3
|
+
*
|
4
|
+
* @example
|
5
|
+
* To create an editor with a toolbar,
|
6
|
+
* ```
|
7
|
+
* const editor = new Editor(document.body);
|
8
|
+
*
|
9
|
+
* const toolbar = editor.addToolbar();
|
10
|
+
* toolbar.addActionButton('Save', () => {
|
11
|
+
* const saveData = editor.toSVG().outerHTML;
|
12
|
+
* // Do something with saveData...
|
13
|
+
* });
|
14
|
+
* ```
|
15
|
+
*
|
16
|
+
* @packageDocumentation
|
17
|
+
*/
|
18
|
+
|
1
19
|
|
2
20
|
import EditorImage from './EditorImage';
|
3
21
|
import ToolController from './tools/ToolController';
|
@@ -6,8 +24,8 @@ import Command from './commands/Command';
|
|
6
24
|
import UndoRedoHistory from './UndoRedoHistory';
|
7
25
|
import Viewport from './Viewport';
|
8
26
|
import EventDispatcher from './EventDispatcher';
|
9
|
-
import { Point2, Vec2 } from './
|
10
|
-
import Vec3 from './
|
27
|
+
import { Point2, Vec2 } from './math/Vec2';
|
28
|
+
import Vec3 from './math/Vec3';
|
11
29
|
import HTMLToolbar from './toolbar/HTMLToolbar';
|
12
30
|
import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
|
13
31
|
import Display, { RenderingMode } from './rendering/Display';
|
@@ -15,49 +33,117 @@ import SVGRenderer from './rendering/renderers/SVGRenderer';
|
|
15
33
|
import Color4 from './Color4';
|
16
34
|
import SVGLoader from './SVGLoader';
|
17
35
|
import Pointer from './Pointer';
|
18
|
-
import Mat33 from './
|
19
|
-
import Rect2 from './
|
36
|
+
import Mat33 from './math/Mat33';
|
37
|
+
import Rect2 from './math/Rect2';
|
20
38
|
import { EditorLocalization } from './localization';
|
21
39
|
import getLocalizationTable from './localizations/getLocalizationTable';
|
22
40
|
|
23
41
|
export interface EditorSettings {
|
24
|
-
|
42
|
+
/** Defaults to `RenderingMode.CanvasRenderer` */
|
25
43
|
renderingMode: RenderingMode,
|
26
44
|
|
27
|
-
|
45
|
+
/** Uses a default English localization if a translation is not given. */
|
28
46
|
localization: Partial<EditorLocalization>,
|
29
47
|
|
30
|
-
|
31
|
-
|
32
|
-
|
48
|
+
/**
|
49
|
+
* `true` if touchpad/mousewheel scrolling should scroll the editor instead of the document.
|
50
|
+
* This does not include pinch-zoom events.
|
51
|
+
* Defaults to true.
|
52
|
+
*/
|
33
53
|
wheelEventsEnabled: boolean|'only-if-focused';
|
34
54
|
|
55
|
+
/** Minimum zoom fraction (e.g. 0.5 → 50% zoom). */
|
35
56
|
minZoom: number,
|
36
57
|
maxZoom: number,
|
37
58
|
}
|
38
59
|
|
60
|
+
// { @inheritDoc Editor! }
|
39
61
|
export class Editor {
|
40
62
|
// Wrapper around the viewport and toolbar
|
41
63
|
private container: HTMLElement;
|
42
64
|
private renderingRegion: HTMLElement;
|
43
65
|
|
44
|
-
public history: UndoRedoHistory;
|
45
66
|
public display: Display;
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Handles undo/redo.
|
70
|
+
*
|
71
|
+
* @example
|
72
|
+
* ```
|
73
|
+
* const editor = new Editor(document.body);
|
74
|
+
*
|
75
|
+
* // Do something undoable.
|
76
|
+
* // ...
|
77
|
+
*
|
78
|
+
* // Undo the last action
|
79
|
+
* editor.history.undo();
|
80
|
+
* ```
|
81
|
+
*/
|
82
|
+
public history: UndoRedoHistory;
|
83
|
+
|
84
|
+
/**
|
85
|
+
* Data structure for adding/removing/querying objects in the image.
|
86
|
+
*
|
87
|
+
* @example
|
88
|
+
* ```
|
89
|
+
* const editor = new Editor(document.body);
|
90
|
+
*
|
91
|
+
* // Create a path.
|
92
|
+
* const stroke = new Stroke([
|
93
|
+
* Path.fromString('M0,0 L30,30 z').toRenderable({ fill: Color4.black }),
|
94
|
+
* ]);
|
95
|
+
* const addElementCommand = editor.image.addElement(stroke);
|
96
|
+
*
|
97
|
+
* // Add the stroke to the editor
|
98
|
+
* editor.dispatch(addElementCommand);
|
99
|
+
* ```
|
100
|
+
*/
|
46
101
|
public image: EditorImage;
|
47
102
|
|
48
|
-
|
103
|
+
/** Viewport for the exported/imported image. */
|
49
104
|
private importExportViewport: Viewport;
|
105
|
+
|
106
|
+
/** @internal */
|
50
107
|
public localization: EditorLocalization;
|
51
108
|
|
52
109
|
public viewport: Viewport;
|
53
110
|
public toolController: ToolController;
|
111
|
+
|
112
|
+
/**
|
113
|
+
* Global event dispatcher/subscriber.
|
114
|
+
* @see {@link types.EditorEventType}
|
115
|
+
*/
|
54
116
|
public notifier: EditorNotifier;
|
55
117
|
|
56
118
|
private loadingWarning: HTMLElement;
|
57
119
|
private accessibilityAnnounceArea: HTMLElement;
|
120
|
+
private accessibilityControlArea: HTMLTextAreaElement;
|
58
121
|
|
59
122
|
private settings: EditorSettings;
|
60
123
|
|
124
|
+
/**
|
125
|
+
* @example
|
126
|
+
* ```
|
127
|
+
* const container = document.body;
|
128
|
+
*
|
129
|
+
* // Create an editor
|
130
|
+
* const editor = new Editor(container, {
|
131
|
+
* // 2e-10 and 1e12 are the default values for minimum/maximum zoom.
|
132
|
+
* minZoom: 2e-10,
|
133
|
+
* maxZoom: 1e12,
|
134
|
+
* });
|
135
|
+
*
|
136
|
+
* // Add the default toolbar
|
137
|
+
* const toolbar = editor.addToolbar();
|
138
|
+
* toolbar.addActionButton({
|
139
|
+
* label: 'Save'
|
140
|
+
* icon: createSaveIcon(),
|
141
|
+
* }, () => {
|
142
|
+
* const saveData = editor.toSVG().outerHTML;
|
143
|
+
* // Do something with saveData
|
144
|
+
* });
|
145
|
+
* ```
|
146
|
+
*/
|
61
147
|
public constructor(
|
62
148
|
parent: HTMLElement,
|
63
149
|
settings: Partial<EditorSettings> = {},
|
@@ -86,15 +172,23 @@ export class Editor {
|
|
86
172
|
this.loadingWarning.ariaLive = 'polite';
|
87
173
|
this.container.appendChild(this.loadingWarning);
|
88
174
|
|
175
|
+
this.accessibilityControlArea = document.createElement('textarea');
|
176
|
+
this.accessibilityControlArea.setAttribute('placeholder', this.localization.accessibilityInputInstructions);
|
177
|
+
this.accessibilityControlArea.style.opacity = '0';
|
178
|
+
this.accessibilityControlArea.style.width = '0';
|
179
|
+
this.accessibilityControlArea.style.height = '0';
|
180
|
+
this.accessibilityControlArea.style.position = 'absolute';
|
181
|
+
|
89
182
|
this.accessibilityAnnounceArea = document.createElement('div');
|
90
|
-
this.accessibilityAnnounceArea.
|
183
|
+
this.accessibilityAnnounceArea.setAttribute('aria-live', 'assertive');
|
91
184
|
this.accessibilityAnnounceArea.className = 'accessibilityAnnouncement';
|
92
185
|
this.container.appendChild(this.accessibilityAnnounceArea);
|
93
186
|
|
94
187
|
this.renderingRegion.style.touchAction = 'none';
|
95
188
|
this.renderingRegion.className = 'imageEditorRenderArea';
|
189
|
+
this.renderingRegion.appendChild(this.accessibilityControlArea);
|
96
190
|
this.renderingRegion.setAttribute('tabIndex', '0');
|
97
|
-
this.renderingRegion.
|
191
|
+
this.renderingRegion.setAttribute('alt', '');
|
98
192
|
|
99
193
|
this.notifier = new EventDispatcher();
|
100
194
|
this.importExportViewport = new Viewport(this.notifier);
|
@@ -136,14 +230,19 @@ export class Editor {
|
|
136
230
|
});
|
137
231
|
}
|
138
232
|
|
139
|
-
|
140
|
-
|
141
|
-
|
233
|
+
/**
|
234
|
+
* @returns a reference to the editor's container.
|
235
|
+
*
|
236
|
+
* @example
|
237
|
+
* ```
|
238
|
+
* editor.getRootElement().style.height = '500px';
|
239
|
+
* ```
|
240
|
+
*/
|
142
241
|
public getRootElement(): HTMLElement {
|
143
242
|
return this.container;
|
144
243
|
}
|
145
244
|
|
146
|
-
|
245
|
+
/** @param fractionLoaded - should be a number from 0 to 1, where 1 represents completely loaded. */
|
147
246
|
public showLoadingWarning(fractionLoaded: number) {
|
148
247
|
const loadingPercent = Math.round(fractionLoaded * 100);
|
149
248
|
this.loadingWarning.innerText = this.localization.loading(loadingPercent);
|
@@ -156,10 +255,23 @@ export class Editor {
|
|
156
255
|
this.announceForAccessibility(this.localization.doneLoading);
|
157
256
|
}
|
158
257
|
|
258
|
+
private previousAccessibilityAnnouncement: string = '';
|
259
|
+
|
260
|
+
// Announce `message` for screen readers. If `message` is the same as the previous
|
261
|
+
// message, it is re-announced.
|
159
262
|
public announceForAccessibility(message: string) {
|
263
|
+
// Force re-announcing an announcement if announced again.
|
264
|
+
if (message === this.previousAccessibilityAnnouncement) {
|
265
|
+
message = message + '. ';
|
266
|
+
}
|
160
267
|
this.accessibilityAnnounceArea.innerText = message;
|
268
|
+
this.previousAccessibilityAnnouncement = message;
|
161
269
|
}
|
162
270
|
|
271
|
+
/**
|
272
|
+
* Creates a toolbar. If `defaultLayout` is true, default buttons are used.
|
273
|
+
* @returns a reference to the toolbar.
|
274
|
+
*/
|
163
275
|
public addToolbar(defaultLayout: boolean = true): HTMLToolbar {
|
164
276
|
const toolbar = new HTMLToolbar(this, this.container, this.localization);
|
165
277
|
|
@@ -319,13 +431,19 @@ export class Editor {
|
|
319
431
|
});
|
320
432
|
this.queueRerender();
|
321
433
|
});
|
434
|
+
|
435
|
+
this.accessibilityControlArea.addEventListener('input', () => {
|
436
|
+
this.accessibilityControlArea.value = '';
|
437
|
+
});
|
322
438
|
}
|
323
439
|
|
324
|
-
|
325
|
-
// editor.
|
440
|
+
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
326
441
|
public handleKeyEventsFrom(elem: HTMLElement) {
|
327
442
|
elem.addEventListener('keydown', evt => {
|
328
|
-
if (
|
443
|
+
if (evt.key === 't' || evt.key === 'T') {
|
444
|
+
evt.preventDefault();
|
445
|
+
this.display.rerenderAsText();
|
446
|
+
} else if (this.toolController.dispatchInputEvent({
|
329
447
|
kind: InputEvtType.KeyPressEvent,
|
330
448
|
key: evt.key,
|
331
449
|
ctrlKey: evt.ctrlKey,
|
@@ -333,7 +451,7 @@ export class Editor {
|
|
333
451
|
evt.preventDefault();
|
334
452
|
} else if (evt.key === 'Escape') {
|
335
453
|
this.renderingRegion.blur();
|
336
|
-
}
|
454
|
+
}
|
337
455
|
});
|
338
456
|
|
339
457
|
elem.addEventListener('keyup', evt => {
|
@@ -347,7 +465,7 @@ export class Editor {
|
|
347
465
|
});
|
348
466
|
}
|
349
467
|
|
350
|
-
|
468
|
+
/** `apply` a command. `command` will be announced for accessibility. */
|
351
469
|
public dispatch(command: Command, addToHistory: boolean = true) {
|
352
470
|
if (addToHistory) {
|
353
471
|
// .push applies [command] to this
|
@@ -356,10 +474,24 @@ export class Editor {
|
|
356
474
|
command.apply(this);
|
357
475
|
}
|
358
476
|
|
359
|
-
this.announceForAccessibility(command.description(this.localization));
|
477
|
+
this.announceForAccessibility(command.description(this, this.localization));
|
360
478
|
}
|
361
479
|
|
362
|
-
|
480
|
+
/**
|
481
|
+
* Dispatches a command without announcing it. By default, does not add to history.
|
482
|
+
* Use this to show finalized commands that don't need to have `announceForAccessibility`
|
483
|
+
* called.
|
484
|
+
*
|
485
|
+
* Prefer `command.apply(editor)` for incomplete commands. `dispatchNoAnnounce` may allow
|
486
|
+
* clients to listen for the application of commands (e.g. `SerializableCommand`s so they can
|
487
|
+
* be sent across the network), while `apply` does not.
|
488
|
+
*
|
489
|
+
* @example
|
490
|
+
* ```
|
491
|
+
* const addToHistory = false;
|
492
|
+
* editor.dispatchNoAnnounce(editor.viewport.zoomTo(someRectangle), addToHistory);
|
493
|
+
* ```
|
494
|
+
*/
|
363
495
|
public dispatchNoAnnounce(command: Command, addToHistory: boolean = false) {
|
364
496
|
if (addToHistory) {
|
365
497
|
this.history.push(command);
|
@@ -368,11 +500,13 @@ export class Editor {
|
|
368
500
|
}
|
369
501
|
}
|
370
502
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
503
|
+
/**
|
504
|
+
* Apply a large transformation in chunks.
|
505
|
+
* If `apply` is `false`, the commands are unapplied.
|
506
|
+
* Triggers a re-render after each `updateChunkSize`-sized group of commands
|
507
|
+
* has been applied.
|
508
|
+
*/
|
509
|
+
public async asyncApplyOrUnapplyCommands(
|
376
510
|
commands: Command[], apply: boolean, updateChunkSize: number
|
377
511
|
) {
|
378
512
|
this.display.setDraftMode(true);
|
@@ -401,23 +535,27 @@ export class Editor {
|
|
401
535
|
this.hideLoadingWarning();
|
402
536
|
}
|
403
537
|
|
538
|
+
// @see {@link #asyncApplyOrUnapplyCommands }
|
404
539
|
public asyncApplyCommands(commands: Command[], chunkSize: number) {
|
405
540
|
return this.asyncApplyOrUnapplyCommands(commands, true, chunkSize);
|
406
541
|
}
|
407
542
|
|
543
|
+
// @see {@link #asyncApplyOrUnapplyCommands }
|
408
544
|
public asyncUnapplyCommands(commands: Command[], chunkSize: number) {
|
409
545
|
return this.asyncApplyOrUnapplyCommands(commands, false, chunkSize);
|
410
546
|
}
|
411
547
|
|
412
548
|
private announceUndoCallback = (command: Command) => {
|
413
|
-
this.announceForAccessibility(this.localization.undoAnnouncement(command.description(this.localization)));
|
549
|
+
this.announceForAccessibility(this.localization.undoAnnouncement(command.description(this, this.localization)));
|
414
550
|
};
|
415
551
|
|
416
552
|
private announceRedoCallback = (command: Command) => {
|
417
|
-
this.announceForAccessibility(this.localization.redoAnnouncement(command.description(this.localization)));
|
553
|
+
this.announceForAccessibility(this.localization.redoAnnouncement(command.description(this, this.localization)));
|
418
554
|
};
|
419
555
|
|
420
556
|
private rerenderQueued: boolean = false;
|
557
|
+
// Schedule a re-render for some time in the near future. Does not schedule an additional
|
558
|
+
// re-render if a re-render is already queued.
|
421
559
|
public queueRerender() {
|
422
560
|
if (!this.rerenderQueued) {
|
423
561
|
this.rerenderQueued = true;
|
@@ -464,11 +602,13 @@ export class Editor {
|
|
464
602
|
this.display.getWetInkRenderer().clear();
|
465
603
|
}
|
466
604
|
|
467
|
-
// Focuses the region used for text input
|
605
|
+
// Focuses the region used for text input/key commands.
|
468
606
|
public focus() {
|
469
607
|
this.renderingRegion.focus();
|
470
608
|
}
|
471
609
|
|
610
|
+
// Creates an element that will be positioned on top of the dry/wet ink
|
611
|
+
// renderers.
|
472
612
|
public createHTMLOverlay(overlay: HTMLElement) {
|
473
613
|
overlay.classList.add('overlay');
|
474
614
|
this.container.appendChild(overlay);
|
@@ -487,7 +627,7 @@ export class Editor {
|
|
487
627
|
}
|
488
628
|
|
489
629
|
// Dispatch a pen event to the currently selected tool.
|
490
|
-
//
|
630
|
+
// Intended primarially for unit tests.
|
491
631
|
public sendPenEvent(
|
492
632
|
eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
|
493
633
|
point: Point2,
|
@@ -567,7 +707,7 @@ export class Editor {
|
|
567
707
|
return this.importExportViewport.visibleRect;
|
568
708
|
}
|
569
709
|
|
570
|
-
// Resize the output SVG
|
710
|
+
// Resize the output SVG to match `imageRect`.
|
571
711
|
public setImportExportRect(imageRect: Rect2): Command {
|
572
712
|
const origSize = this.importExportViewport.visibleRect.size;
|
573
713
|
const origTransform = this.importExportViewport.canvasToScreenTransform;
|
@@ -587,14 +727,18 @@ export class Editor {
|
|
587
727
|
editor.queueRerender();
|
588
728
|
}
|
589
729
|
|
590
|
-
public description(localizationTable: EditorLocalization) {
|
730
|
+
public description(_editor: Editor, localizationTable: EditorLocalization) {
|
591
731
|
return localizationTable.resizeOutputCommand(imageRect);
|
592
732
|
}
|
593
733
|
};
|
594
734
|
}
|
595
735
|
|
596
|
-
|
597
|
-
|
736
|
+
/**
|
737
|
+
* Alias for loadFrom(SVGLoader.fromString).
|
738
|
+
*
|
739
|
+
* This is particularly useful when accessing a bundled version of the editor,
|
740
|
+
* where `SVGLoader.fromString` is unavailable.
|
741
|
+
*/
|
598
742
|
public async loadFromSVG(svgData: string) {
|
599
743
|
const loader = SVGLoader.fromString(svgData);
|
600
744
|
await this.loadFrom(loader);
|
package/src/EditorImage.test.ts
CHANGED
@@ -1,9 +1,7 @@
|
|
1
|
-
/* @jest-environment jsdom */
|
2
|
-
|
3
1
|
import EditorImage from './EditorImage';
|
4
2
|
import Stroke from './components/Stroke';
|
5
|
-
import { Vec2 } from './
|
6
|
-
import Path, { PathCommandType } from './
|
3
|
+
import { Vec2 } from './math/Vec2';
|
4
|
+
import Path, { PathCommandType } from './math/Path';
|
7
5
|
import Color4 from './Color4';
|
8
6
|
import DummyRenderer from './rendering/renderers/DummyRenderer';
|
9
7
|
import createEditor from './testing/createEditor';
|
package/src/EditorImage.ts
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
import Editor from './Editor';
|
2
2
|
import AbstractRenderer from './rendering/renderers/AbstractRenderer';
|
3
|
-
import Command from './commands/Command';
|
4
3
|
import Viewport from './Viewport';
|
5
4
|
import AbstractComponent from './components/AbstractComponent';
|
6
|
-
import Rect2 from './
|
5
|
+
import Rect2 from './math/Rect2';
|
7
6
|
import { EditorLocalization } from './localization';
|
8
7
|
import RenderingCache from './rendering/caching/RenderingCache';
|
9
8
|
import SerializableCommand from './commands/SerializableCommand';
|
10
9
|
|
10
|
+
// @internal
|
11
11
|
export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
|
12
12
|
leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
|
13
13
|
};
|
@@ -17,6 +17,7 @@ export default class EditorImage {
|
|
17
17
|
private root: ImageNode;
|
18
18
|
private componentsById: Record<string, AbstractComponent>;
|
19
19
|
|
20
|
+
// @internal
|
20
21
|
public constructor() {
|
21
22
|
this.root = new ImageNode();
|
22
23
|
this.componentsById = {};
|
@@ -33,15 +34,17 @@ export default class EditorImage {
|
|
33
34
|
return null;
|
34
35
|
}
|
35
36
|
|
37
|
+
/** @internal */
|
36
38
|
public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {
|
37
39
|
cache.render(screenRenderer, this.root, viewport);
|
38
40
|
}
|
39
41
|
|
42
|
+
/** @internal */
|
40
43
|
public render(renderer: AbstractRenderer, viewport: Viewport) {
|
41
44
|
this.root.render(renderer, viewport.visibleRect);
|
42
45
|
}
|
43
46
|
|
44
|
-
|
47
|
+
/** Renders all nodes, even ones not within the viewport. @internal */
|
45
48
|
public renderAll(renderer: AbstractRenderer) {
|
46
49
|
const leaves = this.root.getLeaves();
|
47
50
|
sortLeavesByZIndex(leaves);
|
@@ -58,6 +61,7 @@ export default class EditorImage {
|
|
58
61
|
return leaves.map(leaf => leaf.getContent()!);
|
59
62
|
}
|
60
63
|
|
64
|
+
/** @internal */
|
61
65
|
public onDestroyElement(elem: AbstractComponent) {
|
62
66
|
delete this.componentsById[elem.getId()];
|
63
67
|
}
|
@@ -71,7 +75,7 @@ export default class EditorImage {
|
|
71
75
|
return this.root.addLeaf(elem);
|
72
76
|
}
|
73
77
|
|
74
|
-
public static addElement(elem: AbstractComponent, applyByFlattening: boolean = false):
|
78
|
+
public static addElement(elem: AbstractComponent, applyByFlattening: boolean = false): SerializableCommand {
|
75
79
|
return new EditorImage.AddElementCommand(elem, applyByFlattening);
|
76
80
|
}
|
77
81
|
|
@@ -108,20 +112,21 @@ export default class EditorImage {
|
|
108
112
|
editor.queueRerender();
|
109
113
|
}
|
110
114
|
|
111
|
-
public description(localization: EditorLocalization) {
|
115
|
+
public description(_editor: Editor, localization: EditorLocalization) {
|
112
116
|
return localization.addElementAction(this.element.description(localization));
|
113
117
|
}
|
114
118
|
|
115
|
-
protected
|
116
|
-
return
|
119
|
+
protected serializeToJSON() {
|
120
|
+
return {
|
117
121
|
elemData: this.element.serialize(),
|
118
|
-
}
|
122
|
+
};
|
119
123
|
}
|
120
124
|
|
121
125
|
static {
|
122
|
-
SerializableCommand.register('add-element', (
|
123
|
-
const
|
124
|
-
const
|
126
|
+
SerializableCommand.register('add-element', (json: any, editor: Editor) => {
|
127
|
+
const id = json.elemData.id;
|
128
|
+
const foundElem = editor.image.lookupElement(id);
|
129
|
+
const elem = foundElem ?? AbstractComponent.deserialize(json.elemData);
|
125
130
|
return new EditorImage.AddElementCommand(elem);
|
126
131
|
});
|
127
132
|
}
|
@@ -130,7 +135,7 @@ export default class EditorImage {
|
|
130
135
|
|
131
136
|
type TooSmallToRenderCheck = (rect: Rect2)=> boolean;
|
132
137
|
|
133
|
-
|
138
|
+
/** Part of the Editor's image. @internal */
|
134
139
|
export class ImageNode {
|
135
140
|
private content: AbstractComponent|null;
|
136
141
|
private bbox: Rect2;
|
@@ -182,19 +187,29 @@ export class ImageNode {
|
|
182
187
|
// Returns a list of `ImageNode`s with content (and thus no children).
|
183
188
|
public getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[] {
|
184
189
|
const result: ImageNode[] = [];
|
190
|
+
let current: ImageNode|undefined;
|
191
|
+
const workList: ImageNode[] = [];
|
185
192
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
}
|
193
|
+
workList.push(this);
|
194
|
+
const toNext = () => {
|
195
|
+
current = undefined;
|
190
196
|
|
191
|
-
|
192
|
-
|
193
|
-
|
197
|
+
const next = workList.pop();
|
198
|
+
if (next && !isTooSmall?.(next.bbox)) {
|
199
|
+
current = next;
|
200
|
+
|
201
|
+
if (current.content !== null && current.getBBox().intersection(region)) {
|
202
|
+
result.push(current);
|
203
|
+
}
|
194
204
|
|
195
|
-
|
196
|
-
|
197
|
-
|
205
|
+
workList.push(
|
206
|
+
...current.getChildrenIntersectingRegion(region)
|
207
|
+
);
|
208
|
+
}
|
209
|
+
};
|
210
|
+
|
211
|
+
while (workList.length > 0) {
|
212
|
+
toNext();
|
198
213
|
}
|
199
214
|
|
200
215
|
return result;
|
@@ -239,15 +254,18 @@ export class ImageNode {
|
|
239
254
|
// share a parent.
|
240
255
|
const leafBBox = leaf.getBBox();
|
241
256
|
if (leafBBox.containsRect(this.getBBox())) {
|
242
|
-
// Create a node for this' children and for the new content..
|
243
257
|
const nodeForNewLeaf = new ImageNode(this);
|
244
|
-
const nodeForChildren = new ImageNode(this);
|
245
258
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
259
|
+
if (this.children.length < this.targetChildCount) {
|
260
|
+
this.children.push(nodeForNewLeaf);
|
261
|
+
} else {
|
262
|
+
const nodeForChildren = new ImageNode(this);
|
250
263
|
|
264
|
+
nodeForChildren.children = this.children;
|
265
|
+
this.children = [nodeForNewLeaf, nodeForChildren];
|
266
|
+
nodeForChildren.recomputeBBox(true);
|
267
|
+
nodeForChildren.updateParents();
|
268
|
+
}
|
251
269
|
return nodeForNewLeaf.addLeaf(leaf);
|
252
270
|
}
|
253
271
|
|
package/src/EventDispatcher.ts
CHANGED
@@ -1,13 +1,28 @@
|
|
1
|
-
|
1
|
+
/**
|
2
|
+
* Handles notifying listeners of events.
|
3
|
+
*
|
4
|
+
* `EventKeyType` is used to distinguish events (e.g. a `ClickEvent` vs a `TouchEvent`)
|
5
|
+
* while `EventMessageType` is the type of the data sent with an event (can be `void`).
|
6
|
+
*
|
7
|
+
* @example
|
8
|
+
* ```
|
9
|
+
* const dispatcher = new EventDispatcher<'event1'|'event2'|'event3', void>();
|
10
|
+
* dispatcher.on('event1', () => {
|
11
|
+
* console.log('Event 1 triggered.');
|
12
|
+
* });
|
13
|
+
* dispatcher.dispatch('event1');
|
14
|
+
* ```
|
15
|
+
*
|
16
|
+
* @packageDocumentation
|
17
|
+
*/
|
18
|
+
|
19
|
+
// Code shared with Joplin (js-draw was originally intended to be part of Joplin).
|
2
20
|
|
3
21
|
type Listener<Value> = (data: Value)=> void;
|
4
22
|
type CallbackHandler<EventType> = (data: EventType)=> void;
|
5
23
|
|
6
|
-
//
|
7
|
-
// while EventMessageType is the type of the data sent with an event (can be `void`)
|
24
|
+
// { @inheritDoc EventDispatcher! }
|
8
25
|
export default class EventDispatcher<EventKeyType extends string|symbol|number, EventMessageType> {
|
9
|
-
// Partial marks all fields as optional. To initialize with an empty object, this is required.
|
10
|
-
// See https://stackoverflow.com/a/64526384
|
11
26
|
private listeners: Partial<Record<EventKeyType, Array<Listener<EventMessageType>>>>;
|
12
27
|
public constructor() {
|
13
28
|
this.listeners = {};
|
@@ -38,7 +53,7 @@ export default class EventDispatcher<EventKeyType extends string|symbol|number,
|
|
38
53
|
};
|
39
54
|
}
|
40
55
|
|
41
|
-
|
56
|
+
/** Removes an event listener. This is equivalent to calling `.remove()` on the object returned by `.on`. */
|
42
57
|
public off(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>) {
|
43
58
|
const listeners = this.listeners[eventName];
|
44
59
|
if (!listeners) return;
|
package/src/Pointer.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Point2, Vec2 } from './
|
1
|
+
import { Point2, Vec2 } from './math/Vec2';
|
2
2
|
import Viewport from './Viewport';
|
3
3
|
|
4
4
|
export enum PointerDevice {
|
@@ -11,7 +11,7 @@ export enum PointerDevice {
|
|
11
11
|
}
|
12
12
|
|
13
13
|
// Provides a snapshot containing information about a pointer. A Pointer
|
14
|
-
// object is immutable
|
14
|
+
// object is immutable — it will not be updated when the pointer's information changes.
|
15
15
|
export default class Pointer {
|
16
16
|
private constructor(
|
17
17
|
// The (x, y) position of the pointer relative to the top-left corner
|
@@ -31,11 +31,12 @@ export default class Pointer {
|
|
31
31
|
// Unique ID for the pointer
|
32
32
|
public readonly id: number,
|
33
33
|
|
34
|
-
// Numeric timestamp (milliseconds, as from (new Date).getTime())
|
34
|
+
// Numeric timestamp (milliseconds, as from `(new Date).getTime()`)
|
35
35
|
public readonly timeStamp: number,
|
36
36
|
) {
|
37
37
|
}
|
38
38
|
|
39
|
+
// Creates a Pointer from a DOM event.
|
39
40
|
public static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport): Pointer {
|
40
41
|
const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
|
41
42
|
|
package/src/SVGLoader.ts
CHANGED
@@ -4,10 +4,10 @@ import Stroke from './components/Stroke';
|
|
4
4
|
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
5
5
|
import Text, { TextStyle } from './components/Text';
|
6
6
|
import UnknownSVGObject from './components/UnknownSVGObject';
|
7
|
-
import Mat33 from './
|
8
|
-
import Path from './
|
9
|
-
import Rect2 from './
|
10
|
-
import { Vec2 } from './
|
7
|
+
import Mat33 from './math/Mat33';
|
8
|
+
import Path from './math/Path';
|
9
|
+
import Rect2 from './math/Rect2';
|
10
|
+
import { Vec2 } from './math/Vec2';
|
11
11
|
import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
|
12
12
|
import RenderingStyle from './rendering/RenderingStyle';
|
13
13
|
import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
|
@@ -210,8 +210,16 @@ export default class SVGLoader implements ImageLoader {
|
|
210
210
|
transformProperty = elem.style.transform || 'none';
|
211
211
|
}
|
212
212
|
|
213
|
-
// Compute transform matrix
|
214
|
-
|
213
|
+
// Compute transform matrix. Prefer the actual .style.transform
|
214
|
+
// to the computed stylesheet -- in some browsers, the computedStyles version
|
215
|
+
// can have lower precision.
|
216
|
+
let transform;
|
217
|
+
try {
|
218
|
+
transform = Mat33.fromCSSMatrix(elem.style.transform);
|
219
|
+
} catch(_e) {
|
220
|
+
transform = Mat33.fromCSSMatrix(transformProperty);
|
221
|
+
}
|
222
|
+
|
215
223
|
const supportedAttrs = [];
|
216
224
|
const elemX = elem.getAttribute('x');
|
217
225
|
const elemY = elem.getAttribute('y');
|