js-draw 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Color4.js +3 -0
  4. package/dist/src/Editor.d.ts +2 -0
  5. package/dist/src/Editor.js +31 -6
  6. package/dist/src/SVGLoader.js +5 -7
  7. package/dist/src/Viewport.js +2 -2
  8. package/dist/src/components/Stroke.js +2 -2
  9. package/dist/src/components/builders/LineBuilder.js +4 -0
  10. package/dist/src/components/util/StrokeSmoother.js +1 -1
  11. package/dist/src/math/Path.js +6 -1
  12. package/dist/src/rendering/renderers/SVGRenderer.js +6 -1
  13. package/dist/src/toolbar/HTMLToolbar.d.ts +3 -0
  14. package/dist/src/toolbar/HTMLToolbar.js +24 -0
  15. package/dist/src/toolbar/IconProvider.d.ts +1 -0
  16. package/dist/src/toolbar/IconProvider.js +43 -1
  17. package/dist/src/toolbar/localization.d.ts +1 -0
  18. package/dist/src/toolbar/localization.js +1 -0
  19. package/dist/src/toolbar/makeColorInput.d.ts +2 -1
  20. package/dist/src/toolbar/makeColorInput.js +13 -2
  21. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +1 -1
  22. package/dist/src/toolbar/widgets/ActionButtonWidget.js +2 -2
  23. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -2
  24. package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -3
  25. package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -2
  26. package/dist/src/toolbar/widgets/BaseWidget.js +67 -6
  27. package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +4 -0
  28. package/dist/src/toolbar/widgets/EraserToolWidget.js +3 -0
  29. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +3 -1
  30. package/dist/src/toolbar/widgets/HandToolWidget.js +22 -13
  31. package/dist/src/toolbar/widgets/PenToolWidget.d.ts +7 -1
  32. package/dist/src/toolbar/widgets/PenToolWidget.js +78 -12
  33. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
  34. package/dist/src/toolbar/widgets/SelectionToolWidget.js +7 -7
  35. package/dist/src/toolbar/widgets/TextToolWidget.d.ts +4 -1
  36. package/dist/src/toolbar/widgets/TextToolWidget.js +17 -3
  37. package/dist/src/tools/PanZoom.d.ts +4 -1
  38. package/dist/src/tools/PanZoom.js +24 -1
  39. package/dist/src/tools/SelectionTool/Selection.js +1 -1
  40. package/package.json +1 -1
  41. package/src/Color4.ts +2 -0
  42. package/src/Editor.ts +43 -9
  43. package/src/SVGLoader.ts +8 -8
  44. package/src/Viewport.ts +2 -2
  45. package/src/components/Stroke.ts +1 -1
  46. package/src/components/builders/LineBuilder.ts +4 -0
  47. package/src/components/util/StrokeSmoother.ts +1 -1
  48. package/src/math/Path.test.ts +24 -0
  49. package/src/math/Path.ts +7 -1
  50. package/src/rendering/renderers/SVGRenderer.ts +5 -1
  51. package/src/toolbar/HTMLToolbar.ts +33 -0
  52. package/src/toolbar/IconProvider.ts +49 -1
  53. package/src/toolbar/localization.ts +2 -0
  54. package/src/toolbar/makeColorInput.ts +21 -3
  55. package/src/toolbar/widgets/ActionButtonWidget.ts +6 -3
  56. package/src/toolbar/widgets/BaseToolWidget.ts +4 -3
  57. package/src/toolbar/widgets/BaseWidget.ts +83 -5
  58. package/src/toolbar/widgets/EraserToolWidget.ts +11 -0
  59. package/src/toolbar/widgets/HandToolWidget.ts +48 -17
  60. package/src/toolbar/widgets/PenToolWidget.ts +105 -13
  61. package/src/toolbar/widgets/SelectionToolWidget.ts +8 -5
  62. package/src/toolbar/widgets/TextToolWidget.ts +29 -4
  63. package/src/tools/PanZoom.ts +28 -1
  64. package/src/tools/SelectionTool/Selection.ts +1 -1
  65. package/.firebase/hosting.ZG9jcw.cache +0 -338
@@ -63,6 +63,9 @@ export default class Color4 {
63
63
  if (text.startsWith('#')) {
64
64
  return Color4.fromHex(text);
65
65
  }
66
+ else if (text === 'none' || text === 'transparent') {
67
+ return Color4.transparent;
68
+ }
66
69
  else {
67
70
  // Otherwise, try to use an HTML5Canvas to determine the color
68
71
  const canvas = document.createElement('canvas');
@@ -197,7 +197,9 @@ export declare class Editor {
197
197
  addStyleSheet(content: string): HTMLStyleElement;
198
198
  sendKeyboardEvent(eventType: InputEvtType.KeyPressEvent | InputEvtType.KeyUpEvent, key: string, ctrlKey?: boolean, altKey?: boolean): void;
199
199
  sendPenEvent(eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, point: Point2, allPointers?: Pointer[]): void;
200
+ toDataURL(format?: 'image/png' | 'image/jpeg' | 'image/webp'): string;
200
201
  toSVG(): SVGElement;
202
+ private renderAllWithTransform;
201
203
  loadFrom(loader: ImageLoader): Promise<void>;
202
204
  getImportExportRect(): Rect2;
203
205
  setImportExportRect(imageRect: Rect2): Command;
@@ -43,6 +43,7 @@ import Mat33 from './math/Mat33';
43
43
  import getLocalizationTable from './localizations/getLocalizationTable';
44
44
  import IconProvider from './toolbar/IconProvider';
45
45
  import { toRoundedString } from './math/rounding';
46
+ import CanvasRenderer from './rendering/renderers/CanvasRenderer';
46
47
  // { @inheritDoc Editor! }
47
48
  export class Editor {
48
49
  /**
@@ -626,17 +627,29 @@ export class Editor {
626
627
  current: mainPointer,
627
628
  });
628
629
  }
630
+ // Get a data URL (e.g. as produced by `HTMLCanvasElement::toDataURL`).
631
+ // If `format` is not `image/png`, a PNG image URL may still be returned (as in the
632
+ // case of `HTMLCanvasElement::toDataURL`).
633
+ //
634
+ // The export resolution is the same as the size of the drawing canvas.
635
+ toDataURL(format = 'image/png') {
636
+ const canvas = document.createElement('canvas');
637
+ const resolution = this.importExportViewport.getResolution();
638
+ canvas.width = resolution.x;
639
+ canvas.height = resolution.y;
640
+ const ctx = canvas.getContext('2d');
641
+ const renderer = new CanvasRenderer(ctx, this.importExportViewport);
642
+ // Render everything with no transform (0,0) should be (0,0) in the output image
643
+ this.renderAllWithTransform(renderer, this.importExportViewport, Mat33.identity);
644
+ const dataURL = canvas.toDataURL(format);
645
+ return dataURL;
646
+ }
629
647
  toSVG() {
630
648
  const importExportViewport = this.importExportViewport;
631
649
  const svgNameSpace = 'http://www.w3.org/2000/svg';
632
650
  const result = document.createElementNS(svgNameSpace, 'svg');
633
651
  const renderer = new SVGRenderer(result, importExportViewport);
634
- const origTransform = importExportViewport.canvasToScreenTransform;
635
- // Reset the transform to ensure that (0, 0) is (0, 0)
636
- importExportViewport.resetTransform(Mat33.identity);
637
- // Render **all** elements.
638
- this.image.renderAll(renderer);
639
- importExportViewport.resetTransform(origTransform);
652
+ this.renderAllWithTransform(renderer, importExportViewport);
640
653
  // Just show the main region
641
654
  const rect = importExportViewport.visibleRect;
642
655
  result.setAttribute('viewBox', [rect.x, rect.y, rect.w, rect.h].map(part => toRoundedString(part)).join(' '));
@@ -649,6 +662,18 @@ export class Editor {
649
662
  result.setAttribute('xmlns', svgNameSpace);
650
663
  return result;
651
664
  }
665
+ // Renders everything in this' image to `renderer`, but first transforming the given `viewport`
666
+ // such that its transform is `transform`. The given `viewport`'s transform is restored before this method
667
+ // returns.
668
+ //
669
+ // For example, rendering with `transform = Mat33.identity` *sets* `viewport`'s transform to `Mat33.identity`,
670
+ // renders everything in this' image to `renderer`, then restores `viewport`'s transform to whatever it was before.
671
+ renderAllWithTransform(renderer, viewport, transform = Mat33.identity) {
672
+ const origTransform = this.importExportViewport.canvasToScreenTransform;
673
+ viewport.resetTransform(transform);
674
+ this.image.renderAll(renderer);
675
+ viewport.resetTransform(origTransform);
676
+ }
652
677
  loadFrom(loader) {
653
678
  return __awaiter(this, void 0, void 0, function* () {
654
679
  this.showLoadingWarning(0);
@@ -22,6 +22,7 @@ export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
22
22
  // Key to retrieve unrecognised attributes from an AbstractComponent
23
23
  export const svgAttributesDataKey = 'svgAttrs';
24
24
  export const svgStyleAttributesDataKey = 'svgStyleAttrs';
25
+ const supportedStrokeFillStyleAttrs = ['stroke', 'fill', 'stroke-width'];
25
26
  export default class SVGLoader {
26
27
  constructor(source, onFinish, storeUnknown = true) {
27
28
  this.source = source;
@@ -123,8 +124,7 @@ export default class SVGLoader {
123
124
  try {
124
125
  const strokeData = this.strokeDataFromElem(node);
125
126
  elem = new Stroke(strokeData);
126
- const supportedStyleAttrs = ['stroke', 'fill', 'stroke-width'];
127
- this.attachUnrecognisedAttrs(elem, node, new Set([...supportedStyleAttrs, 'd']), new Set(supportedStyleAttrs));
127
+ this.attachUnrecognisedAttrs(elem, node, new Set([...supportedStrokeFillStyleAttrs, 'd']), new Set(supportedStrokeFillStyleAttrs));
128
128
  }
129
129
  catch (e) {
130
130
  console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
@@ -193,8 +193,8 @@ export default class SVGLoader {
193
193
  const fontSizeMatch = /^([-0-9.e]+)px/i.exec(computedStyles.fontSize);
194
194
  const supportedStyleAttrs = [
195
195
  'fontFamily',
196
- 'fill',
197
- 'transform'
196
+ 'transform',
197
+ ...supportedStrokeFillStyleAttrs,
198
198
  ];
199
199
  let fontSize = 12;
200
200
  if (fontSizeMatch) {
@@ -204,9 +204,7 @@ export default class SVGLoader {
204
204
  const style = {
205
205
  size: fontSize,
206
206
  fontFamily: computedStyles.fontFamily || elem.style.fontFamily || 'sans-serif',
207
- renderingStyle: {
208
- fill: Color4.fromString(computedStyles.fill || elem.style.fill || '#000')
209
- },
207
+ renderingStyle: this.getStyle(elem),
210
208
  };
211
209
  const supportedAttrs = [];
212
210
  const transform = this.getTransform(elem, supportedAttrs, computedStyles);
@@ -103,8 +103,8 @@ export class Viewport {
103
103
  }
104
104
  // Represent as k 10ⁿ for some n, k ∈ ℤ.
105
105
  const decimalComponent = Math.pow(10, Math.floor(Math.log10(Math.abs(scaleRatio))));
106
- const roundAnountFactor = Math.pow(2, roundAmount);
107
- scaleRatio = Math.round(scaleRatio / decimalComponent * roundAnountFactor) / roundAnountFactor * decimalComponent;
106
+ const roundAmountFactor = Math.pow(2, roundAmount);
107
+ scaleRatio = Math.round(scaleRatio / decimalComponent * roundAmountFactor) / roundAmountFactor * decimalComponent;
108
108
  return scaleRatio;
109
109
  }
110
110
  // Computes and returns an affine transformation that makes `toMakeVisible` visible and roughly centered on the screen.
@@ -36,7 +36,7 @@ export default class Stroke extends AbstractComponent {
36
36
  return false;
37
37
  }
38
38
  render(canvas, visibleRect) {
39
- var _a;
39
+ var _a, _b;
40
40
  canvas.startObject(this.getBBox());
41
41
  for (const part of this.parts) {
42
42
  const bbox = this.bboxForPart(part.path.bbox, part.style);
@@ -45,7 +45,7 @@ export default class Stroke extends AbstractComponent {
45
45
  continue;
46
46
  }
47
47
  const muchBiggerThanVisible = bbox.size.x > visibleRect.size.x * 2 || bbox.size.y > visibleRect.size.y * 2;
48
- if (muchBiggerThanVisible && !part.path.roughlyIntersects(visibleRect, (_a = part.style.stroke) === null || _a === void 0 ? void 0 : _a.width)) {
48
+ if (muchBiggerThanVisible && !part.path.roughlyIntersects(visibleRect, (_b = (_a = part.style.stroke) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : 0)) {
49
49
  continue;
50
50
  }
51
51
  }
@@ -37,6 +37,10 @@ export default class LineBuilder {
37
37
  kind: PathCommandType.LineTo,
38
38
  point: endPoint.minus(scaledEndNormal),
39
39
  },
40
+ {
41
+ kind: PathCommandType.LineTo,
42
+ point: startPoint.minus(scaledStartNormal),
43
+ },
40
44
  ],
41
45
  style: {
42
46
  fill: this.startPoint.color,
@@ -167,7 +167,7 @@ export class StrokeSmoother {
167
167
  if (!controlPoint || segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
168
168
  // Position the control point closer to the first -- the connecting
169
169
  // segment will be roughly a line.
170
- controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 3));
170
+ controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 4));
171
171
  }
172
172
  console.assert(!segmentStart.eq(controlPoint, 1e-11), 'Start and control points are equal!');
173
173
  console.assert(!controlPoint.eq(segmentEnd, 1e-11), 'Control and end points are equal!');
@@ -212,7 +212,7 @@ export default class Path {
212
212
  return rect.containsPoint(this.startPoint);
213
213
  }
214
214
  const isClosed = this.startPoint.eq(this.getEndPoint());
215
- if (isClosed && strokeWidth == 0) {
215
+ if (isClosed && strokeWidth === 0) {
216
216
  return this.closedRoughlyIntersects(rect);
217
217
  }
218
218
  if (rect.containsRect(this.bbox)) {
@@ -305,6 +305,11 @@ export default class Path {
305
305
  point: corner,
306
306
  });
307
307
  }
308
+ // Close the shape
309
+ commands.push({
310
+ kind: PathCommandType.LineTo,
311
+ point: startPoint,
312
+ });
308
313
  return new Path(startPoint, commands);
309
314
  }
310
315
  static fromRenderable(renderable) {
@@ -79,7 +79,12 @@ export default class SVGRenderer extends AbstractRenderer {
79
79
  const pathElem = document.createElementNS(svgNameSpace, 'path');
80
80
  pathElem.setAttribute('d', this.lastPathString.join(' '));
81
81
  const style = this.lastPathStyle;
82
- pathElem.setAttribute('fill', style.fill.toHexString());
82
+ if (style.fill.a > 0) {
83
+ pathElem.setAttribute('fill', style.fill.toHexString());
84
+ }
85
+ else {
86
+ pathElem.setAttribute('fill', 'none');
87
+ }
83
88
  if (style.stroke) {
84
89
  pathElem.setAttribute('stroke', style.stroke.color.toHexString());
85
90
  pathElem.setAttribute('stroke-width', style.stroke.width.toString());
@@ -7,12 +7,15 @@ export default class HTMLToolbar {
7
7
  private editor;
8
8
  private localizationTable;
9
9
  private container;
10
+ private widgets;
10
11
  private static colorisStarted;
11
12
  private updateColoris;
12
13
  /** @internal */
13
14
  constructor(editor: Editor, parent: HTMLElement, localizationTable?: ToolbarLocalization);
14
15
  setupColorPickers(): void;
15
16
  addWidget(widget: BaseWidget): void;
17
+ serializeState(): string;
18
+ deserializeState(state: string): void;
16
19
  addActionButton(title: string | ActionButtonIcon, command: () => void, parent?: Element): HTMLButtonElement;
17
20
  addUndoRedoButtons(): void;
18
21
  addDefaultToolWidgets(): void;
@@ -18,6 +18,7 @@ export default class HTMLToolbar {
18
18
  constructor(editor, parent, localizationTable = defaultToolbarLocalization) {
19
19
  this.editor = editor;
20
20
  this.localizationTable = localizationTable;
21
+ this.widgets = {};
21
22
  this.updateColoris = null;
22
23
  this.container = document.createElement('div');
23
24
  this.container.classList.add(`${toolbarCSSPrefix}root`);
@@ -95,9 +96,32 @@ export default class HTMLToolbar {
95
96
  // Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
96
97
  // (i.e. its `addTo` method should not have been called).
97
98
  addWidget(widget) {
99
+ // Prevent name collisions
100
+ const id = widget.getUniqueIdIn(this.widgets);
101
+ // Add the widget
102
+ this.widgets[id] = widget;
103
+ // Add HTML elements.
98
104
  widget.addTo(this.container);
99
105
  this.setupColorPickers();
100
106
  }
107
+ serializeState() {
108
+ const result = {};
109
+ for (const widgetId in this.widgets) {
110
+ result[widgetId] = this.widgets[widgetId].serializeState();
111
+ }
112
+ return JSON.stringify(result);
113
+ }
114
+ // Deserialize toolbar widgets from the given state.
115
+ // Assumes that toolbar widgets are in the same order as when state was serialized.
116
+ deserializeState(state) {
117
+ const data = JSON.parse(state);
118
+ for (const widgetId in data) {
119
+ if (!(widgetId in this.widgets)) {
120
+ console.warn(`Unable to deserialize widget ${widgetId} ­— no such widget.`);
121
+ }
122
+ this.widgets[widgetId].deserializeFrom(data[widgetId]);
123
+ }
124
+ }
101
125
  addActionButton(title, command, parent) {
102
126
  const button = document.createElement('button');
103
127
  button.classList.add(`${toolbarCSSPrefix}button`);
@@ -18,6 +18,7 @@ export default class IconProvider {
18
18
  makeTouchPanningIcon(): IconType;
19
19
  makeAllDevicePanningIcon(): IconType;
20
20
  makeZoomIcon(): IconType;
21
+ makeRotationLockIcon(): IconType;
21
22
  makeTextIcon(textStyle: TextStyle): IconType;
22
23
  makePenIcon(tipThickness: number, color: string | Color4): IconType;
23
24
  makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): IconType;
@@ -113,7 +113,7 @@ export default class IconProvider {
113
113
  const fill = 'none';
114
114
  const strokeColor = 'var(--icon-color)';
115
115
  const strokeWidth = '3';
116
- // Draw a cursor-like shape (like some of the other icons, made with Inkscape)
116
+ // Draw a cursor-like shape
117
117
  return this.makeIconFromPath(`
118
118
  m 10,60
119
119
  5,30
@@ -242,6 +242,48 @@ export default class IconProvider {
242
242
  addTextNode('-', 70, 75);
243
243
  return icon;
244
244
  }
245
+ makeRotationLockIcon() {
246
+ const icon = this.makeIconFromPath(`
247
+ M 40.1 25.1
248
+ C 32.5 25 27.9 34.1 27.9 34.1
249
+ L 25.7 30
250
+ L 28 44.7
251
+ L 36.6 40.3
252
+ L 32.3 38.3
253
+ C 33.6 28 38.1 25.2 45.1 31.8
254
+ L 49.4 29.6
255
+ C 45.9 26.3 42.8 25.1 40.1 25.1
256
+ z
257
+
258
+ M 51.7 34.2
259
+ L 43.5 39.1
260
+ L 48 40.8
261
+ C 47.4 51.1 43.1 54.3 35.7 48.2
262
+ L 31.6 50.7
263
+ C 45.5 62.1 52.6 44.6 52.6 44.6
264
+ L 55.1 48.6
265
+ L 51.7 34.2
266
+ z
267
+
268
+ M 56.9 49.9
269
+ C 49.8 49.9 49.2 57.3 49.3 60.9
270
+ L 47.6 60.9
271
+ L 47.6 73.7
272
+ L 66.1 73.7
273
+ L 66.1 60.9
274
+ L 64.4 60.9
275
+ C 64.5 57.3 63.9 49.9 56.9 49.9
276
+ z
277
+
278
+ M 56.9 53.5
279
+ C 60.8 53.5 61 58.2 60.8 60.9
280
+ L 52.9 60.9
281
+ C 52.7 58.2 52.9 53.5 56.9 53.5
282
+ z
283
+ `);
284
+ icon.setAttribute('viewBox', '10 10 70 70');
285
+ return icon;
286
+ }
245
287
  makeTextIcon(textStyle) {
246
288
  var _a, _b;
247
289
  const icon = document.createElementNS(svgNamespace, 'svg');
@@ -1,6 +1,7 @@
1
1
  export interface ToolbarLocalization {
2
2
  fontLabel: string;
3
3
  touchPanning: string;
4
+ lockRotation: string;
4
5
  outlinedRectanglePen: string;
5
6
  filledRectanglePen: string;
6
7
  linePen: string;
@@ -24,6 +24,7 @@ export const defaultToolbarLocalization = {
24
24
  linePen: 'Line',
25
25
  outlinedRectanglePen: 'Outlined rectangle',
26
26
  filledRectanglePen: 'Filled rectangle',
27
+ lockRotation: 'Lock rotation',
27
28
  dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
28
29
  dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
29
30
  zoomLevel: (zoomPercent) => `Zoom: ${zoomPercent}%`,
@@ -1,5 +1,6 @@
1
1
  import Color4 from '../Color4';
2
2
  import Editor from '../Editor';
3
3
  declare type OnColorChangeListener = (color: Color4) => void;
4
- export declare const makeColorInput: (editor: Editor, onColorChange: OnColorChangeListener) => [HTMLInputElement, HTMLElement];
4
+ declare type SetColorCallback = (color: Color4 | string) => void;
5
+ export declare const makeColorInput: (editor: Editor, onColorChange: OnColorChangeListener) => [HTMLInputElement, HTMLElement, SetColorCallback];
5
6
  export default makeColorInput;
@@ -1,7 +1,7 @@
1
1
  import Color4 from '../Color4';
2
2
  import PipetteTool from '../tools/PipetteTool';
3
3
  import { EditorEventType } from '../types';
4
- // Returns [ color input, input container ].
4
+ // Returns [ color input, input container, callback to change the color value ].
5
5
  export const makeColorInput = (editor, onColorChange) => {
6
6
  const colorInputContainer = document.createElement('span');
7
7
  const colorInput = document.createElement('input');
@@ -22,6 +22,8 @@ export const makeColorInput = (editor, onColorChange) => {
22
22
  const handleColorInput = () => {
23
23
  currentColor = Color4.fromHex(colorInput.value);
24
24
  };
25
+ // Only change the pen color when we finish sending input (this limits the number of
26
+ // editor events triggered and accessibility announcements).
25
27
  const onInputEnd = () => {
26
28
  handleColorInput();
27
29
  if (currentColor) {
@@ -47,7 +49,16 @@ export const makeColorInput = (editor, onColorChange) => {
47
49
  });
48
50
  onInputEnd();
49
51
  });
50
- return [colorInput, colorInputContainer];
52
+ const setColorInputValue = (color) => {
53
+ if (typeof color === 'object') {
54
+ color = color.toHexString();
55
+ }
56
+ colorInput.value = color;
57
+ // Fire all color event listeners. See
58
+ // https://github.com/mdbassit/Coloris#manually-updating-the-thumbnail
59
+ colorInput.dispatchEvent(new Event('input', { bubbles: true }));
60
+ };
61
+ return [colorInput, colorInputContainer, setColorInputValue];
51
62
  };
52
63
  const addPipetteTool = (editor, container, onColorChange) => {
53
64
  const pipetteButton = document.createElement('button');
@@ -5,7 +5,7 @@ export default class ActionButtonWidget extends BaseWidget {
5
5
  protected makeIcon: () => Element;
6
6
  protected title: string;
7
7
  protected clickAction: () => void;
8
- constructor(editor: Editor, localizationTable: ToolbarLocalization, makeIcon: () => Element, title: string, clickAction: () => void);
8
+ constructor(editor: Editor, id: string, makeIcon: () => Element, title: string, clickAction: () => void, localizationTable?: ToolbarLocalization);
9
9
  protected handleClick(): void;
10
10
  protected getTitle(): string;
11
11
  protected createIcon(): Element;
@@ -1,7 +1,7 @@
1
1
  import BaseWidget from './BaseWidget';
2
2
  export default class ActionButtonWidget extends BaseWidget {
3
- constructor(editor, localizationTable, makeIcon, title, clickAction) {
4
- super(editor, localizationTable);
3
+ constructor(editor, id, makeIcon, title, clickAction, localizationTable) {
4
+ super(editor, id, localizationTable);
5
5
  this.makeIcon = makeIcon;
6
6
  this.title = title;
7
7
  this.clickAction = clickAction;
@@ -5,8 +5,7 @@ import BaseWidget from './BaseWidget';
5
5
  export default abstract class BaseToolWidget extends BaseWidget {
6
6
  protected editor: Editor;
7
7
  protected targetTool: BaseTool;
8
- protected localizationTable: ToolbarLocalization;
9
- constructor(editor: Editor, targetTool: BaseTool, localizationTable: ToolbarLocalization);
8
+ constructor(editor: Editor, targetTool: BaseTool, id: string, localizationTable?: ToolbarLocalization);
10
9
  protected handleClick(): void;
11
10
  addTo(parent: HTMLElement): void;
12
11
  }
@@ -1,11 +1,10 @@
1
1
  import { EditorEventType } from '../../types';
2
2
  import BaseWidget from './BaseWidget';
3
3
  export default class BaseToolWidget extends BaseWidget {
4
- constructor(editor, targetTool, localizationTable) {
5
- super(editor, localizationTable);
4
+ constructor(editor, targetTool, id, localizationTable) {
5
+ super(editor, id, localizationTable);
6
6
  this.editor = editor;
7
7
  this.targetTool = targetTool;
8
- this.localizationTable = localizationTable;
9
8
  editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
10
9
  if (toolEvt.kind !== EditorEventType.ToolEnabled) {
11
10
  throw new Error('Incorrect event type! (Expected ToolEnabled)');
@@ -1,10 +1,11 @@
1
1
  import Editor from '../../Editor';
2
2
  import { KeyPressEvent } from '../../types';
3
3
  import { ToolbarLocalization } from '../localization';
4
+ export declare type SavedToolbuttonState = Record<string, any>;
4
5
  export default abstract class BaseWidget {
5
6
  #private;
6
7
  protected editor: Editor;
7
- protected localizationTable: ToolbarLocalization;
8
+ protected id: string;
8
9
  protected readonly container: HTMLElement;
9
10
  private button;
10
11
  private icon;
@@ -14,7 +15,19 @@ export default abstract class BaseWidget {
14
15
  private disabled;
15
16
  private subWidgets;
16
17
  private toplevel;
17
- constructor(editor: Editor, localizationTable: ToolbarLocalization);
18
+ protected readonly localizationTable: ToolbarLocalization;
19
+ constructor(editor: Editor, id: string, localizationTable?: ToolbarLocalization);
20
+ getId(): string;
21
+ /**
22
+ * Returns the ID of this widget in `container`. Adds a suffix to this' ID
23
+ * if an item in `container` already has this' ID.
24
+ *
25
+ * For example, if `this` has ID `foo` and if
26
+ * `container = { 'foo': somethingNotThis, 'foo-1': somethingElseNotThis }`, this method
27
+ * returns `foo-2` because elements with IDs `foo` and `foo-1` are already present in
28
+ * `container`.
29
+ */
30
+ getUniqueIdIn(container: Record<string, BaseWidget>): string;
18
31
  protected abstract getTitle(): string;
19
32
  protected abstract createIcon(): Element;
20
33
  protected fillDropdown(dropdown: HTMLElement): boolean;
@@ -34,4 +47,21 @@ export default abstract class BaseWidget {
34
47
  protected isDropdownVisible(): boolean;
35
48
  protected isSelected(): boolean;
36
49
  private createDropdownIcon;
50
+ /**
51
+ * Serialize state associated with this widget.
52
+ * Override this method to allow saving/restoring from state on application load.
53
+ *
54
+ * Overriders should call `super` and include the output of `super.serializeState` in
55
+ * the output dictionary.
56
+ *
57
+ * Clients should not rely on the output from `saveState` being in any particular
58
+ * format.
59
+ */
60
+ serializeState(): SavedToolbuttonState;
61
+ /**
62
+ * Restore widget state from serialized data. See also `saveState`.
63
+ *
64
+ * Overriders must call `super`.
65
+ */
66
+ deserializeFrom(state: SavedToolbuttonState): void;
37
67
  }
@@ -14,13 +14,15 @@ import ToolbarShortcutHandler from '../../tools/ToolbarShortcutHandler';
14
14
  import { EditorEventType, InputEvtType } from '../../types';
15
15
  import { toolbarCSSPrefix } from '../HTMLToolbar';
16
16
  export default class BaseWidget {
17
- constructor(editor, localizationTable) {
17
+ constructor(editor, id, localizationTable) {
18
18
  this.editor = editor;
19
- this.localizationTable = localizationTable;
19
+ this.id = id;
20
20
  _BaseWidget_hasDropdown.set(this, void 0);
21
21
  this.disabled = false;
22
- this.subWidgets = [];
22
+ // Maps subWidget IDs to subWidgets.
23
+ this.subWidgets = {};
23
24
  this.toplevel = true;
25
+ this.localizationTable = localizationTable !== null && localizationTable !== void 0 ? localizationTable : editor.localization;
24
26
  this.icon = null;
25
27
  this.container = document.createElement('div');
26
28
  this.container.classList.add(`${toolbarCSSPrefix}toolContainer`);
@@ -40,13 +42,35 @@ export default class BaseWidget {
40
42
  toolbarShortcutHandlers[0].registerListener(event => this.onKeyPress(event));
41
43
  }
42
44
  }
45
+ getId() {
46
+ return this.id;
47
+ }
48
+ /**
49
+ * Returns the ID of this widget in `container`. Adds a suffix to this' ID
50
+ * if an item in `container` already has this' ID.
51
+ *
52
+ * For example, if `this` has ID `foo` and if
53
+ * `container = { 'foo': somethingNotThis, 'foo-1': somethingElseNotThis }`, this method
54
+ * returns `foo-2` because elements with IDs `foo` and `foo-1` are already present in
55
+ * `container`.
56
+ */
57
+ getUniqueIdIn(container) {
58
+ let id = this.getId();
59
+ let idCounter = 0;
60
+ while (id in container && container[id] !== this) {
61
+ id = this.getId() + '-' + idCounter.toString();
62
+ idCounter++;
63
+ }
64
+ return id;
65
+ }
43
66
  // Add content to the widget's associated dropdown menu.
44
67
  // Returns true if such a menu should be created, false otherwise.
45
68
  fillDropdown(dropdown) {
46
- if (this.subWidgets.length === 0) {
69
+ if (Object.keys(this.subWidgets).length === 0) {
47
70
  return false;
48
71
  }
49
- for (const widget of this.subWidgets) {
72
+ for (const widgetId in this.subWidgets) {
73
+ const widget = this.subWidgets[widgetId];
50
74
  widget.addTo(dropdown);
51
75
  widget.setIsToplevel(false);
52
76
  }
@@ -100,7 +124,9 @@ export default class BaseWidget {
100
124
  }
101
125
  // Add a widget to this' dropdown. Must be called before this.addTo.
102
126
  addSubWidget(widget) {
103
- this.subWidgets.push(widget);
127
+ // Generate a unique ID for the widget.
128
+ const id = widget.getUniqueIdIn(this.subWidgets);
129
+ this.subWidgets[id] = widget;
104
130
  }
105
131
  // Adds this to [parent]. This can only be called once for each ToolbarWidget.
106
132
  // @internal
@@ -209,5 +235,40 @@ export default class BaseWidget {
209
235
  icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
210
236
  return icon;
211
237
  }
238
+ /**
239
+ * Serialize state associated with this widget.
240
+ * Override this method to allow saving/restoring from state on application load.
241
+ *
242
+ * Overriders should call `super` and include the output of `super.serializeState` in
243
+ * the output dictionary.
244
+ *
245
+ * Clients should not rely on the output from `saveState` being in any particular
246
+ * format.
247
+ */
248
+ serializeState() {
249
+ const subwidgetState = {};
250
+ // Save all subwidget state.
251
+ for (const subwidgetId in this.subWidgets) {
252
+ subwidgetState[subwidgetId] = this.subWidgets[subwidgetId].serializeState();
253
+ }
254
+ return {
255
+ subwidgetState,
256
+ };
257
+ }
258
+ /**
259
+ * Restore widget state from serialized data. See also `saveState`.
260
+ *
261
+ * Overriders must call `super`.
262
+ */
263
+ deserializeFrom(state) {
264
+ if (state.subwidgetState) {
265
+ // Deserialize all subwidgets.
266
+ for (const subwidgetId in state.subwidgetState) {
267
+ if (subwidgetId in this.subWidgets) {
268
+ this.subWidgets[subwidgetId].deserializeFrom(state.subwidgetState[subwidgetId]);
269
+ }
270
+ }
271
+ }
272
+ }
212
273
  }
213
274
  _BaseWidget_hasDropdown = new WeakMap();
@@ -1,5 +1,9 @@
1
+ import Editor from '../../Editor';
2
+ import Eraser from '../../tools/Eraser';
3
+ import { ToolbarLocalization } from '../localization';
1
4
  import BaseToolWidget from './BaseToolWidget';
2
5
  export default class EraserToolWidget extends BaseToolWidget {
6
+ constructor(editor: Editor, tool: Eraser, localizationTable?: ToolbarLocalization);
3
7
  protected getTitle(): string;
4
8
  protected createIcon(): Element;
5
9
  protected fillDropdown(_dropdown: HTMLElement): boolean;
@@ -1,5 +1,8 @@
1
1
  import BaseToolWidget from './BaseToolWidget';
2
2
  export default class EraserToolWidget extends BaseToolWidget {
3
+ constructor(editor, tool, localizationTable) {
4
+ super(editor, tool, 'eraser-tool-widget', localizationTable);
5
+ }
3
6
  getTitle() {
4
7
  return this.localizationTable.eraser;
5
8
  }
@@ -2,9 +2,9 @@ import Editor from '../../Editor';
2
2
  import PanZoom from '../../tools/PanZoom';
3
3
  import { ToolbarLocalization } from '../localization';
4
4
  import BaseToolWidget from './BaseToolWidget';
5
+ import { SavedToolbuttonState } from './BaseWidget';
5
6
  export default class HandToolWidget extends BaseToolWidget {
6
7
  protected overridePanZoomTool: PanZoom;
7
- private touchPanningWidget;
8
8
  private allowTogglingBaseTool;
9
9
  constructor(editor: Editor, overridePanZoomTool: PanZoom, localizationTable: ToolbarLocalization);
10
10
  private static getPrimaryHandTool;
@@ -12,4 +12,6 @@ export default class HandToolWidget extends BaseToolWidget {
12
12
  protected createIcon(): Element;
13
13
  protected handleClick(): void;
14
14
  setSelected(selected: boolean): void;
15
+ serializeState(): SavedToolbuttonState;
16
+ deserializeFrom(state: SavedToolbuttonState): void;
15
17
  }