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
@@ -8,9 +8,10 @@ export default abstract class BaseToolWidget extends BaseWidget {
8
8
  public constructor(
9
9
  protected editor: Editor,
10
10
  protected targetTool: BaseTool,
11
- protected localizationTable: ToolbarLocalization,
11
+ id: string,
12
+ localizationTable?: ToolbarLocalization,
12
13
  ) {
13
- super(editor, localizationTable);
14
+ super(editor, id, localizationTable);
14
15
 
15
16
  editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
16
17
  if (toolEvt.kind !== EditorEventType.ToolEnabled) {
@@ -50,4 +51,4 @@ export default abstract class BaseToolWidget extends BaseWidget {
50
51
  super.addTo(parent);
51
52
  this.setSelected(this.targetTool.isEnabled());
52
53
  }
53
- }
54
+ }
@@ -4,6 +4,8 @@ import { EditorEventType, InputEvtType, KeyPressEvent } from '../../types';
4
4
  import { toolbarCSSPrefix } from '../HTMLToolbar';
5
5
  import { ToolbarLocalization } from '../localization';
6
6
 
7
+ export type SavedToolbuttonState = Record<string, any>;
8
+
7
9
  export default abstract class BaseWidget {
8
10
  protected readonly container: HTMLElement;
9
11
  private button: HTMLElement;
@@ -13,13 +15,20 @@ export default abstract class BaseWidget {
13
15
  private label: HTMLLabelElement;
14
16
  #hasDropdown: boolean;
15
17
  private disabled: boolean = false;
16
- private subWidgets: BaseWidget[] = [];
18
+
19
+ // Maps subWidget IDs to subWidgets.
20
+ private subWidgets: Record<string, BaseWidget> = {};
21
+
17
22
  private toplevel: boolean = true;
23
+ protected readonly localizationTable: ToolbarLocalization;
18
24
 
19
25
  public constructor(
20
26
  protected editor: Editor,
21
- protected localizationTable: ToolbarLocalization,
27
+ protected id: string,
28
+ localizationTable?: ToolbarLocalization,
22
29
  ) {
30
+ this.localizationTable = localizationTable ?? editor.localization;
31
+
23
32
  this.icon = null;
24
33
  this.container = document.createElement('div');
25
34
  this.container.classList.add(`${toolbarCSSPrefix}toolContainer`);
@@ -43,17 +52,44 @@ export default abstract class BaseWidget {
43
52
  }
44
53
  }
45
54
 
55
+ public getId(): string {
56
+ return this.id;
57
+ }
58
+
59
+ /**
60
+ * Returns the ID of this widget in `container`. Adds a suffix to this' ID
61
+ * if an item in `container` already has this' ID.
62
+ *
63
+ * For example, if `this` has ID `foo` and if
64
+ * `container = { 'foo': somethingNotThis, 'foo-1': somethingElseNotThis }`, this method
65
+ * returns `foo-2` because elements with IDs `foo` and `foo-1` are already present in
66
+ * `container`.
67
+ */
68
+ public getUniqueIdIn(container: Record<string, BaseWidget>): string {
69
+ let id = this.getId();
70
+ let idCounter = 0;
71
+
72
+ while (id in container && container[id] !== this) {
73
+ id = this.getId() + '-' + idCounter.toString();
74
+ idCounter ++;
75
+ }
76
+
77
+ return id;
78
+ }
79
+
46
80
  protected abstract getTitle(): string;
47
81
  protected abstract createIcon(): Element;
48
82
 
49
83
  // Add content to the widget's associated dropdown menu.
50
84
  // Returns true if such a menu should be created, false otherwise.
51
85
  protected fillDropdown(dropdown: HTMLElement): boolean {
52
- if (this.subWidgets.length === 0) {
86
+ if (Object.keys(this.subWidgets).length === 0) {
53
87
  return false;
54
88
  }
55
89
 
56
- for (const widget of this.subWidgets) {
90
+ for (const widgetId in this.subWidgets) {
91
+ const widget = this.subWidgets[widgetId];
92
+
57
93
  widget.addTo(dropdown);
58
94
  widget.setIsToplevel(false);
59
95
  }
@@ -118,7 +154,10 @@ export default abstract class BaseWidget {
118
154
 
119
155
  // Add a widget to this' dropdown. Must be called before this.addTo.
120
156
  protected addSubWidget(widget: BaseWidget) {
121
- this.subWidgets.push(widget);
157
+ // Generate a unique ID for the widget.
158
+ const id = widget.getUniqueIdIn(this.subWidgets);
159
+
160
+ this.subWidgets[id] = widget;
122
161
  }
123
162
 
124
163
  // Adds this to [parent]. This can only be called once for each ToolbarWidget.
@@ -251,4 +290,43 @@ export default abstract class BaseWidget {
251
290
  icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
252
291
  return icon;
253
292
  }
293
+
294
+ /**
295
+ * Serialize state associated with this widget.
296
+ * Override this method to allow saving/restoring from state on application load.
297
+ *
298
+ * Overriders should call `super` and include the output of `super.serializeState` in
299
+ * the output dictionary.
300
+ *
301
+ * Clients should not rely on the output from `saveState` being in any particular
302
+ * format.
303
+ */
304
+ public serializeState(): SavedToolbuttonState {
305
+ const subwidgetState: Record<string, any> = {};
306
+
307
+ // Save all subwidget state.
308
+ for (const subwidgetId in this.subWidgets) {
309
+ subwidgetState[subwidgetId] = this.subWidgets[subwidgetId].serializeState();
310
+ }
311
+
312
+ return {
313
+ subwidgetState,
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Restore widget state from serialized data. See also `saveState`.
319
+ *
320
+ * Overriders must call `super`.
321
+ */
322
+ public deserializeFrom(state: SavedToolbuttonState): void {
323
+ if (state.subwidgetState) {
324
+ // Deserialize all subwidgets.
325
+ for (const subwidgetId in state.subwidgetState) {
326
+ if (subwidgetId in this.subWidgets) {
327
+ this.subWidgets[subwidgetId].deserializeFrom(state.subwidgetState[subwidgetId]);
328
+ }
329
+ }
330
+ }
331
+ }
254
332
  }
@@ -1,6 +1,17 @@
1
+ import Editor from '../../Editor';
2
+ import Eraser from '../../tools/Eraser';
3
+ import { ToolbarLocalization } from '../localization';
1
4
  import BaseToolWidget from './BaseToolWidget';
2
5
 
3
6
  export default class EraserToolWidget extends BaseToolWidget {
7
+ public constructor(
8
+ editor: Editor,
9
+ tool: Eraser,
10
+ localizationTable?: ToolbarLocalization
11
+ ) {
12
+ super(editor, tool, 'eraser-tool-widget', localizationTable);
13
+ }
14
+
4
15
  protected getTitle(): string {
5
16
  return this.localizationTable.eraser;
6
17
  }
@@ -7,7 +7,7 @@ import Viewport from '../../Viewport';
7
7
  import { toolbarCSSPrefix } from '../HTMLToolbar';
8
8
  import { ToolbarLocalization } from '../localization';
9
9
  import BaseToolWidget from './BaseToolWidget';
10
- import BaseWidget from './BaseWidget';
10
+ import BaseWidget, { SavedToolbuttonState } from './BaseWidget';
11
11
 
12
12
  const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor) => {
13
13
  const zoomLevelRow = document.createElement('div');
@@ -74,8 +74,8 @@ const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor)
74
74
  };
75
75
 
76
76
  class ZoomWidget extends BaseWidget {
77
- public constructor(editor: Editor, localizationTable: ToolbarLocalization) {
78
- super(editor, localizationTable);
77
+ public constructor(editor: Editor, localizationTable?: ToolbarLocalization) {
78
+ super(editor, 'zoom-widget', localizationTable);
79
79
 
80
80
  // Make it possible to open the dropdown, even if this widget isn't selected.
81
81
  this.container.classList.add('dropdownShowable');
@@ -101,12 +101,14 @@ class ZoomWidget extends BaseWidget {
101
101
 
102
102
  class HandModeWidget extends BaseWidget {
103
103
  public constructor(
104
- editor: Editor, localizationTable: ToolbarLocalization,
104
+ editor: Editor,
105
105
 
106
106
  protected tool: PanZoom, protected flag: PanZoomMode, protected makeIcon: ()=> Element,
107
107
  private title: string,
108
+
109
+ localizationTable?: ToolbarLocalization,
108
110
  ) {
109
- super(editor, localizationTable);
111
+ super(editor, `pan-mode-${flag}`, localizationTable);
110
112
 
111
113
  editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
112
114
  if (toolEvt.kind === EditorEventType.ToolUpdated && toolEvt.tool === tool) {
@@ -122,12 +124,7 @@ class HandModeWidget extends BaseWidget {
122
124
  }
123
125
 
124
126
  private setModeFlag(enabled: boolean) {
125
- const mode = this.tool.getMode();
126
- if (enabled) {
127
- this.tool.setMode(mode | this.flag);
128
- } else {
129
- this.tool.setMode(mode & ~this.flag);
130
- }
127
+ this.tool.setModeEnabled(this.flag, enabled);
131
128
  }
132
129
 
133
130
  protected handleClick() {
@@ -148,7 +145,6 @@ class HandModeWidget extends BaseWidget {
148
145
  }
149
146
 
150
147
  export default class HandToolWidget extends BaseToolWidget {
151
- private touchPanningWidget: HandModeWidget;
152
148
  private allowTogglingBaseTool: boolean;
153
149
 
154
150
  public constructor(
@@ -162,7 +158,7 @@ export default class HandToolWidget extends BaseToolWidget {
162
158
  ) {
163
159
  const primaryHandTool = HandToolWidget.getPrimaryHandTool(editor.toolController);
164
160
  const tool = primaryHandTool ?? overridePanZoomTool;
165
- super(editor, tool, localizationTable);
161
+ super(editor, tool, 'hand-tool-widget', localizationTable);
166
162
 
167
163
  // Only allow toggling a hand tool if we're using the primary hand tool and not the override
168
164
  // hand tool for this button.
@@ -174,16 +170,29 @@ export default class HandToolWidget extends BaseToolWidget {
174
170
  }
175
171
 
176
172
  // Controls for the overriding hand tool.
177
- this.touchPanningWidget = new HandModeWidget(
178
- editor, localizationTable,
173
+ const touchPanningWidget = new HandModeWidget(
174
+ editor,
179
175
 
180
176
  overridePanZoomTool, PanZoomMode.OneFingerTouchGestures,
181
177
  () => this.editor.icons.makeTouchPanningIcon(),
182
178
 
183
- localizationTable.touchPanning
179
+ localizationTable.touchPanning,
180
+
181
+ localizationTable,
184
182
  );
183
+
184
+ const rotationLockWidget = new HandModeWidget(
185
+ editor,
185
186
 
186
- this.addSubWidget(this.touchPanningWidget);
187
+ overridePanZoomTool, PanZoomMode.RotationLocked,
188
+ () => this.editor.icons.makeRotationLockIcon(),
189
+
190
+ localizationTable.lockRotation,
191
+ localizationTable,
192
+ );
193
+
194
+ this.addSubWidget(touchPanningWidget);
195
+ this.addSubWidget(rotationLockWidget);
187
196
  this.addSubWidget(
188
197
  new ZoomWidget(editor, localizationTable)
189
198
  );
@@ -216,4 +225,26 @@ export default class HandToolWidget extends BaseToolWidget {
216
225
  super.setSelected(selected);
217
226
  }
218
227
  }
228
+
229
+ public serializeState(): SavedToolbuttonState {
230
+ const toolMode = this.overridePanZoomTool.getMode();
231
+
232
+ return {
233
+ ...super.serializeState(),
234
+ touchPanning: toolMode & PanZoomMode.OneFingerTouchGestures,
235
+ rotationLocked: toolMode & PanZoomMode.RotationLocked,
236
+ };
237
+ }
238
+
239
+ public deserializeFrom(state: SavedToolbuttonState): void {
240
+ if (state.touchPanning !== undefined) {
241
+ this.overridePanZoomTool.setModeEnabled(PanZoomMode.OneFingerTouchGestures, state.touchPanning);
242
+ }
243
+
244
+ if (state.rotationLocked !== undefined) {
245
+ this.overridePanZoomTool.setModeEnabled(PanZoomMode.RotationLocked, state.rotationLocked);
246
+ }
247
+
248
+ super.deserializeFrom(state);
249
+ }
219
250
  }
@@ -11,12 +11,17 @@ import { toolbarCSSPrefix } from '../HTMLToolbar';
11
11
  import { ToolbarLocalization } from '../localization';
12
12
  import makeColorInput from '../makeColorInput';
13
13
  import BaseToolWidget from './BaseToolWidget';
14
+ import Color4 from '../../Color4';
15
+ import { SavedToolbuttonState } from './BaseWidget';
14
16
 
15
17
 
16
18
  export interface PenTypeRecord {
17
19
  // Description of the factory (e.g. 'Freehand line')
18
20
  name: string;
19
21
 
22
+ // A unique ID for the facotory (e.g. 'chisel-tip-pen')
23
+ id: string;
24
+
20
25
  // Creates an `AbstractComponent` from pen input.
21
26
  factory: ComponentBuilderFactory;
22
27
  }
@@ -26,34 +31,46 @@ export default class PenToolWidget extends BaseToolWidget {
26
31
  protected penTypes: PenTypeRecord[];
27
32
 
28
33
  public constructor(
29
- editor: Editor, private tool: Pen, localization: ToolbarLocalization
34
+ editor: Editor, private tool: Pen, localization?: ToolbarLocalization
30
35
  ) {
31
- super(editor, tool, localization);
36
+ super(editor, tool, 'pen', localization);
32
37
 
33
38
  // Default pen types
34
39
  this.penTypes = [
35
40
  {
36
- name: localization.pressureSensitiveFreehandPen,
41
+ name: this.localizationTable.pressureSensitiveFreehandPen,
42
+ id: 'pressure-sensitive-pen',
43
+
37
44
  factory: makePressureSensitiveFreehandLineBuilder,
38
45
  },
39
46
  {
40
- name: localization.freehandPen,
47
+ name: this.localizationTable.freehandPen,
48
+ id: 'freehand-pen',
49
+
41
50
  factory: makeFreehandLineBuilder,
42
51
  },
43
52
  {
44
- name: localization.arrowPen,
53
+ name: this.localizationTable.arrowPen,
54
+ id: 'arrow',
55
+
45
56
  factory: makeArrowBuilder,
46
57
  },
47
58
  {
48
- name: localization.linePen,
59
+ name: this.localizationTable.linePen,
60
+ id: 'line',
61
+
49
62
  factory: makeLineBuilder,
50
63
  },
51
64
  {
52
- name: localization.filledRectanglePen,
65
+ name: this.localizationTable.filledRectanglePen,
66
+ id: 'filled-rectangle',
67
+
53
68
  factory: makeFilledRectangleBuilder,
54
69
  },
55
70
  {
56
- name: localization.outlinedRectanglePen,
71
+ name: this.localizationTable.outlinedRectanglePen,
72
+ id: 'outlined-rectangle',
73
+
57
74
  factory: makeOutlinedRectangleBuilder,
58
75
  },
59
76
  ];
@@ -75,6 +92,30 @@ export default class PenToolWidget extends BaseToolWidget {
75
92
  return this.targetTool.description;
76
93
  }
77
94
 
95
+ // Return the index of this tool's stroke factory in the list of
96
+ // all stroke factories.
97
+ //
98
+ // Returns -1 if the stroke factory is not in the list of all stroke factories.
99
+ private getCurrentPenTypeIdx(): number {
100
+ const currentFactory = this.tool.getStrokeFactory();
101
+
102
+ for (let i = 0; i < this.penTypes.length; i ++) {
103
+ if (this.penTypes[i].factory === currentFactory) {
104
+ return i;
105
+ }
106
+ }
107
+ return -1;
108
+ }
109
+
110
+ private getCurrentPenType(): PenTypeRecord|null {
111
+ for (const penType of this.penTypes) {
112
+ if (penType.factory === this.tool.getStrokeFactory()) {
113
+ return penType;
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+
78
119
  protected createIcon(): Element {
79
120
  const strokeFactory = this.tool.getStrokeFactory();
80
121
  if (strokeFactory === makeFreehandLineBuilder || strokeFactory === makePressureSensitiveFreehandLineBuilder) {
@@ -138,7 +179,7 @@ export default class PenToolWidget extends BaseToolWidget {
138
179
 
139
180
  const colorRow = document.createElement('div');
140
181
  const colorLabel = document.createElement('label');
141
- const [ colorInput, colorInputContainer ] = makeColorInput(this.editor, color => {
182
+ const [ colorInput, colorInputContainer, setColorInputValue ] = makeColorInput(this.editor, color => {
142
183
  this.tool.setColor(color);
143
184
  });
144
185
 
@@ -150,9 +191,10 @@ export default class PenToolWidget extends BaseToolWidget {
150
191
  colorRow.appendChild(colorInputContainer);
151
192
 
152
193
  this.updateInputs = () => {
153
- colorInput.value = this.tool.getColor().toHexString();
194
+ setColorInputValue(this.tool.getColor());
154
195
  thicknessInput.value = inverseThicknessInputFn(this.tool.getThickness()).toString();
155
196
 
197
+ // Update the list of stroke factories
156
198
  objectTypeSelect.replaceChildren();
157
199
  for (let i = 0; i < this.penTypes.length; i ++) {
158
200
  const penType = this.penTypes[i];
@@ -161,10 +203,14 @@ export default class PenToolWidget extends BaseToolWidget {
161
203
  option.innerText = penType.name;
162
204
 
163
205
  objectTypeSelect.appendChild(option);
206
+ }
164
207
 
165
- if (penType.factory === this.tool.getStrokeFactory()) {
166
- objectTypeSelect.value = i.toString();
167
- }
208
+ // Update the selected stroke factory.
209
+ const strokeFactoryIdx = this.getCurrentPenTypeIdx();
210
+ if (strokeFactoryIdx === -1) {
211
+ objectTypeSelect.value = '';
212
+ } else {
213
+ objectTypeSelect.value = strokeFactoryIdx.toString();
168
214
  }
169
215
  };
170
216
  this.updateInputs();
@@ -190,4 +236,50 @@ export default class PenToolWidget extends BaseToolWidget {
190
236
 
191
237
  return false;
192
238
  }
239
+
240
+ public serializeState(): SavedToolbuttonState {
241
+ return {
242
+ ...super.serializeState(),
243
+
244
+ color: this.tool.getColor().toHexString(),
245
+ thickness: this.tool.getThickness(),
246
+ strokeFactoryId: this.getCurrentPenType()?.id,
247
+ };
248
+ }
249
+
250
+ public deserializeFrom(state: SavedToolbuttonState) {
251
+ super.deserializeFrom(state);
252
+
253
+ const verifyPropertyType = (propertyName: string, expectedType: 'string'|'number'|'object') => {
254
+ const actualType = typeof(state[propertyName]);
255
+ if (actualType !== expectedType) {
256
+ throw new Error(
257
+ `Deserializing property ${propertyName}: Invalid type. Expected ${expectedType},` +
258
+ ` was ${actualType}.`
259
+ );
260
+ }
261
+ };
262
+
263
+ if (state.color) {
264
+ verifyPropertyType('color', 'string');
265
+ this.tool.setColor(Color4.fromHex(state.color));
266
+ }
267
+
268
+ if (state.thickness) {
269
+ verifyPropertyType('thickness', 'number');
270
+ this.tool.setThickness(state.thickness);
271
+ }
272
+
273
+ if (state.strokeFactoryId) {
274
+ verifyPropertyType('strokeFactoryId', 'string');
275
+
276
+ const factoryId: string = state.strokeFactoryId;
277
+ for (const penType of this.penTypes) {
278
+ if (factoryId === penType.id) {
279
+ this.tool.setStrokeFactory(penType.factory);
280
+ break;
281
+ }
282
+ }
283
+ }
284
+ }
193
285
  }
@@ -7,20 +7,21 @@ import BaseToolWidget from './BaseToolWidget';
7
7
 
8
8
  export default class SelectionToolWidget extends BaseToolWidget {
9
9
  public constructor(
10
- editor: Editor, private tool: SelectionTool, localization: ToolbarLocalization
10
+ editor: Editor, private tool: SelectionTool, localization?: ToolbarLocalization
11
11
  ) {
12
- super(editor, tool, localization);
12
+ super(editor, tool, 'selection-tool-widget', localization);
13
13
 
14
14
  const resizeButton = new ActionButtonWidget(
15
- editor, localization,
15
+ editor, 'resize-btn',
16
16
  () => editor.icons.makeResizeViewportIcon(),
17
17
  this.localizationTable.resizeImageToSelection,
18
18
  () => {
19
19
  this.resizeImageToSelection();
20
20
  },
21
+ localization,
21
22
  );
22
23
  const deleteButton = new ActionButtonWidget(
23
- editor, localization,
24
+ editor, 'delete-btn',
24
25
  () => editor.icons.makeDeleteSelectionIcon(),
25
26
  this.localizationTable.deleteSelection,
26
27
  () => {
@@ -28,15 +29,17 @@ export default class SelectionToolWidget extends BaseToolWidget {
28
29
  this.editor.dispatch(selection!.deleteSelectedObjects());
29
30
  this.tool.clearSelection();
30
31
  },
32
+ localization,
31
33
  );
32
34
  const duplicateButton = new ActionButtonWidget(
33
- editor, localization,
35
+ editor, 'duplicate-btn',
34
36
  () => editor.icons.makeDuplicateSelectionIcon(),
35
37
  this.localizationTable.duplicateSelection,
36
38
  () => {
37
39
  const selection = this.tool.getSelection();
38
40
  this.editor.dispatch(selection!.duplicateSelectedObjects());
39
41
  },
42
+ localization,
40
43
  );
41
44
 
42
45
  this.addSubWidget(resizeButton);
@@ -1,3 +1,4 @@
1
+ import Color4 from '../../Color4';
1
2
  import Editor from '../../Editor';
2
3
  import TextTool from '../../tools/TextTool';
3
4
  import { EditorEventType } from '../../types';
@@ -5,11 +6,12 @@ import { toolbarCSSPrefix } from '../HTMLToolbar';
5
6
  import { ToolbarLocalization } from '../localization';
6
7
  import makeColorInput from '../makeColorInput';
7
8
  import BaseToolWidget from './BaseToolWidget';
9
+ import { SavedToolbuttonState } from './BaseWidget';
8
10
 
9
11
  export default class TextToolWidget extends BaseToolWidget {
10
12
  private updateDropdownInputs: (()=>void)|null = null;
11
- public constructor(editor: Editor, private tool: TextTool, localization: ToolbarLocalization) {
12
- super(editor, tool, localization);
13
+ public constructor(editor: Editor, private tool: TextTool, localization?: ToolbarLocalization) {
14
+ super(editor, tool, 'text-tool-widget', localization);
13
15
 
14
16
  editor.notifier.on(EditorEventType.ToolUpdated, evt => {
15
17
  if (evt.kind === EditorEventType.ToolUpdated && evt.tool === tool) {
@@ -36,7 +38,7 @@ export default class TextToolWidget extends BaseToolWidget {
36
38
  const fontInput = document.createElement('select');
37
39
  const fontLabel = document.createElement('label');
38
40
 
39
- const [ colorInput, colorInputContainer ] = makeColorInput(this.editor, color => {
41
+ const [ colorInput, colorInputContainer, setColorInputValue ] = makeColorInput(this.editor, color => {
40
42
  this.tool.setColor(color);
41
43
  });
42
44
  const colorLabel = document.createElement('label');
@@ -74,7 +76,7 @@ export default class TextToolWidget extends BaseToolWidget {
74
76
 
75
77
  this.updateDropdownInputs = () => {
76
78
  const style = this.tool.getTextStyle();
77
- colorInput.value = style.renderingStyle.fill.toHexString();
79
+ setColorInputValue(style.renderingStyle.fill);
78
80
 
79
81
  if (!fontsInInput.has(style.fontFamily)) {
80
82
  addFontToInput(style.fontFamily);
@@ -86,4 +88,27 @@ export default class TextToolWidget extends BaseToolWidget {
86
88
  dropdown.replaceChildren(colorRow, fontRow);
87
89
  return true;
88
90
  }
91
+
92
+ public serializeState(): SavedToolbuttonState {
93
+ const textStyle = this.tool.getTextStyle();
94
+
95
+ return {
96
+ ...super.serializeState(),
97
+
98
+ fontFamily: textStyle.fontFamily,
99
+ color: textStyle.renderingStyle.fill.toHexString(),
100
+ };
101
+ }
102
+
103
+ public deserializeFrom(state: SavedToolbuttonState) {
104
+ if (state.fontFamily && typeof(state.fontFamily) === 'string') {
105
+ this.tool.setFontFamily(state.fontFamily);
106
+ }
107
+
108
+ if (state.color && typeof(state.color) === 'string') {
109
+ this.tool.setColor(Color4.fromHex(state.color));
110
+ }
111
+
112
+ super.deserializeFrom(state);
113
+ }
89
114
  }
@@ -21,6 +21,8 @@ export enum PanZoomMode {
21
21
  RightClickDrags = 0x1 << 2,
22
22
  SinglePointerGestures = 0x1 << 3,
23
23
  Keyboard = 0x1 << 4,
24
+
25
+ RotationLocked = 0x1 << 5,
24
26
  }
25
27
 
26
28
  export default class PanZoom extends BaseTool {
@@ -90,10 +92,15 @@ export default class PanZoom extends BaseTool {
90
92
  const { screenCenter, canvasCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
91
93
 
92
94
  const delta = this.getCenterDelta(screenCenter);
95
+ let rotation = angle - this.lastAngle;
96
+
97
+ if (this.isRotationLocked()) {
98
+ rotation = 0;
99
+ }
93
100
 
94
101
  const transformUpdate = Mat33.translation(delta)
95
102
  .rightMul(Mat33.scaling2D(dist / this.lastDist, canvasCenter))
96
- .rightMul(Mat33.zRotation(angle - this.lastAngle, canvasCenter));
103
+ .rightMul(Mat33.zRotation(rotation, canvasCenter));
97
104
  this.lastScreenCenter = screenCenter;
98
105
  this.lastDist = dist;
99
106
  this.lastAngle = angle;
@@ -250,6 +257,10 @@ export default class PanZoom extends BaseTool {
250
257
  rotation += 0.0001;
251
258
  }
252
259
 
260
+ if (this.isRotationLocked()) {
261
+ rotation = 0;
262
+ }
263
+
253
264
  const toCanvas = this.editor.viewport.screenToCanvasTransform;
254
265
 
255
266
  // Transform without translating (treat toCanvas as a linear instead of
@@ -270,6 +281,22 @@ export default class PanZoom extends BaseTool {
270
281
  return true;
271
282
  }
272
283
 
284
+ private isRotationLocked(): boolean {
285
+ return !!(this.mode & PanZoomMode.RotationLocked);
286
+ }
287
+
288
+ // Sets whether the given `mode` is enabled. `mode` should be a single
289
+ // mode from the `PanZoomMode` enum.
290
+ public setModeEnabled(mode: PanZoomMode, enabled: boolean) {
291
+ let newMode = this.mode;
292
+ if (enabled) {
293
+ newMode |= mode;
294
+ } else {
295
+ newMode &= ~mode;
296
+ }
297
+ this.setMode(newMode);
298
+ }
299
+
273
300
  public setMode(mode: PanZoomMode) {
274
301
  if (mode !== this.mode) {
275
302
  this.mode = mode;