js-draw 1.4.1 → 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 +13 -12
- 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 +13 -4
- package/dist/cjs/toolbar/AbstractToolbar.js +18 -5
- package/dist/cjs/toolbar/AbstractToolbar.test.d.ts +1 -0
- package/dist/cjs/toolbar/EdgeToolbar.js +2 -2
- 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/toolbar/widgets/SaveActionWidget.d.ts +2 -1
- package/dist/cjs/toolbar/widgets/SaveActionWidget.js +6 -2
- package/dist/cjs/tools/BaseTool.js +1 -0
- package/dist/cjs/tools/SelectionTool/Selection.js +4 -2
- 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 +13 -4
- package/dist/mjs/toolbar/AbstractToolbar.mjs +18 -5
- package/dist/mjs/toolbar/AbstractToolbar.test.d.ts +1 -0
- package/dist/mjs/toolbar/EdgeToolbar.mjs +2 -2
- 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/toolbar/widgets/SaveActionWidget.d.ts +2 -1
- package/dist/mjs/toolbar/widgets/SaveActionWidget.mjs +6 -2
- package/dist/mjs/tools/BaseTool.mjs +1 -0
- package/dist/mjs/tools/SelectionTool/Selection.mjs +4 -2
- 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/src/toolbar/EdgeToolbar.scss +7 -4
- package/src/tools/SelectionTool/SelectionTool.scss +2 -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();
|
@@ -124,24 +124,30 @@ export default abstract class AbstractToolbar {
|
|
124
124
|
* toolbar.addDefaults();
|
125
125
|
* toolbar.addSaveButton(() => alert('save clicked!'));
|
126
126
|
* ```
|
127
|
+
*
|
128
|
+
* `labelOverride` can optionally be used to change the `label` or `icon` of the button.
|
127
129
|
*/
|
128
|
-
addSaveButton(saveCallback: () => void): BaseWidget;
|
130
|
+
addSaveButton(saveCallback: () => void, labelOverride?: Partial<ActionButtonIcon>): BaseWidget;
|
129
131
|
/**
|
130
132
|
* Adds an "Exit" button that, when clicked, calls `exitCallback`.
|
131
133
|
*
|
132
|
-
* **Note**: This is equivalent to
|
134
|
+
* **Note**: This is roughly equivalent to
|
133
135
|
* ```ts
|
134
136
|
* toolbar.addTaggedActionButton([ ToolbarWidgetTag.Exit ], {
|
135
137
|
* label: this.editor.localization.exit,
|
136
138
|
* icon: this.editor.icons.makeCloseIcon(),
|
139
|
+
*
|
140
|
+
* // labelOverride can be used to override label or icon.
|
141
|
+
* ...labelOverride,
|
137
142
|
* }, () => {
|
138
143
|
* exitCallback();
|
139
144
|
* });
|
140
145
|
* ```
|
146
|
+
* with some additional configuration.
|
141
147
|
*
|
142
148
|
* @final
|
143
149
|
*/
|
144
|
-
addExitButton(exitCallback: () => void): BaseWidget;
|
150
|
+
addExitButton(exitCallback: () => void, labelOverride?: Partial<ActionButtonIcon>): BaseWidget;
|
145
151
|
/**
|
146
152
|
* Adds undo and redo buttons that trigger the editor's built-in undo and redo
|
147
153
|
* functionality.
|
@@ -156,7 +162,10 @@ export default abstract class AbstractToolbar {
|
|
156
162
|
* Adds both the default tool widgets and action buttons.
|
157
163
|
*/
|
158
164
|
abstract addDefaults(): void;
|
159
|
-
/**
|
165
|
+
/**
|
166
|
+
* Remove this toolbar from its container and clean up listeners.
|
167
|
+
* This should only be called **once** for a given toolbar.
|
168
|
+
*/
|
160
169
|
remove(): void;
|
161
170
|
/**
|
162
171
|
* Removes `listener` when {@link remove} is called.
|
@@ -287,9 +287,11 @@ class AbstractToolbar {
|
|
287
287
|
* toolbar.addDefaults();
|
288
288
|
* toolbar.addSaveButton(() => alert('save clicked!'));
|
289
289
|
* ```
|
290
|
+
*
|
291
|
+
* `labelOverride` can optionally be used to change the `label` or `icon` of the button.
|
290
292
|
*/
|
291
|
-
addSaveButton(saveCallback) {
|
292
|
-
const widget = new SaveActionWidget(this.editor, this.localizationTable, saveCallback);
|
293
|
+
addSaveButton(saveCallback, labelOverride = {}) {
|
294
|
+
const widget = new SaveActionWidget(this.editor, this.localizationTable, saveCallback, labelOverride);
|
293
295
|
widget.setTags([ToolbarWidgetTag.Save]);
|
294
296
|
this.addWidget(widget);
|
295
297
|
return widget;
|
@@ -297,22 +299,27 @@ class AbstractToolbar {
|
|
297
299
|
/**
|
298
300
|
* Adds an "Exit" button that, when clicked, calls `exitCallback`.
|
299
301
|
*
|
300
|
-
* **Note**: This is equivalent to
|
302
|
+
* **Note**: This is roughly equivalent to
|
301
303
|
* ```ts
|
302
304
|
* toolbar.addTaggedActionButton([ ToolbarWidgetTag.Exit ], {
|
303
305
|
* label: this.editor.localization.exit,
|
304
306
|
* icon: this.editor.icons.makeCloseIcon(),
|
307
|
+
*
|
308
|
+
* // labelOverride can be used to override label or icon.
|
309
|
+
* ...labelOverride,
|
305
310
|
* }, () => {
|
306
311
|
* exitCallback();
|
307
312
|
* });
|
308
313
|
* ```
|
314
|
+
* with some additional configuration.
|
309
315
|
*
|
310
316
|
* @final
|
311
317
|
*/
|
312
|
-
addExitButton(exitCallback) {
|
318
|
+
addExitButton(exitCallback, labelOverride = {}) {
|
313
319
|
return this.addTaggedActionButton([ToolbarWidgetTag.Exit], {
|
314
320
|
label: this.editor.localization.exit,
|
315
321
|
icon: this.editor.icons.makeCloseIcon(),
|
322
|
+
...labelOverride,
|
316
323
|
}, () => {
|
317
324
|
exitCallback();
|
318
325
|
}, {
|
@@ -392,7 +399,10 @@ class AbstractToolbar {
|
|
392
399
|
addDefaultActionButtons() {
|
393
400
|
this.addUndoRedoButtons();
|
394
401
|
}
|
395
|
-
/**
|
402
|
+
/**
|
403
|
+
* Remove this toolbar from its container and clean up listeners.
|
404
|
+
* This should only be called **once** for a given toolbar.
|
405
|
+
*/
|
396
406
|
remove() {
|
397
407
|
this.closeColorPickerOverlay?.remove();
|
398
408
|
for (const listener of __classPrivateFieldGet(this, _AbstractToolbar_listeners, "f")) {
|
@@ -400,6 +410,9 @@ class AbstractToolbar {
|
|
400
410
|
}
|
401
411
|
__classPrivateFieldSet(this, _AbstractToolbar_listeners, [], "f");
|
402
412
|
this.onRemove();
|
413
|
+
for (const widget of __classPrivateFieldGet(this, _AbstractToolbar_widgetList, "f")) {
|
414
|
+
widget.remove();
|
415
|
+
}
|
403
416
|
}
|
404
417
|
/**
|
405
418
|
* Removes `listener` when {@link remove} is called.
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -228,11 +228,11 @@ export default class EdgeToolbar extends AbstractToolbar {
|
|
228
228
|
widget.removeCSSClassFromContainer('label-right');
|
229
229
|
if (tags.includes(ToolbarWidgetTag.Save)) {
|
230
230
|
widget.addCSSClassToContainer('label-inline');
|
231
|
-
widget.addCSSClassToContainer('label-
|
231
|
+
widget.addCSSClassToContainer('label-left');
|
232
232
|
}
|
233
233
|
if (tags.includes(ToolbarWidgetTag.Exit)) {
|
234
234
|
widget.addCSSClassToContainer('label-inline');
|
235
|
-
widget.addCSSClassToContainer('label-
|
235
|
+
widget.addCSSClassToContainer('label-right');
|
236
236
|
}
|
237
237
|
}
|
238
238
|
addWidgetInternal(widget) {
|
@@ -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 {};
|
@@ -2,8 +2,9 @@ import { KeyPressEvent } from '../../inputEvents';
|
|
2
2
|
import Editor from '../../Editor';
|
3
3
|
import { ToolbarLocalization } from '../localization';
|
4
4
|
import ActionButtonWidget from './ActionButtonWidget';
|
5
|
+
import { ActionButtonIcon } from '../types';
|
5
6
|
declare class SaveActionWidget extends ActionButtonWidget {
|
6
|
-
constructor(editor: Editor, localization: ToolbarLocalization, saveCallback: () => void);
|
7
|
+
constructor(editor: Editor, localization: ToolbarLocalization, saveCallback: () => void, labelOverride?: Partial<ActionButtonIcon>);
|
7
8
|
protected shouldAutoDisableInReadOnlyEditor(): boolean;
|
8
9
|
protected onKeyPress(event: KeyPressEvent): boolean;
|
9
10
|
mustBeInToplevelMenu(): boolean;
|