js-draw 1.5.0 → 1.6.1
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/README.md +11 -2
- package/dist/Editor.css +2 -2
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +8 -0
- package/dist/cjs/Editor.js +32 -9
- package/dist/cjs/SVGLoader.d.ts +6 -0
- package/dist/cjs/SVGLoader.js +100 -56
- package/dist/cjs/commands/Erase.d.ts +7 -1
- package/dist/cjs/commands/Erase.js +13 -4
- package/dist/cjs/commands/invertCommand.js +1 -1
- package/dist/cjs/components/Stroke.d.ts +1 -1
- package/dist/cjs/components/Stroke.js +1 -1
- package/dist/cjs/image/EditorImage.js +9 -9
- package/dist/cjs/toolbar/AbstractToolbar.d.ts +4 -1
- package/dist/cjs/toolbar/AbstractToolbar.js +7 -1
- package/dist/cjs/toolbar/widgets/BaseToolWidget.js +4 -13
- package/dist/cjs/toolbar/widgets/BaseToolWidget.test.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/BaseWidget.d.ts +5 -1
- package/dist/cjs/toolbar/widgets/BaseWidget.js +45 -28
- package/dist/cjs/toolbar/widgets/BaseWidget.test.d.ts +1 -0
- package/dist/cjs/tools/BaseTool.js +1 -0
- package/dist/cjs/tools/SelectionTool/Selection.js +7 -5
- package/dist/cjs/tools/ToolController.d.ts +23 -0
- package/dist/cjs/tools/ToolController.js +65 -4
- package/dist/cjs/tools/ToolController.test.d.ts +1 -0
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +8 -0
- package/dist/mjs/Editor.mjs +32 -9
- package/dist/mjs/SVGLoader.d.ts +6 -0
- package/dist/mjs/SVGLoader.mjs +99 -55
- package/dist/mjs/commands/Erase.d.ts +7 -1
- package/dist/mjs/commands/Erase.mjs +13 -4
- package/dist/mjs/commands/invertCommand.mjs +1 -1
- package/dist/mjs/components/Stroke.d.ts +1 -1
- package/dist/mjs/components/Stroke.mjs +1 -1
- package/dist/mjs/image/EditorImage.mjs +9 -9
- package/dist/mjs/toolbar/AbstractToolbar.d.ts +4 -1
- package/dist/mjs/toolbar/AbstractToolbar.mjs +7 -1
- package/dist/mjs/toolbar/widgets/BaseToolWidget.mjs +4 -13
- package/dist/mjs/toolbar/widgets/BaseToolWidget.test.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/BaseWidget.d.ts +5 -1
- package/dist/mjs/toolbar/widgets/BaseWidget.mjs +45 -28
- package/dist/mjs/toolbar/widgets/BaseWidget.test.d.ts +1 -0
- package/dist/mjs/tools/BaseTool.mjs +1 -0
- package/dist/mjs/tools/SelectionTool/Selection.mjs +7 -5
- package/dist/mjs/tools/ToolController.d.ts +23 -0
- package/dist/mjs/tools/ToolController.mjs +65 -4
- package/dist/mjs/tools/ToolController.test.d.ts +1 -0
- package/dist/mjs/version.mjs +1 -1
- package/docs/img/readme-images/js-draw.png +0 -0
- package/docs/img/readme-images/logo.svg +1 -0
- package/package.json +6 -6
- package/src/Editor.scss +4 -2
package/dist/mjs/SVGLoader.mjs
CHANGED
@@ -18,6 +18,12 @@ export const svgLoaderAttributeContainerID = 'svgContainerID';
|
|
18
18
|
// If present in the exported SVG's class list, the image will be
|
19
19
|
// autoresized when components are added/removed.
|
20
20
|
export const svgLoaderAutoresizeClassName = 'js-draw--autoresize';
|
21
|
+
// @internal
|
22
|
+
export var SVGLoaderLoadMethod;
|
23
|
+
(function (SVGLoaderLoadMethod) {
|
24
|
+
SVGLoaderLoadMethod["IFrame"] = "iframe";
|
25
|
+
SVGLoaderLoadMethod["DOMParser"] = "domparser";
|
26
|
+
})(SVGLoaderLoadMethod || (SVGLoaderLoadMethod = {}));
|
21
27
|
const supportedStrokeFillStyleAttrs = ['stroke', 'fill', 'stroke-width'];
|
22
28
|
// Handles loading images from SVG.
|
23
29
|
export default class SVGLoader {
|
@@ -39,7 +45,7 @@ export default class SVGLoader {
|
|
39
45
|
let fill = Color4.transparent;
|
40
46
|
let stroke;
|
41
47
|
// If possible, use computedStyles (allows property inheritance).
|
42
|
-
const fillAttribute = node.getAttribute('fill') ?? computedStyles?.fill ?? node.style
|
48
|
+
const fillAttribute = node.getAttribute('fill') ?? computedStyles?.fill ?? node.style?.fill;
|
43
49
|
if (fillAttribute) {
|
44
50
|
try {
|
45
51
|
fill = Color4.fromString(fillAttribute);
|
@@ -48,8 +54,8 @@ export default class SVGLoader {
|
|
48
54
|
console.error('Unknown fill color,', fillAttribute);
|
49
55
|
}
|
50
56
|
}
|
51
|
-
const strokeAttribute = node.getAttribute('stroke') ?? computedStyles?.stroke ?? node.style
|
52
|
-
const strokeWidthAttr = node.getAttribute('stroke-width') ?? computedStyles?.strokeWidth ?? node.style
|
57
|
+
const strokeAttribute = node.getAttribute('stroke') ?? computedStyles?.stroke ?? node.style?.stroke ?? '';
|
58
|
+
const strokeWidthAttr = node.getAttribute('stroke-width') ?? computedStyles?.strokeWidth ?? node.style?.strokeWidth ?? '';
|
53
59
|
if (strokeAttribute && strokeWidthAttr) {
|
54
60
|
try {
|
55
61
|
let width = parseFloat(strokeWidthAttr ?? '1');
|
@@ -208,6 +214,16 @@ export default class SVGLoader {
|
|
208
214
|
await this.addUnknownNode(node);
|
209
215
|
}
|
210
216
|
}
|
217
|
+
getComputedStyle(element) {
|
218
|
+
try {
|
219
|
+
// getComputedStyle may fail in jsdom when using a DOMParser.
|
220
|
+
return window.getComputedStyle(element);
|
221
|
+
}
|
222
|
+
catch (error) {
|
223
|
+
console.warn('Error computing style', error);
|
224
|
+
return undefined;
|
225
|
+
}
|
226
|
+
}
|
211
227
|
// If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
|
212
228
|
// to prevent storing duplicate transform information when saving the component.
|
213
229
|
getTransform(elem, supportedAttrs, computedStyles) {
|
@@ -225,10 +241,10 @@ export default class SVGLoader {
|
|
225
241
|
}
|
226
242
|
}
|
227
243
|
if (!transform) {
|
228
|
-
computedStyles ??=
|
229
|
-
let transformProperty = computedStyles
|
230
|
-
if (transformProperty
|
231
|
-
transformProperty = elem.style
|
244
|
+
computedStyles ??= this.getComputedStyle(elem);
|
245
|
+
let transformProperty = computedStyles?.transform;
|
246
|
+
if (!transformProperty || transformProperty === 'none') {
|
247
|
+
transformProperty = elem.style?.transform || 'none';
|
232
248
|
}
|
233
249
|
// Prefer the actual .style.transform
|
234
250
|
// to the computed stylesheet -- in some browsers, the computedStyles version
|
@@ -278,18 +294,18 @@ export default class SVGLoader {
|
|
278
294
|
contentList.push('');
|
279
295
|
}
|
280
296
|
// Compute styles.
|
281
|
-
const computedStyles =
|
297
|
+
const computedStyles = this.getComputedStyle(elem);
|
282
298
|
const fontSizeExp = /^([-0-9.e]+)px/i;
|
283
299
|
// In some environments, computedStyles.fontSize can be increased by the system.
|
284
300
|
// Thus, to prevent text from growing on load/save, prefer .style.fontSize.
|
285
|
-
let fontSizeMatch = fontSizeExp.exec(elem.style
|
301
|
+
let fontSizeMatch = fontSizeExp.exec(elem.style?.fontSize ?? '');
|
286
302
|
if (!fontSizeMatch && elem.tagName.toLowerCase() === 'tspan' && elem.parentElement) {
|
287
303
|
// Try to inherit the font size of the parent text element.
|
288
|
-
fontSizeMatch = fontSizeExp.exec(elem.parentElement.style
|
304
|
+
fontSizeMatch = fontSizeExp.exec(elem.parentElement.style?.fontSize ?? '');
|
289
305
|
}
|
290
306
|
// If we still couldn't find a font size, try to use computedStyles (which can be
|
291
307
|
// wrong).
|
292
|
-
if (!fontSizeMatch) {
|
308
|
+
if (!fontSizeMatch && computedStyles) {
|
293
309
|
fontSizeMatch = fontSizeExp.exec(computedStyles.fontSize);
|
294
310
|
}
|
295
311
|
const supportedStyleAttrs = [
|
@@ -304,9 +320,9 @@ export default class SVGLoader {
|
|
304
320
|
}
|
305
321
|
const style = {
|
306
322
|
size: fontSize,
|
307
|
-
fontFamily: computedStyles
|
308
|
-
fontWeight: computedStyles
|
309
|
-
fontStyle: computedStyles
|
323
|
+
fontFamily: computedStyles?.fontFamily || elem.style?.fontFamily || 'sans-serif',
|
324
|
+
fontWeight: computedStyles?.fontWeight || elem.style?.fontWeight || undefined,
|
325
|
+
fontStyle: computedStyles?.fontStyle || elem.style?.fontStyle || undefined,
|
310
326
|
renderingStyle: this.getStyle(elem, computedStyles),
|
311
327
|
};
|
312
328
|
const supportedAttrs = [];
|
@@ -513,43 +529,74 @@ export default class SVGLoader {
|
|
513
529
|
* @param options - if `true` or `false`, treated as the `sanitize` option -- don't store unknown attributes.
|
514
530
|
*/
|
515
531
|
static fromString(text, options = false) {
|
516
|
-
const
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
532
|
+
const domParserLoad = typeof options !== 'boolean' && options?.loadMethod === 'domparser';
|
533
|
+
const { svgElem, cleanUp } = (() => {
|
534
|
+
// If the user requested an iframe load (the default) try to load with an iframe.
|
535
|
+
// There are some cases (e.g. in a sandboxed iframe) where this doesn't work.
|
536
|
+
if (!domParserLoad) {
|
537
|
+
try {
|
538
|
+
const sandbox = document.createElement('iframe');
|
539
|
+
sandbox.src = 'about:blank';
|
540
|
+
// allow-same-origin is necessary for how we interact with the sandbox. As such,
|
541
|
+
// DO NOT ENABLE ALLOW-SCRIPTS.
|
542
|
+
sandbox.setAttribute('sandbox', 'allow-same-origin');
|
543
|
+
sandbox.setAttribute('csp', 'default-src \'about:blank\'');
|
544
|
+
sandbox.style.display = 'none';
|
545
|
+
// Required to access the frame's DOM. See https://stackoverflow.com/a/17777943/17055750
|
546
|
+
document.body.appendChild(sandbox);
|
547
|
+
if (!sandbox.hasAttribute('sandbox')) {
|
548
|
+
sandbox.remove();
|
549
|
+
throw new Error('SVG loading iframe is not sandboxed.');
|
550
|
+
}
|
551
|
+
const sandboxDoc = sandbox.contentWindow?.document ?? sandbox.contentDocument;
|
552
|
+
if (sandboxDoc == null)
|
553
|
+
throw new Error('Unable to open a sandboxed iframe!');
|
554
|
+
sandboxDoc.open();
|
555
|
+
sandboxDoc.write(`
|
556
|
+
<!DOCTYPE html>
|
557
|
+
<html>
|
558
|
+
<head>
|
559
|
+
<title>SVG Loading Sandbox</title>
|
560
|
+
<meta name='viewport' conent='width=device-width,initial-scale=1.0'/>
|
561
|
+
<meta charset='utf-8'/>
|
562
|
+
</head>
|
563
|
+
<body style='font-size: 12px;'>
|
564
|
+
<script>
|
565
|
+
console.error('JavaScript should not be able to run here!');
|
566
|
+
throw new Error(
|
567
|
+
'The SVG sandbox is broken! Please double-check the sandboxing setting.'
|
568
|
+
);
|
569
|
+
</script>
|
570
|
+
</body>
|
571
|
+
</html>
|
572
|
+
`);
|
573
|
+
sandboxDoc.close();
|
574
|
+
const svgElem = sandboxDoc.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
575
|
+
svgElem.innerHTML = text;
|
576
|
+
sandboxDoc.body.appendChild(svgElem);
|
577
|
+
const cleanUp = () => {
|
578
|
+
svgElem.remove();
|
579
|
+
sandbox.remove();
|
580
|
+
};
|
581
|
+
return { svgElem, cleanUp };
|
582
|
+
}
|
583
|
+
catch (error) {
|
584
|
+
console.warn('Failed loading SVG via a sandboxed iframe. Some styles may not be loaded correctly. Error: ', error);
|
585
|
+
}
|
586
|
+
}
|
587
|
+
// Fall back to creating a DOMParser
|
588
|
+
const parser = new DOMParser();
|
589
|
+
const doc = parser.parseFromString(`<svg xmlns="http://www.w3.org/2000/svg">${text}</svg>`, 'text/html');
|
590
|
+
const svgElem = doc.querySelector('svg');
|
591
|
+
// Handle error messages reported while parsing. See
|
592
|
+
// https://developer.mozilla.org/en-US/docs/Web/Guide/Parsing_and_serializing_XML
|
593
|
+
const errorReportNode = doc.querySelector('parsererror');
|
594
|
+
if (errorReportNode) {
|
595
|
+
throw new Error('Parse error: ' + errorReportNode.textContent);
|
596
|
+
}
|
597
|
+
const cleanUp = () => { };
|
598
|
+
return { svgElem, cleanUp };
|
599
|
+
})();
|
553
600
|
// Handle options
|
554
601
|
let sanitize;
|
555
602
|
let disableUnknownObjectWarnings;
|
@@ -561,10 +608,7 @@ export default class SVGLoader {
|
|
561
608
|
sanitize = options.sanitize ?? false;
|
562
609
|
disableUnknownObjectWarnings = options.disableUnknownObjectWarnings ?? false;
|
563
610
|
}
|
564
|
-
return new SVGLoader(svgElem,
|
565
|
-
svgElem.remove();
|
566
|
-
sandbox.remove();
|
567
|
-
}, {
|
611
|
+
return new SVGLoader(svgElem, cleanUp, {
|
568
612
|
sanitize, disableUnknownObjectWarnings,
|
569
613
|
});
|
570
614
|
}
|
@@ -30,5 +30,11 @@ export default class Erase extends SerializableCommand {
|
|
30
30
|
unapply(editor: Editor): void;
|
31
31
|
onDrop(editor: Editor): void;
|
32
32
|
description(_editor: Editor, localizationTable: EditorLocalization): string;
|
33
|
-
protected serializeToJSON():
|
33
|
+
protected serializeToJSON(): {
|
34
|
+
name: string;
|
35
|
+
zIndex: number;
|
36
|
+
id: string;
|
37
|
+
loadSaveData: import("../components/AbstractComponent").LoadSaveDataTable;
|
38
|
+
data: string | number | any[] | Record<string, any>;
|
39
|
+
}[];
|
34
40
|
}
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import AbstractComponent from '../components/AbstractComponent.mjs';
|
1
2
|
import describeComponentList from '../components/util/describeComponentList.mjs';
|
2
3
|
import EditorImage from '../image/EditorImage.mjs';
|
3
4
|
import SerializableCommand from './SerializableCommand.mjs';
|
@@ -63,15 +64,23 @@ class Erase extends SerializableCommand {
|
|
63
64
|
return localizationTable.eraseAction(description, this.toRemove.length);
|
64
65
|
}
|
65
66
|
serializeToJSON() {
|
66
|
-
|
67
|
-
return
|
67
|
+
// If applied, the elements can't be fetched from the image because they're
|
68
|
+
// erased. Serialize and return the elements themselves.
|
69
|
+
const elems = this.toRemove.map(elem => elem.serialize());
|
70
|
+
return elems;
|
68
71
|
}
|
69
72
|
}
|
70
73
|
(() => {
|
71
74
|
SerializableCommand.register('erase', (json, editor) => {
|
75
|
+
if (!Array.isArray(json)) {
|
76
|
+
throw new Error('seralized erase data must be an array');
|
77
|
+
}
|
72
78
|
const elems = json
|
73
|
-
.map((
|
74
|
-
|
79
|
+
.map((elemData) => {
|
80
|
+
const componentId = typeof elemData === 'string' ? elemData : `${elemData.id}`;
|
81
|
+
const component = editor.image.lookupElement(componentId) ?? AbstractComponent.deserialize(elemData);
|
82
|
+
return component;
|
83
|
+
});
|
75
84
|
return new Erase(elems);
|
76
85
|
});
|
77
86
|
})();
|
@@ -28,7 +28,7 @@ class EditorImage {
|
|
28
28
|
this.settingExportRect = false;
|
29
29
|
this.root = new RootImageNode();
|
30
30
|
this.background = new RootImageNode();
|
31
|
-
this.componentsById =
|
31
|
+
this.componentsById = Object.create(null);
|
32
32
|
this.notifier = new EventDispatcher();
|
33
33
|
this.importExportViewport = new Viewport(() => {
|
34
34
|
this.onExportViewportChanged();
|
@@ -182,11 +182,11 @@ class EditorImage {
|
|
182
182
|
* @see {@link Display.flatten}
|
183
183
|
*/
|
184
184
|
static addElement(elem, applyByFlattening = false) {
|
185
|
-
return new
|
185
|
+
return new _a.AddElementCommand(elem, applyByFlattening);
|
186
186
|
}
|
187
187
|
/** @see EditorImage.addElement */
|
188
188
|
addElement(elem, applyByFlattening) {
|
189
|
-
return
|
189
|
+
return _a.addElement(elem, applyByFlattening);
|
190
190
|
}
|
191
191
|
/**
|
192
192
|
* @returns a `Viewport` for rendering the image when importing/exporting.
|
@@ -205,7 +205,7 @@ class EditorImage {
|
|
205
205
|
* autoresize (if it was previously enabled).
|
206
206
|
*/
|
207
207
|
setImportExportRect(imageRect) {
|
208
|
-
return
|
208
|
+
return _a.SetImportExportRectCommand.of(this, imageRect, false);
|
209
209
|
}
|
210
210
|
getAutoresizeEnabled() {
|
211
211
|
return this.shouldAutoresizeExportViewport;
|
@@ -216,7 +216,7 @@ class EditorImage {
|
|
216
216
|
return Command.empty;
|
217
217
|
}
|
218
218
|
const newBBox = this.root.getBBox();
|
219
|
-
return
|
219
|
+
return _a.SetImportExportRectCommand.of(this, newBBox, autoresize);
|
220
220
|
}
|
221
221
|
setAutoresizeEnabledDirectly(shouldAutoresize) {
|
222
222
|
if (shouldAutoresize !== this.shouldAutoresizeExportViewport) {
|
@@ -322,7 +322,7 @@ EditorImage.AddElementCommand = (_b = class extends SerializableCommand {
|
|
322
322
|
const id = json.elemData.id;
|
323
323
|
const foundElem = editor.image.lookupElement(id);
|
324
324
|
const elem = foundElem ?? AbstractComponent.deserialize(json.elemData);
|
325
|
-
const result = new
|
325
|
+
const result = new _a.AddElementCommand(elem);
|
326
326
|
result.serializedElem = json.elemData;
|
327
327
|
return result;
|
328
328
|
});
|
@@ -331,7 +331,7 @@ EditorImage.AddElementCommand = (_b = class extends SerializableCommand {
|
|
331
331
|
// Handles resizing the background import/export region of the image.
|
332
332
|
EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand {
|
333
333
|
constructor(originalSize, originalTransform, originalAutoresize, newExportRect, newAutoresize) {
|
334
|
-
super(
|
334
|
+
super(_a.SetImportExportRectCommand.commandId);
|
335
335
|
this.originalSize = originalSize;
|
336
336
|
this.originalTransform = originalTransform;
|
337
337
|
this.originalAutoresize = originalAutoresize;
|
@@ -344,7 +344,7 @@ EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand
|
|
344
344
|
const originalSize = importExportViewport.visibleRect.size;
|
345
345
|
const originalTransform = importExportViewport.canvasToScreenTransform;
|
346
346
|
const originalAutoresize = image.getAutoresizeEnabled();
|
347
|
-
return new
|
347
|
+
return new _a.SetImportExportRectCommand(originalSize, originalTransform, originalAutoresize, newExportRect, newAutoresize);
|
348
348
|
}
|
349
349
|
apply(editor) {
|
350
350
|
editor.image.setAutoresizeEnabledDirectly(this.newAutoresize);
|
@@ -405,7 +405,7 @@ EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand
|
|
405
405
|
const finalRect = new Rect2(json.newRegion.x, json.newRegion.y, json.newRegion.w, json.newRegion.h);
|
406
406
|
const autoresize = json.autoresize ?? false;
|
407
407
|
const originalAutoresize = json.originalAutoresize ?? false;
|
408
|
-
return new
|
408
|
+
return new _a.SetImportExportRectCommand(originalSize, originalTransform, originalAutoresize, finalRect, autoresize);
|
409
409
|
});
|
410
410
|
})(),
|
411
411
|
_c);
|
@@ -162,7 +162,10 @@ export default abstract class AbstractToolbar {
|
|
162
162
|
* Adds both the default tool widgets and action buttons.
|
163
163
|
*/
|
164
164
|
abstract addDefaults(): void;
|
165
|
-
/**
|
165
|
+
/**
|
166
|
+
* Remove this toolbar from its container and clean up listeners.
|
167
|
+
* This should only be called **once** for a given toolbar.
|
168
|
+
*/
|
166
169
|
remove(): void;
|
167
170
|
/**
|
168
171
|
* Removes `listener` when {@link remove} is called.
|
@@ -399,7 +399,10 @@ class AbstractToolbar {
|
|
399
399
|
addDefaultActionButtons() {
|
400
400
|
this.addUndoRedoButtons();
|
401
401
|
}
|
402
|
-
/**
|
402
|
+
/**
|
403
|
+
* Remove this toolbar from its container and clean up listeners.
|
404
|
+
* This should only be called **once** for a given toolbar.
|
405
|
+
*/
|
403
406
|
remove() {
|
404
407
|
this.closeColorPickerOverlay?.remove();
|
405
408
|
for (const listener of __classPrivateFieldGet(this, _AbstractToolbar_listeners, "f")) {
|
@@ -407,6 +410,9 @@ class AbstractToolbar {
|
|
407
410
|
}
|
408
411
|
__classPrivateFieldSet(this, _AbstractToolbar_listeners, [], "f");
|
409
412
|
this.onRemove();
|
413
|
+
for (const widget of __classPrivateFieldGet(this, _AbstractToolbar_widgetList, "f")) {
|
414
|
+
widget.remove();
|
415
|
+
}
|
410
416
|
}
|
411
417
|
/**
|
412
418
|
* Removes `listener` when {@link remove} is called.
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import { EditorEventType } from '../../types.mjs';
|
2
1
|
import BaseWidget from './BaseWidget.mjs';
|
3
2
|
import { toolbarCSSPrefix } from '../constants.mjs';
|
4
3
|
const isToolWidgetFocused = () => {
|
@@ -9,11 +8,8 @@ export default class BaseToolWidget extends BaseWidget {
|
|
9
8
|
constructor(editor, targetTool, id, localizationTable) {
|
10
9
|
super(editor, id, localizationTable);
|
11
10
|
this.targetTool = targetTool;
|
12
|
-
|
13
|
-
if (
|
14
|
-
throw new Error('Incorrect event type! (Expected ToolEnabled)');
|
15
|
-
}
|
16
|
-
if (toolEvt.tool === targetTool) {
|
11
|
+
this.targetTool.enabledValue().onUpdateAndNow(enabled => {
|
12
|
+
if (enabled) {
|
17
13
|
this.setSelected(true);
|
18
14
|
// Transfer focus to the current button, only if another toolbar button is
|
19
15
|
// focused.
|
@@ -23,12 +19,7 @@ export default class BaseToolWidget extends BaseWidget {
|
|
23
19
|
this.focus();
|
24
20
|
}
|
25
21
|
}
|
26
|
-
|
27
|
-
editor.notifier.on(EditorEventType.ToolDisabled, toolEvt => {
|
28
|
-
if (toolEvt.kind !== EditorEventType.ToolDisabled) {
|
29
|
-
throw new Error('Incorrect event type! (Expected ToolDisabled)');
|
30
|
-
}
|
31
|
-
if (toolEvt.tool === targetTool) {
|
22
|
+
else {
|
32
23
|
this.setSelected(false);
|
33
24
|
this.setDropdownVisible(false);
|
34
25
|
}
|
@@ -52,7 +43,7 @@ export default class BaseToolWidget extends BaseWidget {
|
|
52
43
|
}
|
53
44
|
}
|
54
45
|
onKeyPress(event) {
|
55
|
-
if (this.isSelected() && event.
|
46
|
+
if (this.isSelected() && event.code === 'Space' && this.hasDropdown) {
|
56
47
|
this.handleClick();
|
57
48
|
return true;
|
58
49
|
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -81,13 +81,17 @@ export default abstract class BaseWidget {
|
|
81
81
|
* @internal
|
82
82
|
*/
|
83
83
|
addTo(parent: HTMLElement): HTMLElement;
|
84
|
+
/**
|
85
|
+
* Remove this. This allows the widget to be added to a toolbar again
|
86
|
+
* in the future using {@link addTo}.
|
87
|
+
*/
|
88
|
+
remove(): void;
|
84
89
|
focus(): void;
|
85
90
|
/**
|
86
91
|
* @internal
|
87
92
|
*/
|
88
93
|
addCSSClassToContainer(className: string): void;
|
89
94
|
removeCSSClassFromContainer(className: string): void;
|
90
|
-
remove(): void;
|
91
95
|
protected updateIcon(): void;
|
92
96
|
setDisabled(disabled: boolean): void;
|
93
97
|
setSelected(selected: boolean): void;
|
@@ -9,7 +9,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
11
11
|
};
|
12
|
-
var _BaseWidget_hasDropdown, _BaseWidget_disabledDueToReadOnlyEditor, _BaseWidget_tags,
|
12
|
+
var _BaseWidget_instances, _a, _BaseWidget_hasDropdown, _BaseWidget_disabledDueToReadOnlyEditor, _BaseWidget_tags, _BaseWidget_removeEditorListeners, _BaseWidget_addEditorListeners;
|
13
13
|
import ToolbarShortcutHandler from '../../tools/ToolbarShortcutHandler.mjs';
|
14
14
|
import { keyPressEventFromHTMLEvent, keyUpEventFromHTMLEvent } from '../../inputEvents.mjs';
|
15
15
|
import { toolbarCSSPrefix } from '../constants.mjs';
|
@@ -26,6 +26,7 @@ export var ToolbarWidgetTag;
|
|
26
26
|
})(ToolbarWidgetTag || (ToolbarWidgetTag = {}));
|
27
27
|
class BaseWidget {
|
28
28
|
constructor(editor, id, localizationTable) {
|
29
|
+
_BaseWidget_instances.add(this);
|
29
30
|
this.editor = editor;
|
30
31
|
this.id = id;
|
31
32
|
this.dropdown = null;
|
@@ -38,8 +39,7 @@ class BaseWidget {
|
|
38
39
|
// Maps subWidget IDs to subWidgets.
|
39
40
|
this.subWidgets = {};
|
40
41
|
this.toplevel = true;
|
41
|
-
|
42
|
-
_BaseWidget_readOnlyListener.set(this, null);
|
42
|
+
_BaseWidget_removeEditorListeners.set(this, null);
|
43
43
|
this.localizationTable = localizationTable ?? editor.localization;
|
44
44
|
// Default layout manager
|
45
45
|
const defaultLayoutManager = new DropdownLayoutManager((text) => this.editor.announceForAccessibility(text), this.localizationTable);
|
@@ -60,12 +60,6 @@ class BaseWidget {
|
|
60
60
|
this.button.oncontextmenu = event => {
|
61
61
|
event.preventDefault();
|
62
62
|
};
|
63
|
-
const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler);
|
64
|
-
// If the onKeyPress function has been extended and the editor is configured to send keypress events to
|
65
|
-
// toolbar widgets,
|
66
|
-
if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== BaseWidget.prototype.onKeyPress) {
|
67
|
-
toolbarShortcutHandlers[0].registerListener(event => this.onKeyPress(event));
|
68
|
-
}
|
69
63
|
}
|
70
64
|
/**
|
71
65
|
* Should return a constant true or false value. If true (the default),
|
@@ -268,22 +262,18 @@ class BaseWidget {
|
|
268
262
|
if (this.container.parentElement) {
|
269
263
|
this.container.remove();
|
270
264
|
}
|
271
|
-
|
272
|
-
if (readOnly && this.shouldAutoDisableInReadOnlyEditor() && !this.disabled) {
|
273
|
-
this.setDisabled(true);
|
274
|
-
__classPrivateFieldSet(this, _BaseWidget_disabledDueToReadOnlyEditor, true, "f");
|
275
|
-
if (__classPrivateFieldGet(this, _BaseWidget_hasDropdown, "f")) {
|
276
|
-
this.dropdown?.requestHide();
|
277
|
-
}
|
278
|
-
}
|
279
|
-
else if (!readOnly && __classPrivateFieldGet(this, _BaseWidget_disabledDueToReadOnlyEditor, "f")) {
|
280
|
-
__classPrivateFieldSet(this, _BaseWidget_disabledDueToReadOnlyEditor, false, "f");
|
281
|
-
this.setDisabled(false);
|
282
|
-
}
|
283
|
-
}), "f");
|
265
|
+
__classPrivateFieldGet(this, _BaseWidget_instances, "m", _BaseWidget_addEditorListeners).call(this);
|
284
266
|
parent.appendChild(this.container);
|
285
267
|
return this.container;
|
286
268
|
}
|
269
|
+
/**
|
270
|
+
* Remove this. This allows the widget to be added to a toolbar again
|
271
|
+
* in the future using {@link addTo}.
|
272
|
+
*/
|
273
|
+
remove() {
|
274
|
+
this.container.remove();
|
275
|
+
__classPrivateFieldGet(this, _BaseWidget_removeEditorListeners, "f")?.call(this);
|
276
|
+
}
|
287
277
|
focus() {
|
288
278
|
this.button.focus();
|
289
279
|
}
|
@@ -296,11 +286,6 @@ class BaseWidget {
|
|
296
286
|
removeCSSClassFromContainer(className) {
|
297
287
|
this.container.classList.remove(className);
|
298
288
|
}
|
299
|
-
remove() {
|
300
|
-
this.container.remove();
|
301
|
-
__classPrivateFieldGet(this, _BaseWidget_readOnlyListener, "f")?.remove();
|
302
|
-
__classPrivateFieldSet(this, _BaseWidget_readOnlyListener, null, "f");
|
303
|
-
}
|
304
289
|
updateIcon() {
|
305
290
|
let newIcon = this.createIcon();
|
306
291
|
if (!newIcon) {
|
@@ -437,5 +422,37 @@ class BaseWidget {
|
|
437
422
|
}
|
438
423
|
}
|
439
424
|
}
|
440
|
-
_BaseWidget_hasDropdown = new WeakMap(), _BaseWidget_disabledDueToReadOnlyEditor = new WeakMap(), _BaseWidget_tags = new WeakMap(),
|
425
|
+
_a = BaseWidget, _BaseWidget_hasDropdown = new WeakMap(), _BaseWidget_disabledDueToReadOnlyEditor = new WeakMap(), _BaseWidget_tags = new WeakMap(), _BaseWidget_removeEditorListeners = new WeakMap(), _BaseWidget_instances = new WeakSet(), _BaseWidget_addEditorListeners = function _BaseWidget_addEditorListeners() {
|
426
|
+
__classPrivateFieldGet(this, _BaseWidget_removeEditorListeners, "f")?.call(this);
|
427
|
+
const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler);
|
428
|
+
let removeKeyPressListener = null;
|
429
|
+
// If the onKeyPress function has been extended and the editor is configured to send keypress events to
|
430
|
+
// toolbar widgets,
|
431
|
+
if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== _a.prototype.onKeyPress) {
|
432
|
+
const keyPressListener = (event) => this.onKeyPress(event);
|
433
|
+
const handler = toolbarShortcutHandlers[0];
|
434
|
+
handler.registerListener(keyPressListener);
|
435
|
+
removeKeyPressListener = () => {
|
436
|
+
handler.removeListener(keyPressListener);
|
437
|
+
};
|
438
|
+
}
|
439
|
+
const readOnlyListener = this.editor.isReadOnlyReactiveValue().onUpdateAndNow(readOnly => {
|
440
|
+
if (readOnly && this.shouldAutoDisableInReadOnlyEditor() && !this.disabled) {
|
441
|
+
this.setDisabled(true);
|
442
|
+
__classPrivateFieldSet(this, _BaseWidget_disabledDueToReadOnlyEditor, true, "f");
|
443
|
+
if (__classPrivateFieldGet(this, _BaseWidget_hasDropdown, "f")) {
|
444
|
+
this.dropdown?.requestHide();
|
445
|
+
}
|
446
|
+
}
|
447
|
+
else if (!readOnly && __classPrivateFieldGet(this, _BaseWidget_disabledDueToReadOnlyEditor, "f")) {
|
448
|
+
__classPrivateFieldSet(this, _BaseWidget_disabledDueToReadOnlyEditor, false, "f");
|
449
|
+
this.setDisabled(false);
|
450
|
+
}
|
451
|
+
});
|
452
|
+
__classPrivateFieldSet(this, _BaseWidget_removeEditorListeners, () => {
|
453
|
+
readOnlyListener.remove();
|
454
|
+
removeKeyPressListener?.();
|
455
|
+
__classPrivateFieldSet(this, _BaseWidget_removeEditorListeners, null, "f");
|
456
|
+
}, "f");
|
457
|
+
};
|
441
458
|
export default BaseWidget;
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -165,6 +165,7 @@ class BaseTool {
|
|
165
165
|
onDestroy() {
|
166
166
|
__classPrivateFieldGet(this, _BaseTool_readOnlyEditorChangeListener, "f")?.remove();
|
167
167
|
__classPrivateFieldSet(this, _BaseTool_readOnlyEditorChangeListener, null, "f");
|
168
|
+
__classPrivateFieldSet(this, _BaseTool_group, null, "f");
|
168
169
|
}
|
169
170
|
}
|
170
171
|
_BaseTool_enabled = new WeakMap(), _BaseTool_group = new WeakMap(), _BaseTool_inputMapper = new WeakMap(), _BaseTool_readOnlyEditorChangeListener = new WeakMap();
|
@@ -129,10 +129,12 @@ class Selection {
|
|
129
129
|
// Reset for the next drag
|
130
130
|
this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
|
131
131
|
this.transform = Mat33.identity;
|
132
|
-
// Make the commands undo-able
|
133
|
-
|
134
|
-
|
135
|
-
|
132
|
+
// Make the commands undo-able.
|
133
|
+
// Don't check for non-empty transforms because this breaks changing the
|
134
|
+
// z-index of the just-transformed commands.
|
135
|
+
//
|
136
|
+
// TODO: Check whether the selectedElems are already all toplevel.
|
137
|
+
await this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform));
|
136
138
|
// Clear renderings of any in-progress transformations
|
137
139
|
const wetInkRenderer = this.editor.display.getWetInkRenderer();
|
138
140
|
wetInkRenderer.clear();
|
@@ -382,7 +384,7 @@ class Selection {
|
|
382
384
|
if (wasTransforming) {
|
383
385
|
// Don't update the selection's focus when redoing/undoing
|
384
386
|
const selectionToUpdate = null;
|
385
|
-
tmpApplyCommand = new
|
387
|
+
tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
|
386
388
|
// Transform to ensure that the duplicates are in the correct location
|
387
389
|
await tmpApplyCommand.apply(this.editor);
|
388
390
|
// Show items again
|