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.
Files changed (51) hide show
  1. package/README.md +10 -1
  2. package/dist/Editor.css +2 -2
  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 +4 -1
  16. package/dist/cjs/toolbar/AbstractToolbar.js +7 -1
  17. package/dist/cjs/toolbar/widgets/BaseToolWidget.js +4 -13
  18. package/dist/cjs/toolbar/widgets/BaseToolWidget.test.d.ts +1 -0
  19. package/dist/cjs/toolbar/widgets/BaseWidget.d.ts +5 -1
  20. package/dist/cjs/toolbar/widgets/BaseWidget.js +45 -28
  21. package/dist/cjs/toolbar/widgets/BaseWidget.test.d.ts +1 -0
  22. package/dist/cjs/tools/BaseTool.js +1 -0
  23. package/dist/cjs/tools/ToolController.d.ts +23 -0
  24. package/dist/cjs/tools/ToolController.js +65 -4
  25. package/dist/cjs/tools/ToolController.test.d.ts +1 -0
  26. package/dist/cjs/version.js +1 -1
  27. package/dist/mjs/Editor.d.ts +8 -0
  28. package/dist/mjs/Editor.mjs +32 -9
  29. package/dist/mjs/SVGLoader.d.ts +6 -0
  30. package/dist/mjs/SVGLoader.mjs +99 -55
  31. package/dist/mjs/commands/Erase.d.ts +7 -1
  32. package/dist/mjs/commands/Erase.mjs +13 -4
  33. package/dist/mjs/commands/invertCommand.mjs +1 -1
  34. package/dist/mjs/components/Stroke.d.ts +1 -1
  35. package/dist/mjs/components/Stroke.mjs +1 -1
  36. package/dist/mjs/image/EditorImage.mjs +1 -1
  37. package/dist/mjs/toolbar/AbstractToolbar.d.ts +4 -1
  38. package/dist/mjs/toolbar/AbstractToolbar.mjs +7 -1
  39. package/dist/mjs/toolbar/widgets/BaseToolWidget.mjs +4 -13
  40. package/dist/mjs/toolbar/widgets/BaseToolWidget.test.d.ts +1 -0
  41. package/dist/mjs/toolbar/widgets/BaseWidget.d.ts +5 -1
  42. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +45 -28
  43. package/dist/mjs/toolbar/widgets/BaseWidget.test.d.ts +1 -0
  44. package/dist/mjs/tools/BaseTool.mjs +1 -0
  45. package/dist/mjs/tools/ToolController.d.ts +23 -0
  46. package/dist/mjs/tools/ToolController.mjs +65 -4
  47. package/dist/mjs/tools/ToolController.test.d.ts +1 -0
  48. package/dist/mjs/version.mjs +1 -1
  49. package/docs/img/readme-images/logo.svg +1 -0
  50. package/package.json +3 -3
  51. package/src/Editor.scss +4 -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();
@@ -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
- /** 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
+ */
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
- /** 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
+ */
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
- 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 {};
@@ -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;