js-draw 1.5.0 → 1.6.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/README.md +10 -1
- 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 +1 -1
- 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/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 +1 -1
- 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/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/logo.svg +1 -0
- package/package.json +3 -3
- 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();
|
@@ -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, _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
|
+
_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 !== BaseWidget.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();
|
@@ -13,9 +13,32 @@ export default class ToolController implements InputEventListener {
|
|
13
13
|
/** @internal */
|
14
14
|
constructor(editor: Editor, localization: ToolLocalization);
|
15
15
|
setTools(tools: BaseTool[], primaryToolGroup?: ToolEnabledGroup): void;
|
16
|
+
/**
|
17
|
+
* Add a tool that acts like one of the primary tools (only one primary tool can be enabled at a time).
|
18
|
+
*
|
19
|
+
* If the tool is already added to this, the tool is converted to a primary tool.
|
20
|
+
*
|
21
|
+
* This should be called before creating the app's toolbar.
|
22
|
+
*/
|
16
23
|
addPrimaryTool(tool: BaseTool): void;
|
17
24
|
getPrimaryTools(): BaseTool[];
|
18
25
|
addTool(tool: BaseTool): void;
|
26
|
+
/**
|
27
|
+
* Removes **and destroys** all tools in `tools` from this.
|
28
|
+
*/
|
29
|
+
removeAndDestroyTools(tools: BaseTool[]): void;
|
30
|
+
private insertTools;
|
31
|
+
/**
|
32
|
+
* Removes a tool from this' tool list and replaces it with `replaceWith`.
|
33
|
+
*
|
34
|
+
* If any of `toolsToInsert` have already been added to this, the tools are
|
35
|
+
* moved.
|
36
|
+
*
|
37
|
+
* This should be called before creating the editor's toolbar.
|
38
|
+
*/
|
39
|
+
insertToolsAfter(insertAfter: BaseTool, toolsToInsert: BaseTool[]): void;
|
40
|
+
/** @see {@link insertToolsAfter} */
|
41
|
+
insertToolsBefore(insertBefore: BaseTool, toolsToInsert: BaseTool[]): void;
|
19
42
|
private onEventInternal;
|
20
43
|
/** Alias for {@link dispatchInputEvent}. */
|
21
44
|
onEvent(event: InputEvt): boolean;
|