js-draw 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/translation.md +4 -1
- package/CHANGELOG.md +8 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +4 -1
- package/dist/src/Editor.js +117 -2
- package/dist/src/EditorImage.js +4 -1
- package/dist/src/SVGLoader.d.ts +4 -1
- package/dist/src/SVGLoader.js +78 -33
- package/dist/src/UndoRedoHistory.d.ts +1 -0
- package/dist/src/UndoRedoHistory.js +6 -0
- package/dist/src/Viewport.d.ts +1 -0
- package/dist/src/Viewport.js +12 -4
- package/dist/src/commands/lib.d.ts +2 -1
- package/dist/src/commands/lib.js +2 -1
- package/dist/src/commands/localization.d.ts +1 -0
- package/dist/src/commands/localization.js +1 -0
- package/dist/src/commands/uniteCommands.d.ts +4 -0
- package/dist/src/commands/uniteCommands.js +105 -0
- package/dist/src/components/AbstractComponent.d.ts +2 -0
- package/dist/src/components/AbstractComponent.js +41 -5
- package/dist/src/components/ImageComponent.d.ts +27 -0
- package/dist/src/components/ImageComponent.js +129 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +2 -2
- package/dist/src/components/lib.d.ts +4 -2
- package/dist/src/components/lib.js +4 -2
- package/dist/src/components/localization.d.ts +2 -0
- package/dist/src/components/localization.js +2 -0
- package/dist/src/math/LineSegment2.d.ts +2 -0
- package/dist/src/math/LineSegment2.js +3 -0
- package/dist/src/rendering/localization.d.ts +3 -0
- package/dist/src/rendering/localization.js +3 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -0
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +5 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +45 -20
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
- package/dist/src/tools/BaseTool.d.ts +3 -1
- package/dist/src/tools/BaseTool.js +6 -0
- package/dist/src/tools/PasteHandler.d.ts +16 -0
- package/dist/src/tools/PasteHandler.js +142 -0
- package/dist/src/tools/SelectionTool.d.ts +7 -1
- package/dist/src/tools/SelectionTool.js +63 -5
- package/dist/src/tools/ToolController.js +36 -27
- package/dist/src/tools/lib.d.ts +1 -0
- package/dist/src/tools/lib.js +1 -0
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/dist/src/types.d.ts +13 -2
- package/dist/src/types.js +2 -0
- package/package.json +1 -1
- package/src/Editor.ts +131 -2
- package/src/EditorImage.ts +7 -1
- package/src/SVGLoader.ts +90 -36
- package/src/UndoRedoHistory.test.ts +33 -0
- package/src/UndoRedoHistory.ts +8 -0
- package/src/Viewport.ts +13 -4
- package/src/commands/lib.ts +2 -0
- package/src/commands/localization.ts +2 -0
- package/src/commands/uniteCommands.test.ts +23 -0
- package/src/commands/uniteCommands.ts +121 -0
- package/src/components/AbstractComponent.ts +55 -9
- package/src/components/ImageComponent.ts +153 -0
- package/src/components/builders/FreehandLineBuilder.ts +2 -2
- package/src/components/lib.ts +7 -2
- package/src/components/localization.ts +4 -0
- package/src/math/LineSegment2.test.ts +9 -0
- package/src/math/LineSegment2.ts +5 -0
- package/src/rendering/localization.ts +6 -0
- package/src/rendering/renderers/AbstractRenderer.ts +16 -0
- package/src/rendering/renderers/CanvasRenderer.ts +10 -1
- package/src/rendering/renderers/DummyRenderer.ts +6 -1
- package/src/rendering/renderers/SVGRenderer.ts +50 -21
- package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
- package/src/tools/BaseTool.ts +9 -1
- package/src/tools/PasteHandler.ts +156 -0
- package/src/tools/SelectionTool.ts +80 -8
- package/src/tools/ToolController.ts +51 -44
- package/src/tools/lib.ts +1 -0
- package/src/tools/localization.ts +8 -0
- package/src/types.ts +16 -2
package/dist/src/Editor.d.ts
CHANGED
@@ -94,6 +94,7 @@ export declare class Editor {
|
|
94
94
|
private loadingWarning;
|
95
95
|
private accessibilityAnnounceArea;
|
96
96
|
private accessibilityControlArea;
|
97
|
+
private eventListenerTargets;
|
97
98
|
private settings;
|
98
99
|
/**
|
99
100
|
* @example
|
@@ -139,6 +140,8 @@ export declare class Editor {
|
|
139
140
|
*/
|
140
141
|
addToolbar(defaultLayout?: boolean): HTMLToolbar;
|
141
142
|
private registerListeners;
|
143
|
+
private isEventSink;
|
144
|
+
private handlePaste;
|
142
145
|
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
143
146
|
handleKeyEventsFrom(elem: HTMLElement): void;
|
144
147
|
/** `apply` a command. `command` will be announced for accessibility. */
|
@@ -191,6 +194,6 @@ export declare class Editor {
|
|
191
194
|
* This is particularly useful when accessing a bundled version of the editor,
|
192
195
|
* where `SVGLoader.fromString` is unavailable.
|
193
196
|
*/
|
194
|
-
loadFromSVG(svgData: string): Promise<void>;
|
197
|
+
loadFromSVG(svgData: string, sanitize?: boolean): Promise<void>;
|
195
198
|
}
|
196
199
|
export default Editor;
|
package/dist/src/Editor.js
CHANGED
@@ -68,6 +68,7 @@ export class Editor {
|
|
68
68
|
*/
|
69
69
|
constructor(parent, settings = {}) {
|
70
70
|
var _a, _b, _c, _d;
|
71
|
+
this.eventListenerTargets = [];
|
71
72
|
this.previousAccessibilityAnnouncement = '';
|
72
73
|
this.announceUndoCallback = (command) => {
|
73
74
|
this.announceForAccessibility(this.localization.undoAnnouncement(command.description(this, this.localization)));
|
@@ -305,6 +306,110 @@ export class Editor {
|
|
305
306
|
this.accessibilityControlArea.addEventListener('input', () => {
|
306
307
|
this.accessibilityControlArea.value = '';
|
307
308
|
});
|
309
|
+
document.addEventListener('copy', evt => {
|
310
|
+
if (!this.isEventSink(document.querySelector(':focus'))) {
|
311
|
+
return;
|
312
|
+
}
|
313
|
+
const clipboardData = evt.clipboardData;
|
314
|
+
if (this.toolController.dispatchInputEvent({
|
315
|
+
kind: InputEvtType.CopyEvent,
|
316
|
+
setData: (mime, data) => {
|
317
|
+
clipboardData === null || clipboardData === void 0 ? void 0 : clipboardData.setData(mime, data);
|
318
|
+
},
|
319
|
+
})) {
|
320
|
+
evt.preventDefault();
|
321
|
+
}
|
322
|
+
});
|
323
|
+
document.addEventListener('paste', evt => {
|
324
|
+
this.handlePaste(evt);
|
325
|
+
});
|
326
|
+
}
|
327
|
+
isEventSink(evtTarget) {
|
328
|
+
let currentElem = evtTarget;
|
329
|
+
while (currentElem !== null) {
|
330
|
+
for (const elem of this.eventListenerTargets) {
|
331
|
+
if (elem === currentElem) {
|
332
|
+
return true;
|
333
|
+
}
|
334
|
+
}
|
335
|
+
currentElem = currentElem.parentElement;
|
336
|
+
}
|
337
|
+
return false;
|
338
|
+
}
|
339
|
+
handlePaste(evt) {
|
340
|
+
var _a, _b;
|
341
|
+
return __awaiter(this, void 0, void 0, function* () {
|
342
|
+
const target = (_a = document.querySelector(':focus')) !== null && _a !== void 0 ? _a : evt.target;
|
343
|
+
if (!this.isEventSink(target)) {
|
344
|
+
return;
|
345
|
+
}
|
346
|
+
const clipboardData = (_b = evt.dataTransfer) !== null && _b !== void 0 ? _b : evt.clipboardData;
|
347
|
+
if (!clipboardData) {
|
348
|
+
return;
|
349
|
+
}
|
350
|
+
// Handle SVG files (prefer to PNG/JPEG)
|
351
|
+
for (const file of clipboardData.files) {
|
352
|
+
if (file.type.toLowerCase() === 'image/svg+xml') {
|
353
|
+
const text = yield file.text();
|
354
|
+
if (this.toolController.dispatchInputEvent({
|
355
|
+
kind: InputEvtType.PasteEvent,
|
356
|
+
mime: file.type,
|
357
|
+
data: text,
|
358
|
+
})) {
|
359
|
+
evt.preventDefault();
|
360
|
+
return;
|
361
|
+
}
|
362
|
+
}
|
363
|
+
}
|
364
|
+
// Handle image files.
|
365
|
+
for (const file of clipboardData.files) {
|
366
|
+
const fileType = file.type.toLowerCase();
|
367
|
+
if (fileType === 'image/png' || fileType === 'image/jpg') {
|
368
|
+
const reader = new FileReader();
|
369
|
+
this.showLoadingWarning(0);
|
370
|
+
try {
|
371
|
+
const data = yield new Promise((resolve, reject) => {
|
372
|
+
reader.onload = () => resolve(reader.result);
|
373
|
+
reader.onerror = reject;
|
374
|
+
reader.onabort = reject;
|
375
|
+
reader.onprogress = (evt) => {
|
376
|
+
this.showLoadingWarning(evt.loaded / evt.total);
|
377
|
+
};
|
378
|
+
reader.readAsDataURL(file);
|
379
|
+
});
|
380
|
+
if (data && this.toolController.dispatchInputEvent({
|
381
|
+
kind: InputEvtType.PasteEvent,
|
382
|
+
mime: fileType,
|
383
|
+
data: data,
|
384
|
+
})) {
|
385
|
+
evt.preventDefault();
|
386
|
+
this.hideLoadingWarning();
|
387
|
+
return;
|
388
|
+
}
|
389
|
+
}
|
390
|
+
catch (e) {
|
391
|
+
console.error('Error reading image:', e);
|
392
|
+
}
|
393
|
+
this.hideLoadingWarning();
|
394
|
+
}
|
395
|
+
}
|
396
|
+
// Supported MIMEs for text data, in order of preference
|
397
|
+
const supportedMIMEs = [
|
398
|
+
'image/svg+xml',
|
399
|
+
'text/plain',
|
400
|
+
];
|
401
|
+
for (const mime of supportedMIMEs) {
|
402
|
+
const data = clipboardData.getData(mime);
|
403
|
+
if (data && this.toolController.dispatchInputEvent({
|
404
|
+
kind: InputEvtType.PasteEvent,
|
405
|
+
mime,
|
406
|
+
data,
|
407
|
+
})) {
|
408
|
+
evt.preventDefault();
|
409
|
+
return;
|
410
|
+
}
|
411
|
+
}
|
412
|
+
});
|
308
413
|
}
|
309
414
|
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
310
415
|
handleKeyEventsFrom(elem) {
|
@@ -333,6 +438,15 @@ export class Editor {
|
|
333
438
|
evt.preventDefault();
|
334
439
|
}
|
335
440
|
});
|
441
|
+
// Allow drop.
|
442
|
+
elem.ondragover = evt => {
|
443
|
+
evt.preventDefault();
|
444
|
+
};
|
445
|
+
elem.ondrop = evt => {
|
446
|
+
evt.preventDefault();
|
447
|
+
this.handlePaste(evt);
|
448
|
+
};
|
449
|
+
this.eventListenerTargets.push(elem);
|
336
450
|
}
|
337
451
|
/** `apply` a command. `command` will be announced for accessibility. */
|
338
452
|
dispatch(command, addToHistory = true) {
|
@@ -376,6 +490,7 @@ export class Editor {
|
|
376
490
|
*/
|
377
491
|
asyncApplyOrUnapplyCommands(commands, apply, updateChunkSize) {
|
378
492
|
return __awaiter(this, void 0, void 0, function* () {
|
493
|
+
console.assert(updateChunkSize > 0);
|
379
494
|
this.display.setDraftMode(true);
|
380
495
|
for (let i = 0; i < commands.length; i += updateChunkSize) {
|
381
496
|
this.showLoadingWarning(i / commands.length);
|
@@ -553,9 +668,9 @@ export class Editor {
|
|
553
668
|
* This is particularly useful when accessing a bundled version of the editor,
|
554
669
|
* where `SVGLoader.fromString` is unavailable.
|
555
670
|
*/
|
556
|
-
loadFromSVG(svgData) {
|
671
|
+
loadFromSVG(svgData, sanitize = false) {
|
557
672
|
return __awaiter(this, void 0, void 0, function* () {
|
558
|
-
const loader = SVGLoader.fromString(svgData);
|
673
|
+
const loader = SVGLoader.fromString(svgData, sanitize);
|
559
674
|
yield this.loadFrom(loader);
|
560
675
|
});
|
561
676
|
}
|
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/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;
|
@@ -5,6 +5,7 @@ class UndoRedoHistory {
|
|
5
5
|
this.editor = editor;
|
6
6
|
this.announceRedoCallback = announceRedoCallback;
|
7
7
|
this.announceUndoCallback = announceUndoCallback;
|
8
|
+
this.maxUndoRedoStackSize = 700;
|
8
9
|
this.undoStack = [];
|
9
10
|
this.redoStack = [];
|
10
11
|
}
|
@@ -25,6 +26,11 @@ class UndoRedoHistory {
|
|
25
26
|
elem.onDrop(this.editor);
|
26
27
|
}
|
27
28
|
this.redoStack = [];
|
29
|
+
if (this.undoStack.length > this.maxUndoRedoStackSize) {
|
30
|
+
const removeAtOnceCount = 10;
|
31
|
+
const removedElements = this.undoStack.splice(0, removeAtOnceCount);
|
32
|
+
removedElements.forEach(elem => elem.onDrop(this.editor));
|
33
|
+
}
|
28
34
|
this.fireUpdateEvent();
|
29
35
|
this.editor.notifier.dispatch(EditorEventType.CommandDone, {
|
30
36
|
kind: EditorEventType.CommandDone,
|
package/dist/src/Viewport.d.ts
CHANGED
@@ -29,6 +29,7 @@ export declare class Viewport {
|
|
29
29
|
getRotationAngle(): number;
|
30
30
|
static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
|
31
31
|
roundPoint(point: Point2): Point2;
|
32
|
+
computeZoomToTransform(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Mat33;
|
32
33
|
zoomTo(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Command;
|
33
34
|
}
|
34
35
|
export default Viewport;
|
package/dist/src/Viewport.js
CHANGED
@@ -29,6 +29,7 @@ export class Viewport {
|
|
29
29
|
updateScreenSize(screenSize) {
|
30
30
|
this.screenRect = this.screenRect.resizedTo(screenSize);
|
31
31
|
}
|
32
|
+
// Get the screen's visible region transformed into canvas space.
|
32
33
|
get visibleRect() {
|
33
34
|
return this.screenRect.transformedBoundingBox(this.inverseTransform);
|
34
35
|
}
|
@@ -93,10 +94,8 @@ export class Viewport {
|
|
93
94
|
roundPoint(point) {
|
94
95
|
return Viewport.roundPoint(point, 1 / this.getScaleFactor());
|
95
96
|
}
|
96
|
-
//
|
97
|
-
|
98
|
-
// Returns null if no transformation is necessary
|
99
|
-
zoomTo(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
|
97
|
+
// Computes and returns an affine transformation that makes `toMakeVisible` visible and roughly centered on the screen.
|
98
|
+
computeZoomToTransform(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
|
100
99
|
let transform = Mat33.identity;
|
101
100
|
if (toMakeVisible.w === 0 || toMakeVisible.h === 0) {
|
102
101
|
throw new Error(`${toMakeVisible.toString()} rectangle is empty! Cannot zoom to!`);
|
@@ -136,6 +135,15 @@ export class Viewport {
|
|
136
135
|
console.warn('Unable to zoom to ', toMakeVisible, '! Computed transform', transform, 'is singular.');
|
137
136
|
transform = Mat33.identity;
|
138
137
|
}
|
138
|
+
return transform;
|
139
|
+
}
|
140
|
+
// Returns a Command that transforms the view such that [rect] is visible, and perhaps
|
141
|
+
// centered in the viewport.
|
142
|
+
// Returns null if no transformation is necessary
|
143
|
+
//
|
144
|
+
// @see {@link computeZoomToTransform}
|
145
|
+
zoomTo(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
|
146
|
+
const transform = this.computeZoomToTransform(toMakeVisible, allowZoomIn, allowZoomOut);
|
139
147
|
return new Viewport.ViewportTransform(transform);
|
140
148
|
}
|
141
149
|
}
|
@@ -3,4 +3,5 @@ import Duplicate from './Duplicate';
|
|
3
3
|
import Erase from './Erase';
|
4
4
|
import invertCommand from './invertCommand';
|
5
5
|
import SerializableCommand from './SerializableCommand';
|
6
|
-
|
6
|
+
import uniteCommands from './uniteCommands';
|
7
|
+
export { Command, Duplicate, Erase, SerializableCommand, invertCommand, uniteCommands, };
|
package/dist/src/commands/lib.js
CHANGED
@@ -3,4 +3,5 @@ import Duplicate from './Duplicate';
|
|
3
3
|
import Erase from './Erase';
|
4
4
|
import invertCommand from './invertCommand';
|
5
5
|
import SerializableCommand from './SerializableCommand';
|
6
|
-
|
6
|
+
import uniteCommands from './uniteCommands';
|
7
|
+
export { Command, Duplicate, Erase, SerializableCommand, invertCommand, uniteCommands, };
|
@@ -17,6 +17,7 @@ export interface CommandLocalization {
|
|
17
17
|
eraseAction: (elemDescription: string, numElems: number) => string;
|
18
18
|
duplicateAction: (elemDescription: string, count: number) => string;
|
19
19
|
inverseOf: (actionDescription: string) => string;
|
20
|
+
unionOf: (actionDescription: string, actionCount: number) => string;
|
20
21
|
selectedElements: (count: number) => string;
|
21
22
|
}
|
22
23
|
export declare const defaultCommandLocalization: CommandLocalization;
|
@@ -5,6 +5,7 @@ export const defaultCommandLocalization = {
|
|
5
5
|
addElementAction: (componentDescription) => `Added ${componentDescription}`,
|
6
6
|
eraseAction: (componentDescription, numElems) => `Erased ${numElems} ${componentDescription}`,
|
7
7
|
duplicateAction: (componentDescription, numElems) => `Duplicated ${numElems} ${componentDescription}`,
|
8
|
+
unionOf: (actionDescription, actionCount) => `Union: ${actionCount} ${actionDescription}`,
|
8
9
|
inverseOf: (actionDescription) => `Inverse of ${actionDescription}`,
|
9
10
|
elements: 'Elements',
|
10
11
|
erasedNoElements: 'Erased nothing',
|
@@ -0,0 +1,4 @@
|
|
1
|
+
import Command from './Command';
|
2
|
+
import SerializableCommand from './SerializableCommand';
|
3
|
+
declare const uniteCommands: <T extends Command>(commands: T[], applyChunkSize?: number) => T extends SerializableCommand ? SerializableCommand : Command;
|
4
|
+
export default uniteCommands;
|
@@ -0,0 +1,105 @@
|
|
1
|
+
import Command from './Command';
|
2
|
+
import SerializableCommand from './SerializableCommand';
|
3
|
+
class NonSerializableUnion extends Command {
|
4
|
+
constructor(commands, applyChunkSize) {
|
5
|
+
super();
|
6
|
+
this.commands = commands;
|
7
|
+
this.applyChunkSize = applyChunkSize;
|
8
|
+
}
|
9
|
+
apply(editor) {
|
10
|
+
if (this.applyChunkSize === undefined) {
|
11
|
+
for (const command of this.commands) {
|
12
|
+
command.apply(editor);
|
13
|
+
}
|
14
|
+
}
|
15
|
+
else {
|
16
|
+
editor.asyncApplyCommands(this.commands, this.applyChunkSize);
|
17
|
+
}
|
18
|
+
}
|
19
|
+
unapply(editor) {
|
20
|
+
if (this.applyChunkSize === undefined) {
|
21
|
+
for (const command of this.commands) {
|
22
|
+
command.unapply(editor);
|
23
|
+
}
|
24
|
+
}
|
25
|
+
else {
|
26
|
+
editor.asyncUnapplyCommands(this.commands, this.applyChunkSize);
|
27
|
+
}
|
28
|
+
}
|
29
|
+
description(editor, localizationTable) {
|
30
|
+
const descriptions = [];
|
31
|
+
let lastDescription = null;
|
32
|
+
let duplicateDescriptionCount = 0;
|
33
|
+
for (const part of this.commands) {
|
34
|
+
const description = part.description(editor, localizationTable);
|
35
|
+
if (description !== lastDescription && lastDescription !== null) {
|
36
|
+
descriptions.push(localizationTable.unionOf(lastDescription, duplicateDescriptionCount));
|
37
|
+
lastDescription = null;
|
38
|
+
duplicateDescriptionCount = 0;
|
39
|
+
}
|
40
|
+
duplicateDescriptionCount++;
|
41
|
+
lastDescription !== null && lastDescription !== void 0 ? lastDescription : (lastDescription = description);
|
42
|
+
}
|
43
|
+
if (duplicateDescriptionCount > 1) {
|
44
|
+
descriptions.push(localizationTable.unionOf(lastDescription, duplicateDescriptionCount));
|
45
|
+
}
|
46
|
+
else if (duplicateDescriptionCount === 1) {
|
47
|
+
descriptions.push(lastDescription);
|
48
|
+
}
|
49
|
+
return descriptions.join(', ');
|
50
|
+
}
|
51
|
+
}
|
52
|
+
class SerializableUnion extends SerializableCommand {
|
53
|
+
constructor(commands, applyChunkSize) {
|
54
|
+
super('union');
|
55
|
+
this.commands = commands;
|
56
|
+
this.applyChunkSize = applyChunkSize;
|
57
|
+
this.nonserializableCommand = new NonSerializableUnion(commands, applyChunkSize);
|
58
|
+
}
|
59
|
+
serializeToJSON() {
|
60
|
+
return {
|
61
|
+
applyChunkSize: this.applyChunkSize,
|
62
|
+
data: this.commands.map(command => command.serialize()),
|
63
|
+
};
|
64
|
+
}
|
65
|
+
apply(editor) {
|
66
|
+
this.nonserializableCommand.apply(editor);
|
67
|
+
}
|
68
|
+
unapply(editor) {
|
69
|
+
this.nonserializableCommand.unapply(editor);
|
70
|
+
}
|
71
|
+
description(editor, localizationTable) {
|
72
|
+
return this.nonserializableCommand.description(editor, localizationTable);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
const uniteCommands = (commands, applyChunkSize) => {
|
76
|
+
let allSerializable = true;
|
77
|
+
for (const command of commands) {
|
78
|
+
if (!(command instanceof SerializableCommand)) {
|
79
|
+
allSerializable = false;
|
80
|
+
break;
|
81
|
+
}
|
82
|
+
}
|
83
|
+
if (!allSerializable) {
|
84
|
+
return new NonSerializableUnion(commands, applyChunkSize);
|
85
|
+
}
|
86
|
+
else {
|
87
|
+
const castedCommands = commands;
|
88
|
+
return new SerializableUnion(castedCommands, applyChunkSize);
|
89
|
+
}
|
90
|
+
};
|
91
|
+
SerializableCommand.register('union', (data, editor) => {
|
92
|
+
if (typeof data.data.length !== 'number') {
|
93
|
+
throw new Error('Unions of commands must serialize to lists of serialization data.');
|
94
|
+
}
|
95
|
+
const applyChunkSize = data.applyChunkSize;
|
96
|
+
if (typeof applyChunkSize !== 'number' && applyChunkSize !== undefined) {
|
97
|
+
throw new Error('serialized applyChunkSize is neither undefined nor a number.');
|
98
|
+
}
|
99
|
+
const commands = [];
|
100
|
+
for (const part of data.data) {
|
101
|
+
commands.push(SerializableCommand.deserialize(part, editor));
|
102
|
+
}
|
103
|
+
return uniteCommands(commands, applyChunkSize);
|
104
|
+
});
|
105
|
+
export default uniteCommands;
|
@@ -28,6 +28,8 @@ export default abstract class AbstractComponent {
|
|
28
28
|
protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
|
29
29
|
protected abstract applyTransformation(affineTransfm: Mat33): void;
|
30
30
|
transformBy(affineTransfm: Mat33): SerializableCommand;
|
31
|
+
private static transformElementCommandId;
|
32
|
+
private static UnresolvedTransformElementCommand;
|
31
33
|
private static TransformElementCommand;
|
32
34
|
abstract description(localizationTable: ImageComponentLocalization): string;
|
33
35
|
protected abstract createClone(): AbstractComponent;
|