js-draw 0.7.2 → 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 (82) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.md +1 -0
  2. package/CHANGELOG.md +12 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Color4.js +3 -0
  5. package/dist/src/SVGLoader.js +5 -7
  6. package/dist/src/components/Stroke.js +10 -3
  7. package/dist/src/components/builders/FreehandLineBuilder.d.ts +10 -23
  8. package/dist/src/components/builders/FreehandLineBuilder.js +70 -396
  9. package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +36 -0
  10. package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.js +339 -0
  11. package/dist/src/components/lib.d.ts +2 -0
  12. package/dist/src/components/lib.js +2 -0
  13. package/dist/src/components/util/StrokeSmoother.d.ts +35 -0
  14. package/dist/src/components/util/StrokeSmoother.js +206 -0
  15. package/dist/src/math/Mat33.d.ts +2 -0
  16. package/dist/src/math/Mat33.js +4 -0
  17. package/dist/src/math/Path.d.ts +2 -0
  18. package/dist/src/math/Path.js +44 -0
  19. package/dist/src/rendering/renderers/CanvasRenderer.js +2 -0
  20. package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -0
  21. package/dist/src/rendering/renderers/SVGRenderer.js +20 -0
  22. package/dist/src/toolbar/HTMLToolbar.d.ts +3 -0
  23. package/dist/src/toolbar/HTMLToolbar.js +24 -0
  24. package/dist/src/toolbar/IconProvider.d.ts +1 -0
  25. package/dist/src/toolbar/IconProvider.js +43 -1
  26. package/dist/src/toolbar/localization.d.ts +2 -0
  27. package/dist/src/toolbar/localization.js +2 -0
  28. package/dist/src/toolbar/makeColorInput.d.ts +2 -1
  29. package/dist/src/toolbar/makeColorInput.js +13 -2
  30. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +1 -1
  31. package/dist/src/toolbar/widgets/ActionButtonWidget.js +2 -2
  32. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -2
  33. package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -3
  34. package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -2
  35. package/dist/src/toolbar/widgets/BaseWidget.js +67 -6
  36. package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +4 -0
  37. package/dist/src/toolbar/widgets/EraserToolWidget.js +3 -0
  38. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +3 -1
  39. package/dist/src/toolbar/widgets/HandToolWidget.js +22 -13
  40. package/dist/src/toolbar/widgets/PenToolWidget.d.ts +7 -1
  41. package/dist/src/toolbar/widgets/PenToolWidget.js +83 -12
  42. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
  43. package/dist/src/toolbar/widgets/SelectionToolWidget.js +7 -7
  44. package/dist/src/toolbar/widgets/TextToolWidget.d.ts +4 -1
  45. package/dist/src/toolbar/widgets/TextToolWidget.js +17 -3
  46. package/dist/src/tools/PanZoom.d.ts +4 -1
  47. package/dist/src/tools/PanZoom.js +24 -1
  48. package/dist/src/tools/Pen.d.ts +2 -2
  49. package/dist/src/tools/Pen.js +2 -2
  50. package/dist/src/tools/SelectionTool/Selection.d.ts +1 -0
  51. package/dist/src/tools/SelectionTool/Selection.js +8 -1
  52. package/dist/src/tools/ToolController.js +2 -1
  53. package/package.json +1 -1
  54. package/src/Color4.ts +2 -0
  55. package/src/SVGLoader.ts +8 -8
  56. package/src/components/Stroke.ts +16 -3
  57. package/src/components/builders/FreehandLineBuilder.ts +54 -495
  58. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +454 -0
  59. package/src/components/lib.ts +3 -1
  60. package/src/components/util/StrokeSmoother.ts +290 -0
  61. package/src/math/Mat33.ts +5 -0
  62. package/src/math/Path.test.ts +49 -0
  63. package/src/math/Path.ts +51 -0
  64. package/src/rendering/renderers/CanvasRenderer.ts +2 -0
  65. package/src/rendering/renderers/SVGRenderer.ts +24 -0
  66. package/src/toolbar/HTMLToolbar.ts +33 -0
  67. package/src/toolbar/IconProvider.ts +49 -1
  68. package/src/toolbar/localization.ts +4 -0
  69. package/src/toolbar/makeColorInput.ts +21 -3
  70. package/src/toolbar/widgets/ActionButtonWidget.ts +6 -3
  71. package/src/toolbar/widgets/BaseToolWidget.ts +4 -3
  72. package/src/toolbar/widgets/BaseWidget.ts +83 -5
  73. package/src/toolbar/widgets/EraserToolWidget.ts +11 -0
  74. package/src/toolbar/widgets/HandToolWidget.ts +48 -17
  75. package/src/toolbar/widgets/PenToolWidget.ts +110 -13
  76. package/src/toolbar/widgets/SelectionToolWidget.ts +8 -5
  77. package/src/toolbar/widgets/TextToolWidget.ts +29 -4
  78. package/src/tools/PanZoom.ts +28 -1
  79. package/src/tools/Pen.test.ts +2 -2
  80. package/src/tools/Pen.ts +1 -1
  81. package/src/tools/SelectionTool/Selection.ts +10 -1
  82. package/src/tools/ToolController.ts +2 -1
@@ -195,6 +195,45 @@ export default class Path {
195
195
  ...other.parts,
196
196
  ]);
197
197
  }
198
+ getEndPoint() {
199
+ if (this.parts.length === 0) {
200
+ return this.startPoint;
201
+ }
202
+ const lastPart = this.parts[this.parts.length - 1];
203
+ if (lastPart.kind === PathCommandType.QuadraticBezierTo || lastPart.kind === PathCommandType.CubicBezierTo) {
204
+ return lastPart.endPoint;
205
+ }
206
+ else {
207
+ return lastPart.point;
208
+ }
209
+ }
210
+ roughlyIntersects(rect, strokeWidth = 0) {
211
+ if (this.parts.length === 0) {
212
+ return rect.containsPoint(this.startPoint);
213
+ }
214
+ const isClosed = this.startPoint.eq(this.getEndPoint());
215
+ if (isClosed && strokeWidth === 0) {
216
+ return this.closedRoughlyIntersects(rect);
217
+ }
218
+ if (rect.containsRect(this.bbox)) {
219
+ return true;
220
+ }
221
+ // Does the rectangle intersect the bounding boxes of any of this' parts?
222
+ let startPoint = this.startPoint;
223
+ for (const part of this.parts) {
224
+ const bbox = Path.computeBBoxForSegment(startPoint, part).grownBy(strokeWidth);
225
+ if (part.kind === PathCommandType.LineTo || part.kind === PathCommandType.MoveTo) {
226
+ startPoint = part.point;
227
+ }
228
+ else {
229
+ startPoint = part.endPoint;
230
+ }
231
+ if (rect.intersects(bbox)) {
232
+ return true;
233
+ }
234
+ }
235
+ return false;
236
+ }
198
237
  // Treats this as a closed path and returns true if part of `rect` is roughly within
199
238
  // this path's interior.
200
239
  //
@@ -266,6 +305,11 @@ export default class Path {
266
305
  point: corner,
267
306
  });
268
307
  }
308
+ // Close the shape
309
+ commands.push({
310
+ kind: PathCommandType.LineTo,
311
+ point: startPoint,
312
+ });
269
313
  return new Path(startPoint, commands);
270
314
  }
271
315
  static fromRenderable(renderable) {
@@ -64,6 +64,8 @@ export default class CanvasRenderer extends AbstractRenderer {
64
64
  if (style.stroke) {
65
65
  this.ctx.strokeStyle = style.stroke.color.toHexString();
66
66
  this.ctx.lineWidth = this.getSizeOfCanvasPixelOnScreen() * style.stroke.width;
67
+ this.ctx.lineCap = 'round';
68
+ this.ctx.lineJoin = 'round';
67
69
  this.ctx.stroke();
68
70
  }
69
71
  this.ctx.closePath();
@@ -6,6 +6,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
6
6
  import Viewport from '../../Viewport';
7
7
  import RenderingStyle from '../RenderingStyle';
8
8
  import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
9
+ export declare const renderedStylesheetId = "js-draw-style-sheet";
9
10
  export default class SVGRenderer extends AbstractRenderer {
10
11
  private elem;
11
12
  private sanitize;
@@ -14,6 +15,7 @@ export default class SVGRenderer extends AbstractRenderer {
14
15
  private objectElems;
15
16
  private overwrittenAttrs;
16
17
  constructor(elem: SVGSVGElement, viewport: Viewport, sanitize?: boolean);
18
+ private addStyleSheet;
17
19
  setRootSVGAttribute(name: string, value: string | null): void;
18
20
  displaySize(): Vec2;
19
21
  clear(): void;
@@ -4,6 +4,7 @@ import { toRoundedString } from '../../math/rounding';
4
4
  import { Vec2 } from '../../math/Vec2';
5
5
  import { svgAttributesDataKey, svgStyleAttributesDataKey } from '../../SVGLoader';
6
6
  import AbstractRenderer from './AbstractRenderer';
7
+ export const renderedStylesheetId = 'js-draw-style-sheet';
7
8
  const svgNameSpace = 'http://www.w3.org/2000/svg';
8
9
  export default class SVGRenderer extends AbstractRenderer {
9
10
  // Renders onto `elem`. If `sanitize`, don't render potentially untrusted data.
@@ -18,6 +19,21 @@ export default class SVGRenderer extends AbstractRenderer {
18
19
  this.textContainer = null;
19
20
  this.textContainerTransform = null;
20
21
  this.clear();
22
+ this.addStyleSheet();
23
+ }
24
+ addStyleSheet() {
25
+ if (!this.elem.querySelector(`#${renderedStylesheetId}`)) {
26
+ // Default to rounded strokes.
27
+ const styleSheet = document.createElementNS('http://www.w3.org/2000/svg', 'style');
28
+ styleSheet.innerHTML = `
29
+ path {
30
+ stroke-linecap: round;
31
+ stroke-linejoin: round;
32
+ }
33
+ `.replace(/\s+/g, '');
34
+ styleSheet.setAttribute('id', renderedStylesheetId);
35
+ this.elem.appendChild(styleSheet);
36
+ }
21
37
  }
22
38
  // Sets an attribute on the root SVG element.
23
39
  setRootSVGAttribute(name, value) {
@@ -214,6 +230,10 @@ export default class SVGRenderer extends AbstractRenderer {
214
230
  if (this.sanitize) {
215
231
  return;
216
232
  }
233
+ // Don't add multiple copies of the default stylesheet.
234
+ if (elem.tagName.toLowerCase() === 'style' && elem.getAttribute('id') === renderedStylesheetId) {
235
+ return;
236
+ }
217
237
  this.elem.appendChild(elem.cloneNode(true));
218
238
  }
219
239
  isTooSmallToRender(_rect) {
@@ -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,11 +1,13 @@
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;
7
8
  arrowPen: string;
8
9
  freehandPen: string;
10
+ pressureSensitiveFreehandPen: string;
9
11
  selectObjectType: string;
10
12
  colorLabel: string;
11
13
  pen: string;
@@ -19,10 +19,12 @@ export const defaultToolbarLocalization = {
19
19
  selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
20
20
  touchPanning: 'Touchscreen panning',
21
21
  freehandPen: 'Freehand',
22
+ pressureSensitiveFreehandPen: 'Freehand (pressure sensitive)',
22
23
  arrowPen: 'Arrow',
23
24
  linePen: 'Line',
24
25
  outlinedRectanglePen: 'Outlined rectangle',
25
26
  filledRectanglePen: 'Filled rectangle',
27
+ lockRotation: 'Lock rotation',
26
28
  dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
27
29
  dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
28
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
  }