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.
Files changed (63) hide show
  1. package/README.md +10 -1
  2. package/dist/Editor.css +13 -12
  3. package/dist/bundle.js +2 -2
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +8 -0
  6. package/dist/cjs/Editor.js +32 -9
  7. package/dist/cjs/SVGLoader.d.ts +6 -0
  8. package/dist/cjs/SVGLoader.js +100 -56
  9. package/dist/cjs/commands/Erase.d.ts +7 -1
  10. package/dist/cjs/commands/Erase.js +13 -4
  11. package/dist/cjs/commands/invertCommand.js +1 -1
  12. package/dist/cjs/components/Stroke.d.ts +1 -1
  13. package/dist/cjs/components/Stroke.js +1 -1
  14. package/dist/cjs/image/EditorImage.js +1 -1
  15. package/dist/cjs/toolbar/AbstractToolbar.d.ts +13 -4
  16. package/dist/cjs/toolbar/AbstractToolbar.js +18 -5
  17. package/dist/cjs/toolbar/AbstractToolbar.test.d.ts +1 -0
  18. package/dist/cjs/toolbar/EdgeToolbar.js +2 -2
  19. package/dist/cjs/toolbar/widgets/BaseToolWidget.js +4 -13
  20. package/dist/cjs/toolbar/widgets/BaseToolWidget.test.d.ts +1 -0
  21. package/dist/cjs/toolbar/widgets/BaseWidget.d.ts +5 -1
  22. package/dist/cjs/toolbar/widgets/BaseWidget.js +45 -28
  23. package/dist/cjs/toolbar/widgets/BaseWidget.test.d.ts +1 -0
  24. package/dist/cjs/toolbar/widgets/SaveActionWidget.d.ts +2 -1
  25. package/dist/cjs/toolbar/widgets/SaveActionWidget.js +6 -2
  26. package/dist/cjs/tools/BaseTool.js +1 -0
  27. package/dist/cjs/tools/SelectionTool/Selection.js +4 -2
  28. package/dist/cjs/tools/ToolController.d.ts +23 -0
  29. package/dist/cjs/tools/ToolController.js +65 -4
  30. package/dist/cjs/tools/ToolController.test.d.ts +1 -0
  31. package/dist/cjs/version.js +1 -1
  32. package/dist/mjs/Editor.d.ts +8 -0
  33. package/dist/mjs/Editor.mjs +32 -9
  34. package/dist/mjs/SVGLoader.d.ts +6 -0
  35. package/dist/mjs/SVGLoader.mjs +99 -55
  36. package/dist/mjs/commands/Erase.d.ts +7 -1
  37. package/dist/mjs/commands/Erase.mjs +13 -4
  38. package/dist/mjs/commands/invertCommand.mjs +1 -1
  39. package/dist/mjs/components/Stroke.d.ts +1 -1
  40. package/dist/mjs/components/Stroke.mjs +1 -1
  41. package/dist/mjs/image/EditorImage.mjs +1 -1
  42. package/dist/mjs/toolbar/AbstractToolbar.d.ts +13 -4
  43. package/dist/mjs/toolbar/AbstractToolbar.mjs +18 -5
  44. package/dist/mjs/toolbar/AbstractToolbar.test.d.ts +1 -0
  45. package/dist/mjs/toolbar/EdgeToolbar.mjs +2 -2
  46. package/dist/mjs/toolbar/widgets/BaseToolWidget.mjs +4 -13
  47. package/dist/mjs/toolbar/widgets/BaseToolWidget.test.d.ts +1 -0
  48. package/dist/mjs/toolbar/widgets/BaseWidget.d.ts +5 -1
  49. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +45 -28
  50. package/dist/mjs/toolbar/widgets/BaseWidget.test.d.ts +1 -0
  51. package/dist/mjs/toolbar/widgets/SaveActionWidget.d.ts +2 -1
  52. package/dist/mjs/toolbar/widgets/SaveActionWidget.mjs +6 -2
  53. package/dist/mjs/tools/BaseTool.mjs +1 -0
  54. package/dist/mjs/tools/SelectionTool/Selection.mjs +4 -2
  55. package/dist/mjs/tools/ToolController.d.ts +23 -0
  56. package/dist/mjs/tools/ToolController.mjs +65 -4
  57. package/dist/mjs/tools/ToolController.test.d.ts +1 -0
  58. package/dist/mjs/version.mjs +1 -1
  59. package/docs/img/readme-images/logo.svg +1 -0
  60. package/package.json +3 -3
  61. package/src/Editor.scss +4 -2
  62. package/src/toolbar/EdgeToolbar.scss +7 -4
  63. package/src/tools/SelectionTool/SelectionTool.scss +2 -2
@@ -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.fill;
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.stroke;
52
- const strokeWidthAttr = node.getAttribute('stroke-width') ?? computedStyles?.strokeWidth ?? node.style.strokeWidth;
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 ??= window.getComputedStyle(elem);
229
- let transformProperty = computedStyles.transform;
230
- if (transformProperty === '' || transformProperty === 'none') {
231
- transformProperty = elem.style.transform || 'none';
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 = window.getComputedStyle(elem);
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.fontSize);
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.fontSize);
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.fontFamily || elem.style.fontFamily || 'sans-serif',
308
- fontWeight: computedStyles.fontWeight || elem.style.fontWeight || undefined,
309
- fontStyle: computedStyles.fontStyle || elem.style.fontStyle || undefined,
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 sandbox = document.createElement('iframe');
517
- sandbox.src = 'about:blank';
518
- sandbox.setAttribute('sandbox', 'allow-same-origin');
519
- sandbox.setAttribute('csp', 'default-src \'about:blank\'');
520
- sandbox.style.display = 'none';
521
- // Required to access the frame's DOM. See https://stackoverflow.com/a/17777943/17055750
522
- document.body.appendChild(sandbox);
523
- if (!sandbox.hasAttribute('sandbox')) {
524
- sandbox.remove();
525
- throw new Error('SVG loading iframe is not sandboxed.');
526
- }
527
- const sandboxDoc = sandbox.contentWindow?.document ?? sandbox.contentDocument;
528
- if (sandboxDoc == null)
529
- throw new Error('Unable to open a sandboxed iframe!');
530
- sandboxDoc.open();
531
- sandboxDoc.write(`
532
- <!DOCTYPE html>
533
- <html>
534
- <head>
535
- <title>SVG Loading Sandbox</title>
536
- <meta name='viewport' conent='width=device-width,initial-scale=1.0'/>
537
- <meta charset='utf-8'/>
538
- </head>
539
- <body style='font-size: 12px;'>
540
- <script>
541
- console.error('JavaScript should not be able to run here!');
542
- throw new Error(
543
- 'The SVG sandbox is broken! Please double-check the sandboxing setting.'
544
- );
545
- </script>
546
- </body>
547
- </html>
548
- `);
549
- sandboxDoc.close();
550
- const svgElem = sandboxDoc.createElementNS('http://www.w3.org/2000/svg', 'svg');
551
- svgElem.innerHTML = text;
552
- sandboxDoc.body.appendChild(svgElem);
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(): string[];
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
- const elemIds = this.toRemove.map(elem => elem.getId());
67
- return elemIds;
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((elemId) => editor.image.lookupElement(elemId))
74
- .filter((elem) => elem !== null);
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
  })();
@@ -13,7 +13,7 @@ const invertCommand = (command) => {
13
13
  command.unapply(editor);
14
14
  }
15
15
  unapply(editor) {
16
- command.unapply(editor);
16
+ command.apply(editor);
17
17
  }
18
18
  onDrop(editor) {
19
19
  command.onDrop(editor);
@@ -39,7 +39,7 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
39
39
  *
40
40
  * const stroke = new Stroke([
41
41
  * // Fill with red
42
- * path.toRenderable({ fill: Color4.red })
42
+ * pathToRenderable({ fill: Color4.red })
43
43
  * ]);
44
44
  * ```
45
45
  */
@@ -32,7 +32,7 @@ export default class Stroke extends AbstractComponent {
32
32
  *
33
33
  * const stroke = new Stroke([
34
34
  * // Fill with red
35
- * path.toRenderable({ fill: Color4.red })
35
+ * pathToRenderable({ fill: Color4.red })
36
36
  * ]);
37
37
  * ```
38
38
  */
@@ -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
- /** Remove this toolbar from its container and clean up listeners. */
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
- /** Remove this toolbar from its container and clean up listeners. */
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-right');
231
+ widget.addCSSClassToContainer('label-left');
232
232
  }
233
233
  if (tags.includes(ToolbarWidgetTag.Exit)) {
234
234
  widget.addCSSClassToContainer('label-inline');
235
- widget.addCSSClassToContainer('label-left');
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
- editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
13
- if (toolEvt.kind !== EditorEventType.ToolEnabled) {
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.key === ' ' && this.hasDropdown) {
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, _BaseWidget_readOnlyListener;
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
- // Listens for changes in whether the editor is read-only
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
- __classPrivateFieldSet(this, _BaseWidget_readOnlyListener, this.editor.isReadOnlyReactiveValue().onUpdateAndNow(readOnly => {
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(), _BaseWidget_readOnlyListener = 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;