js-draw 0.8.0 → 0.9.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/CHANGELOG.md +8 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Color4.js +3 -0
  4. package/dist/src/SVGLoader.js +5 -7
  5. package/dist/src/components/Stroke.js +2 -2
  6. package/dist/src/math/Path.js +6 -1
  7. package/dist/src/toolbar/HTMLToolbar.d.ts +3 -0
  8. package/dist/src/toolbar/HTMLToolbar.js +24 -0
  9. package/dist/src/toolbar/IconProvider.d.ts +1 -0
  10. package/dist/src/toolbar/IconProvider.js +43 -1
  11. package/dist/src/toolbar/localization.d.ts +1 -0
  12. package/dist/src/toolbar/localization.js +1 -0
  13. package/dist/src/toolbar/makeColorInput.d.ts +2 -1
  14. package/dist/src/toolbar/makeColorInput.js +13 -2
  15. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +1 -1
  16. package/dist/src/toolbar/widgets/ActionButtonWidget.js +2 -2
  17. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -2
  18. package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -3
  19. package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -2
  20. package/dist/src/toolbar/widgets/BaseWidget.js +67 -6
  21. package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +4 -0
  22. package/dist/src/toolbar/widgets/EraserToolWidget.js +3 -0
  23. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +3 -1
  24. package/dist/src/toolbar/widgets/HandToolWidget.js +22 -13
  25. package/dist/src/toolbar/widgets/PenToolWidget.d.ts +7 -1
  26. package/dist/src/toolbar/widgets/PenToolWidget.js +78 -12
  27. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
  28. package/dist/src/toolbar/widgets/SelectionToolWidget.js +7 -7
  29. package/dist/src/toolbar/widgets/TextToolWidget.d.ts +4 -1
  30. package/dist/src/toolbar/widgets/TextToolWidget.js +17 -3
  31. package/dist/src/tools/PanZoom.d.ts +4 -1
  32. package/dist/src/tools/PanZoom.js +24 -1
  33. package/package.json +1 -1
  34. package/src/Color4.ts +2 -0
  35. package/src/SVGLoader.ts +8 -8
  36. package/src/components/Stroke.ts +1 -1
  37. package/src/math/Path.test.ts +24 -0
  38. package/src/math/Path.ts +7 -1
  39. package/src/toolbar/HTMLToolbar.ts +33 -0
  40. package/src/toolbar/IconProvider.ts +49 -1
  41. package/src/toolbar/localization.ts +2 -0
  42. package/src/toolbar/makeColorInput.ts +21 -3
  43. package/src/toolbar/widgets/ActionButtonWidget.ts +6 -3
  44. package/src/toolbar/widgets/BaseToolWidget.ts +4 -3
  45. package/src/toolbar/widgets/BaseWidget.ts +83 -5
  46. package/src/toolbar/widgets/EraserToolWidget.ts +11 -0
  47. package/src/toolbar/widgets/HandToolWidget.ts +48 -17
  48. package/src/toolbar/widgets/PenToolWidget.ts +105 -13
  49. package/src/toolbar/widgets/SelectionToolWidget.ts +8 -5
  50. package/src/toolbar/widgets/TextToolWidget.ts +29 -4
  51. package/src/tools/PanZoom.ts +28 -1
@@ -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');
@@ -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);
@@ -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
  }
@@ -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) {
@@ -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
  }
@@ -57,7 +57,7 @@ const makeZoomControl = (localizationTable, editor) => {
57
57
  };
58
58
  class ZoomWidget extends BaseWidget {
59
59
  constructor(editor, localizationTable) {
60
- super(editor, localizationTable);
60
+ super(editor, 'zoom-widget', localizationTable);
61
61
  // Make it possible to open the dropdown, even if this widget isn't selected.
62
62
  this.container.classList.add('dropdownShowable');
63
63
  }
@@ -76,8 +76,8 @@ class ZoomWidget extends BaseWidget {
76
76
  }
77
77
  }
78
78
  class HandModeWidget extends BaseWidget {
79
- constructor(editor, localizationTable, tool, flag, makeIcon, title) {
80
- super(editor, localizationTable);
79
+ constructor(editor, tool, flag, makeIcon, title, localizationTable) {
80
+ super(editor, `pan-mode-${flag}`, localizationTable);
81
81
  this.tool = tool;
82
82
  this.flag = flag;
83
83
  this.makeIcon = makeIcon;
@@ -94,13 +94,7 @@ class HandModeWidget extends BaseWidget {
94
94
  this.setSelected(false);
95
95
  }
96
96
  setModeFlag(enabled) {
97
- const mode = this.tool.getMode();
98
- if (enabled) {
99
- this.tool.setMode(mode | this.flag);
100
- }
101
- else {
102
- this.tool.setMode(mode & ~this.flag);
103
- }
97
+ this.tool.setModeEnabled(this.flag, enabled);
104
98
  }
105
99
  handleClick() {
106
100
  this.setModeFlag(!this.isSelected());
@@ -122,7 +116,7 @@ export default class HandToolWidget extends BaseToolWidget {
122
116
  overridePanZoomTool, localizationTable) {
123
117
  const primaryHandTool = HandToolWidget.getPrimaryHandTool(editor.toolController);
124
118
  const tool = primaryHandTool !== null && primaryHandTool !== void 0 ? primaryHandTool : overridePanZoomTool;
125
- super(editor, tool, localizationTable);
119
+ super(editor, tool, 'hand-tool-widget', localizationTable);
126
120
  this.overridePanZoomTool = overridePanZoomTool;
127
121
  // Only allow toggling a hand tool if we're using the primary hand tool and not the override
128
122
  // hand tool for this button.
@@ -132,8 +126,10 @@ export default class HandToolWidget extends BaseToolWidget {
132
126
  this.container.classList.add('dropdownShowable');
133
127
  }
134
128
  // Controls for the overriding hand tool.
135
- this.touchPanningWidget = new HandModeWidget(editor, localizationTable, overridePanZoomTool, PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning);
136
- this.addSubWidget(this.touchPanningWidget);
129
+ const touchPanningWidget = new HandModeWidget(editor, overridePanZoomTool, PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning, localizationTable);
130
+ const rotationLockWidget = new HandModeWidget(editor, overridePanZoomTool, PanZoomMode.RotationLocked, () => this.editor.icons.makeRotationLockIcon(), localizationTable.lockRotation, localizationTable);
131
+ this.addSubWidget(touchPanningWidget);
132
+ this.addSubWidget(rotationLockWidget);
137
133
  this.addSubWidget(new ZoomWidget(editor, localizationTable));
138
134
  }
139
135
  static getPrimaryHandTool(toolController) {
@@ -160,4 +156,17 @@ export default class HandToolWidget extends BaseToolWidget {
160
156
  super.setSelected(selected);
161
157
  }
162
158
  }
159
+ serializeState() {
160
+ const toolMode = this.overridePanZoomTool.getMode();
161
+ return Object.assign(Object.assign({}, super.serializeState()), { touchPanning: toolMode & PanZoomMode.OneFingerTouchGestures, rotationLocked: toolMode & PanZoomMode.RotationLocked });
162
+ }
163
+ deserializeFrom(state) {
164
+ if (state.touchPanning !== undefined) {
165
+ this.overridePanZoomTool.setModeEnabled(PanZoomMode.OneFingerTouchGestures, state.touchPanning);
166
+ }
167
+ if (state.rotationLocked !== undefined) {
168
+ this.overridePanZoomTool.setModeEnabled(PanZoomMode.RotationLocked, state.rotationLocked);
169
+ }
170
+ super.deserializeFrom(state);
171
+ }
163
172
  }
@@ -4,18 +4,24 @@ import Pen from '../../tools/Pen';
4
4
  import { KeyPressEvent } from '../../types';
5
5
  import { ToolbarLocalization } from '../localization';
6
6
  import BaseToolWidget from './BaseToolWidget';
7
+ import { SavedToolbuttonState } from './BaseWidget';
7
8
  export interface PenTypeRecord {
8
9
  name: string;
10
+ id: string;
9
11
  factory: ComponentBuilderFactory;
10
12
  }
11
13
  export default class PenToolWidget extends BaseToolWidget {
12
14
  private tool;
13
15
  private updateInputs;
14
16
  protected penTypes: PenTypeRecord[];
15
- constructor(editor: Editor, tool: Pen, localization: ToolbarLocalization);
17
+ constructor(editor: Editor, tool: Pen, localization?: ToolbarLocalization);
16
18
  protected getTitle(): string;
19
+ private getCurrentPenTypeIdx;
20
+ private getCurrentPenType;
17
21
  protected createIcon(): Element;
18
22
  private static idCounter;
19
23
  protected fillDropdown(dropdown: HTMLElement): boolean;
20
24
  protected onKeyPress(event: KeyPressEvent): boolean;
25
+ serializeState(): SavedToolbuttonState;
26
+ deserializeFrom(state: SavedToolbuttonState): void;
21
27
  }