js-draw 0.9.2 → 0.10.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 (50) hide show
  1. package/.firebase/hosting.ZG9jcw.cache +338 -0
  2. package/.github/ISSUE_TEMPLATE/translation.yml +9 -1
  3. package/CHANGELOG.md +12 -0
  4. package/build_tools/buildTranslationTemplate.ts +6 -4
  5. package/dist/build_tools/buildTranslationTemplate.js +5 -4
  6. package/dist/bundle.js +1 -1
  7. package/dist/src/Color4.d.ts +1 -0
  8. package/dist/src/Color4.js +34 -15
  9. package/dist/src/Editor.js +2 -3
  10. package/dist/src/SVGLoader.js +25 -11
  11. package/dist/src/components/builders/FreehandLineBuilder.d.ts +2 -0
  12. package/dist/src/components/builders/FreehandLineBuilder.js +12 -4
  13. package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.js +1 -1
  14. package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -0
  15. package/dist/src/rendering/renderers/SVGRenderer.js +24 -7
  16. package/dist/src/toolbar/HTMLToolbar.d.ts +6 -1
  17. package/dist/src/toolbar/HTMLToolbar.js +24 -27
  18. package/dist/src/toolbar/localization.d.ts +1 -0
  19. package/dist/src/toolbar/localization.js +1 -0
  20. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -3
  21. package/dist/src/toolbar/widgets/BaseWidget.d.ts +1 -1
  22. package/dist/src/toolbar/widgets/BaseWidget.js +9 -4
  23. package/dist/src/toolbar/widgets/TextToolWidget.js +23 -2
  24. package/dist/src/tools/PanZoom.d.ts +5 -1
  25. package/dist/src/tools/PanZoom.js +108 -10
  26. package/dist/src/tools/SelectionTool/SelectionHandle.js +1 -1
  27. package/dist/src/tools/TextTool.js +8 -2
  28. package/dist/src/{language → util}/assertions.d.ts +0 -0
  29. package/dist/src/{language → util}/assertions.js +1 -0
  30. package/dist/src/util/untilNextAnimationFrame.d.ts +3 -0
  31. package/dist/src/util/untilNextAnimationFrame.js +7 -0
  32. package/package.json +16 -16
  33. package/src/Color4.test.ts +7 -0
  34. package/src/Color4.ts +47 -18
  35. package/src/Editor.toSVG.test.ts +84 -0
  36. package/src/Editor.ts +2 -3
  37. package/src/SVGLoader.ts +26 -10
  38. package/src/components/builders/FreehandLineBuilder.ts +14 -4
  39. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +1 -1
  40. package/src/rendering/renderers/SVGRenderer.ts +24 -6
  41. package/src/toolbar/HTMLToolbar.ts +33 -30
  42. package/src/toolbar/localization.ts +2 -0
  43. package/src/toolbar/widgets/ActionButtonWidget.ts +2 -2
  44. package/src/toolbar/widgets/BaseWidget.ts +9 -4
  45. package/src/toolbar/widgets/TextToolWidget.ts +29 -1
  46. package/src/tools/PanZoom.ts +124 -7
  47. package/src/tools/SelectionTool/SelectionHandle.ts +1 -1
  48. package/src/tools/TextTool.ts +10 -2
  49. package/src/{language → util}/assertions.ts +1 -0
  50. package/src/util/untilNextAnimationFrame.ts +9 -0
@@ -30,6 +30,7 @@ export default class Color4 {
30
30
  * ```
31
31
  */
32
32
  toHexString(): string;
33
+ toString(): string;
33
34
  static transparent: Color4;
34
35
  static red: Color4;
35
36
  static green: Color4;
@@ -63,24 +63,40 @@ export default class Color4 {
63
63
  if (text.startsWith('#')) {
64
64
  return Color4.fromHex(text);
65
65
  }
66
- else if (text === 'none' || text === 'transparent') {
66
+ if (text === 'none' || text === 'transparent') {
67
67
  return Color4.transparent;
68
68
  }
69
- else {
70
- // Otherwise, try to use an HTML5Canvas to determine the color
71
- const canvas = document.createElement('canvas');
72
- canvas.width = 1;
73
- canvas.height = 1;
74
- const ctx = canvas.getContext('2d');
75
- ctx.fillStyle = text;
76
- ctx.fillRect(0, 0, 1, 1);
77
- const data = ctx.getImageData(0, 0, 1, 1);
78
- const red = data.data[0] / 255;
79
- const green = data.data[1] / 255;
80
- const blue = data.data[2] / 255;
81
- const alpha = data.data[3] / 255;
82
- return Color4.ofRGBA(red, green, blue, alpha);
69
+ // rgba?: Match both rgb and rgba strings.
70
+ // ([,0-9.]+): Match any string of only numeric, '.' and ',' characters.
71
+ const rgbRegex = /^rgba?\(([,0-9.]+)\)$/i;
72
+ const rgbMatch = text.replace(/\s*/g, '').match(rgbRegex);
73
+ if (rgbMatch) {
74
+ const componentsListStr = rgbMatch[1];
75
+ const componentsList = JSON.parse(`[ ${componentsListStr} ]`);
76
+ if (componentsList.length === 3) {
77
+ return Color4.ofRGB(componentsList[0] / 255, componentsList[1] / 255, componentsList[2] / 255);
78
+ }
79
+ else if (componentsList.length === 4) {
80
+ return Color4.ofRGBA(componentsList[0] / 255, componentsList[1] / 255, componentsList[2] / 255, componentsList[3]);
81
+ }
82
+ else {
83
+ throw new Error(`RGB string, ${text}, has wrong number of components: ${componentsList.length}`);
84
+ }
83
85
  }
86
+ // Otherwise, try to use an HTMLCanvasElement to determine the color.
87
+ // Note: We may be unable to create an HTMLCanvasElement if running as a unit test.
88
+ const canvas = document.createElement('canvas');
89
+ canvas.width = 1;
90
+ canvas.height = 1;
91
+ const ctx = canvas.getContext('2d');
92
+ ctx.fillStyle = text;
93
+ ctx.fillRect(0, 0, 1, 1);
94
+ const data = ctx.getImageData(0, 0, 1, 1);
95
+ const red = data.data[0] / 255;
96
+ const green = data.data[1] / 255;
97
+ const blue = data.data[2] / 255;
98
+ const alpha = data.data[3] / 255;
99
+ return Color4.ofRGBA(red, green, blue, alpha);
84
100
  }
85
101
  /** @returns true if `this` and `other` are approximately equal. */
86
102
  eq(other) {
@@ -118,6 +134,9 @@ export default class Color4 {
118
134
  this.hexString = `#${red}${green}${blue}${alpha}`;
119
135
  return this.hexString;
120
136
  }
137
+ toString() {
138
+ return this.toHexString();
139
+ }
121
140
  }
122
141
  Color4.transparent = Color4.ofRGBA(0, 0, 0, 0);
123
142
  Color4.red = Color4.ofRGB(1.0, 0.0, 0.0);
@@ -44,6 +44,7 @@ import getLocalizationTable from './localizations/getLocalizationTable';
44
44
  import IconProvider from './toolbar/IconProvider';
45
45
  import { toRoundedString } from './math/rounding';
46
46
  import CanvasRenderer from './rendering/renderers/CanvasRenderer';
47
+ import untilNextAnimationFrame from './util/untilNextAnimationFrame';
47
48
  // { @inheritDoc Editor! }
48
49
  export class Editor {
49
50
  /**
@@ -676,9 +677,7 @@ export class Editor {
676
677
  if (countProcessed % 500 === 0) {
677
678
  this.showLoadingWarning(countProcessed / totalToProcess);
678
679
  this.rerender();
679
- return new Promise(resolve => {
680
- requestAnimationFrame(() => resolve());
681
- });
680
+ return untilNextAnimationFrame();
682
681
  }
683
682
  return null;
684
683
  }, (importExportRect) => {
@@ -34,12 +34,14 @@ export default class SVGLoader {
34
34
  this.processedCount = 0;
35
35
  this.totalToProcess = 0;
36
36
  }
37
- getStyle(node) {
38
- var _a, _b, _c;
37
+ // If [computedStyles] is given, it is preferred to directly accessing node's style object.
38
+ getStyle(node, computedStyles) {
39
+ var _a, _b, _c, _d, _f, _g;
39
40
  const style = {
40
41
  fill: Color4.transparent,
41
42
  };
42
- const fillAttribute = (_a = node.getAttribute('fill')) !== null && _a !== void 0 ? _a : node.style.fill;
43
+ // If possible, use computedStyles (allows property inheritance).
44
+ const fillAttribute = (_b = (_a = node.getAttribute('fill')) !== null && _a !== void 0 ? _a : computedStyles === null || computedStyles === void 0 ? void 0 : computedStyles.fill) !== null && _b !== void 0 ? _b : node.style.fill;
43
45
  if (fillAttribute) {
44
46
  try {
45
47
  style.fill = Color4.fromString(fillAttribute);
@@ -48,18 +50,21 @@ export default class SVGLoader {
48
50
  console.error('Unknown fill color,', fillAttribute);
49
51
  }
50
52
  }
51
- const strokeAttribute = (_b = node.getAttribute('stroke')) !== null && _b !== void 0 ? _b : node.style.stroke;
52
- const strokeWidthAttr = (_c = node.getAttribute('stroke-width')) !== null && _c !== void 0 ? _c : node.style.strokeWidth;
53
- if (strokeAttribute) {
53
+ const strokeAttribute = (_d = (_c = node.getAttribute('stroke')) !== null && _c !== void 0 ? _c : computedStyles === null || computedStyles === void 0 ? void 0 : computedStyles.stroke) !== null && _d !== void 0 ? _d : node.style.stroke;
54
+ const strokeWidthAttr = (_g = (_f = node.getAttribute('stroke-width')) !== null && _f !== void 0 ? _f : computedStyles === null || computedStyles === void 0 ? void 0 : computedStyles.strokeWidth) !== null && _g !== void 0 ? _g : node.style.strokeWidth;
55
+ if (strokeAttribute && strokeWidthAttr) {
54
56
  try {
55
57
  let width = parseFloat(strokeWidthAttr !== null && strokeWidthAttr !== void 0 ? strokeWidthAttr : '1');
56
58
  if (!isFinite(width)) {
57
59
  width = 0;
58
60
  }
59
- style.stroke = {
60
- width,
61
- color: Color4.fromString(strokeAttribute),
62
- };
61
+ const strokeColor = Color4.fromString(strokeAttribute);
62
+ if (strokeColor.a > 0) {
63
+ style.stroke = {
64
+ width,
65
+ color: strokeColor,
66
+ };
67
+ }
63
68
  }
64
69
  catch (e) {
65
70
  console.error('Error parsing stroke data:', e);
@@ -188,6 +193,10 @@ export default class SVGLoader {
188
193
  throw new Error(`Unrecognized text child node: ${child}.`);
189
194
  }
190
195
  }
196
+ // If no content, the content is an empty string.
197
+ if (contentList.length === 0) {
198
+ contentList.push('');
199
+ }
191
200
  // Compute styles.
192
201
  const computedStyles = window.getComputedStyle(elem);
193
202
  const fontSizeMatch = /^([-0-9.e]+)px/i.exec(computedStyles.fontSize);
@@ -204,7 +213,7 @@ export default class SVGLoader {
204
213
  const style = {
205
214
  size: fontSize,
206
215
  fontFamily: computedStyles.fontFamily || elem.style.fontFamily || 'sans-serif',
207
- renderingStyle: this.getStyle(elem),
216
+ renderingStyle: this.getStyle(elem, computedStyles),
208
217
  };
209
218
  const supportedAttrs = [];
210
219
  const transform = this.getTransform(elem, supportedAttrs, computedStyles);
@@ -297,6 +306,9 @@ export default class SVGLoader {
297
306
  this.updateViewBox(node);
298
307
  this.updateSVGAttrs(node);
299
308
  break;
309
+ case 'style':
310
+ this.addUnknownNode(node);
311
+ break;
300
312
  default:
301
313
  console.warn('Unknown SVG element,', node);
302
314
  if (!(node instanceof SVGElement)) {
@@ -361,6 +373,8 @@ export default class SVGLoader {
361
373
  <html>
362
374
  <head>
363
375
  <title>SVG Loading Sandbox</title>
376
+ <meta name='viewport' conent='width=device-width,initial-scale=1.0'/>
377
+ <meta charset='utf-8'/>
364
378
  </head>
365
379
  <body>
366
380
  <script>
@@ -24,7 +24,9 @@ export default class FreehandLineBuilder implements ComponentBuilder {
24
24
  private previewStroke;
25
25
  preview(renderer: AbstractRenderer): void;
26
26
  build(): Stroke;
27
+ private getMinFit;
27
28
  private roundPoint;
29
+ private roundDistance;
28
30
  private curveToPathCommands;
29
31
  private addCurve;
30
32
  addPoint(newPoint: StrokeDataPoint): void;
@@ -33,7 +33,7 @@ export default class FreehandLineBuilder {
33
33
  fill: Color4.transparent,
34
34
  stroke: {
35
35
  color: this.startPoint.color,
36
- width: this.averageWidth,
36
+ width: this.roundDistance(this.averageWidth / 2),
37
37
  }
38
38
  };
39
39
  }
@@ -76,13 +76,21 @@ export default class FreehandLineBuilder {
76
76
  this.curveFitter.finalizeCurrentCurve();
77
77
  return this.previewStroke();
78
78
  }
79
- roundPoint(point) {
80
- let minFit = Math.min(this.minFitAllowed, this.averageWidth / 2);
79
+ getMinFit() {
80
+ let minFit = Math.min(this.minFitAllowed, this.averageWidth / 5);
81
81
  if (minFit < 1e-10) {
82
82
  minFit = this.minFitAllowed;
83
83
  }
84
+ return minFit;
85
+ }
86
+ roundPoint(point) {
87
+ const minFit = this.getMinFit();
84
88
  return Viewport.roundPoint(point, minFit);
85
89
  }
90
+ roundDistance(dist) {
91
+ const minFit = this.getMinFit();
92
+ return Viewport.roundPoint(dist, minFit);
93
+ }
86
94
  curveToPathCommands(curve) {
87
95
  // Case where no points have been added
88
96
  if (!curve) {
@@ -90,7 +98,7 @@ export default class FreehandLineBuilder {
90
98
  if (!this.isFirstSegment) {
91
99
  return [];
92
100
  }
93
- const width = Viewport.roundPoint(this.startPoint.width / 3.5, Math.min(this.minFitAllowed, this.startPoint.width / 4));
101
+ const width = Viewport.roundPoint(this.startPoint.width / 9, Math.min(this.minFitAllowed, this.startPoint.width / 5));
94
102
  const center = this.roundPoint(this.startPoint.pos);
95
103
  // Start on the right, cycle clockwise:
96
104
  // |
@@ -141,7 +141,7 @@ export default class PressureSensitiveFreehandLineBuilder {
141
141
  return this.previewStroke();
142
142
  }
143
143
  roundPoint(point) {
144
- let minFit = Math.min(this.minFitAllowed, this.curveStartWidth / 2);
144
+ let minFit = Math.min(this.minFitAllowed, this.curveStartWidth / 3);
145
145
  if (minFit < 1e-10) {
146
146
  minFit = this.minFitAllowed;
147
147
  }
@@ -24,6 +24,7 @@ export default class SVGRenderer extends AbstractRenderer {
24
24
  private transformFrom;
25
25
  private textContainer;
26
26
  private textContainerTransform;
27
+ private textParentStyle;
27
28
  drawText(text: string, transform: Mat33, style: TextStyle): void;
28
29
  drawImage(image: RenderableImage): void;
29
30
  startObject(boundingBox: Rect2): void;
@@ -18,6 +18,7 @@ export default class SVGRenderer extends AbstractRenderer {
18
18
  this.overwrittenAttrs = {};
19
19
  this.textContainer = null;
20
20
  this.textContainerTransform = null;
21
+ this.textParentStyle = null;
21
22
  this.clear();
22
23
  this.addStyleSheet();
23
24
  }
@@ -87,7 +88,7 @@ export default class SVGRenderer extends AbstractRenderer {
87
88
  }
88
89
  if (style.stroke) {
89
90
  pathElem.setAttribute('stroke', style.stroke.color.toHexString());
90
- pathElem.setAttribute('stroke-width', style.stroke.width.toString());
91
+ pathElem.setAttribute('stroke-width', toRoundedString(style.stroke.width));
91
92
  }
92
93
  this.elem.appendChild(pathElem);
93
94
  (_a = this.objectElems) === null || _a === void 0 ? void 0 : _a.push(pathElem);
@@ -130,12 +131,26 @@ export default class SVGRenderer extends AbstractRenderer {
130
131
  drawText(text, transform, style) {
131
132
  var _a;
132
133
  const applyTextStyles = (elem, style) => {
133
- var _a, _b;
134
- elem.style.fontFamily = style.fontFamily;
135
- elem.style.fontVariant = (_a = style.fontVariant) !== null && _a !== void 0 ? _a : '';
136
- elem.style.fontWeight = (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '';
137
- elem.style.fontSize = style.size + 'px';
138
- elem.style.fill = style.renderingStyle.fill.toHexString();
134
+ var _a, _b, _c, _d, _e, _f;
135
+ if (style.fontFamily !== ((_a = this.textParentStyle) === null || _a === void 0 ? void 0 : _a.fontFamily)) {
136
+ elem.style.fontFamily = style.fontFamily;
137
+ }
138
+ if (style.fontVariant !== ((_b = this.textParentStyle) === null || _b === void 0 ? void 0 : _b.fontVariant)) {
139
+ elem.style.fontVariant = (_c = style.fontVariant) !== null && _c !== void 0 ? _c : '';
140
+ }
141
+ if (style.fontWeight !== ((_d = this.textParentStyle) === null || _d === void 0 ? void 0 : _d.fontWeight)) {
142
+ elem.style.fontWeight = (_e = style.fontWeight) !== null && _e !== void 0 ? _e : '';
143
+ }
144
+ if (style.size !== ((_f = this.textParentStyle) === null || _f === void 0 ? void 0 : _f.size)) {
145
+ elem.style.fontSize = style.size + 'px';
146
+ }
147
+ const fillString = style.renderingStyle.fill.toHexString();
148
+ // TODO: Uncomment at some future major version release --- currently causes incompatibility due
149
+ // to an SVG parsing bug in older versions.
150
+ //const parentFillString = this.textParentStyle?.renderingStyle?.fill?.toHexString();
151
+ //if (fillString !== parentFillString) {
152
+ elem.style.fill = fillString;
153
+ //}
139
154
  if (style.renderingStyle.stroke) {
140
155
  const strokeStyle = style.renderingStyle.stroke;
141
156
  elem.style.stroke = strokeStyle.color.toHexString();
@@ -156,6 +171,7 @@ export default class SVGRenderer extends AbstractRenderer {
156
171
  if (this.objectLevel > 0) {
157
172
  this.textContainer = container;
158
173
  this.textContainerTransform = transform;
174
+ this.textParentStyle = style;
159
175
  }
160
176
  }
161
177
  else {
@@ -188,6 +204,7 @@ export default class SVGRenderer extends AbstractRenderer {
188
204
  this.lastPathString = [];
189
205
  this.lastPathStyle = null;
190
206
  this.textContainer = null;
207
+ this.textParentStyle = null;
191
208
  this.objectElems = [];
192
209
  }
193
210
  endObject(loaderData) {
@@ -16,7 +16,12 @@ export default class HTMLToolbar {
16
16
  addWidget(widget: BaseWidget): void;
17
17
  serializeState(): string;
18
18
  deserializeState(state: string): void;
19
- addActionButton(title: string | ActionButtonIcon, command: () => void, parent?: Element): HTMLButtonElement;
19
+ /**
20
+ * Adds an action button with `title` to this toolbar (or to the given `parent` element).
21
+ *
22
+ * @return The added button.
23
+ */
24
+ addActionButton(title: string | ActionButtonIcon, command: () => void): BaseWidget;
20
25
  addUndoRedoButtons(): void;
21
26
  addDefaultToolWidgets(): void;
22
27
  addDefaultActionButtons(): void;
@@ -12,6 +12,7 @@ import EraserWidget from './widgets/EraserToolWidget';
12
12
  import SelectionToolWidget from './widgets/SelectionToolWidget';
13
13
  import TextToolWidget from './widgets/TextToolWidget';
14
14
  import HandToolWidget from './widgets/HandToolWidget';
15
+ import { ActionButtonWidget } from './lib';
15
16
  export const toolbarCSSPrefix = 'toolbar-';
16
17
  export default class HTMLToolbar {
17
18
  /** @internal */
@@ -122,49 +123,45 @@ export default class HTMLToolbar {
122
123
  this.widgets[widgetId].deserializeFrom(data[widgetId]);
123
124
  }
124
125
  }
125
- addActionButton(title, command, parent) {
126
- const button = document.createElement('button');
127
- button.classList.add(`${toolbarCSSPrefix}button`);
128
- if (typeof title === 'string') {
129
- button.innerText = title;
130
- }
131
- else {
132
- const iconElem = title.icon.cloneNode(true);
133
- const labelElem = document.createElement('label');
134
- // Use the label to describe the icon -- no additional description should be necessary.
135
- iconElem.setAttribute('alt', '');
136
- labelElem.innerText = title.label;
137
- iconElem.classList.add('toolbar-icon');
138
- button.replaceChildren(iconElem, labelElem);
139
- }
140
- button.onclick = command;
141
- (parent !== null && parent !== void 0 ? parent : this.container).appendChild(button);
142
- return button;
126
+ /**
127
+ * Adds an action button with `title` to this toolbar (or to the given `parent` element).
128
+ *
129
+ * @return The added button.
130
+ */
131
+ addActionButton(title, command) {
132
+ const titleString = typeof title === 'string' ? title : title.label;
133
+ const widgetId = 'action-button';
134
+ const makeIcon = () => {
135
+ if (typeof title === 'string') {
136
+ return null;
137
+ }
138
+ return title.icon;
139
+ };
140
+ const widget = new ActionButtonWidget(this.editor, widgetId, makeIcon, titleString, command, this.editor.localization);
141
+ this.addWidget(widget);
142
+ return widget;
143
143
  }
144
144
  addUndoRedoButtons() {
145
- const undoRedoGroup = document.createElement('div');
146
- undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
147
145
  const undoButton = this.addActionButton({
148
146
  label: this.localizationTable.undo,
149
147
  icon: this.editor.icons.makeUndoIcon()
150
148
  }, () => {
151
149
  this.editor.history.undo();
152
- }, undoRedoGroup);
150
+ });
153
151
  const redoButton = this.addActionButton({
154
152
  label: this.localizationTable.redo,
155
153
  icon: this.editor.icons.makeRedoIcon(),
156
154
  }, () => {
157
155
  this.editor.history.redo();
158
- }, undoRedoGroup);
159
- this.container.appendChild(undoRedoGroup);
160
- undoButton.disabled = true;
161
- redoButton.disabled = true;
156
+ });
157
+ undoButton.setDisabled(true);
158
+ redoButton.setDisabled(true);
162
159
  this.editor.notifier.on(EditorEventType.UndoRedoStackUpdated, event => {
163
160
  if (event.kind !== EditorEventType.UndoRedoStackUpdated) {
164
161
  throw new Error('Wrong event type!');
165
162
  }
166
- undoButton.disabled = event.undoStackSize === 0;
167
- redoButton.disabled = event.redoStackSize === 0;
163
+ undoButton.setDisabled(event.undoStackSize === 0);
164
+ redoButton.setDisabled(event.redoStackSize === 0);
168
165
  });
169
166
  }
170
167
  addDefaultToolWidgets() {
@@ -1,5 +1,6 @@
1
1
  export interface ToolbarLocalization {
2
2
  fontLabel: string;
3
+ textSize: string;
3
4
  touchPanning: string;
4
5
  lockRotation: string;
5
6
  outlinedRectanglePen: string;
@@ -8,6 +8,7 @@ export const defaultToolbarLocalization = {
8
8
  thicknessLabel: 'Thickness: ',
9
9
  colorLabel: 'Color: ',
10
10
  fontLabel: 'Font: ',
11
+ textSize: 'Size: ',
11
12
  resizeImageToSelection: 'Resize image to selection',
12
13
  deleteSelection: 'Delete selection',
13
14
  duplicateSelection: 'Duplicate selection',
@@ -2,12 +2,12 @@ import Editor from '../../Editor';
2
2
  import { ToolbarLocalization } from '../localization';
3
3
  import BaseWidget from './BaseWidget';
4
4
  export default class ActionButtonWidget extends BaseWidget {
5
- protected makeIcon: () => Element;
5
+ protected makeIcon: () => Element | null;
6
6
  protected title: string;
7
7
  protected clickAction: () => void;
8
- constructor(editor: Editor, id: string, makeIcon: () => Element, title: string, clickAction: () => void, localizationTable?: ToolbarLocalization);
8
+ constructor(editor: Editor, id: string, makeIcon: () => Element | null, title: string, clickAction: () => void, localizationTable?: ToolbarLocalization);
9
9
  protected handleClick(): void;
10
10
  protected getTitle(): string;
11
- protected createIcon(): Element;
11
+ protected createIcon(): Element | null;
12
12
  protected fillDropdown(_dropdown: HTMLElement): boolean;
13
13
  }
@@ -29,7 +29,7 @@ export default abstract class BaseWidget {
29
29
  */
30
30
  getUniqueIdIn(container: Record<string, BaseWidget>): string;
31
31
  protected abstract getTitle(): string;
32
- protected abstract createIcon(): Element;
32
+ protected abstract createIcon(): Element | null;
33
33
  protected fillDropdown(dropdown: HTMLElement): boolean;
34
34
  protected setupActionBtnClickListener(button: HTMLElement): void;
35
35
  protected onKeyPress(_event: KeyPressEvent): boolean;
@@ -156,11 +156,16 @@ export default class BaseWidget {
156
156
  parent.appendChild(this.container);
157
157
  }
158
158
  updateIcon() {
159
- var _a;
159
+ var _a, _b;
160
160
  const newIcon = this.createIcon();
161
- (_a = this.icon) === null || _a === void 0 ? void 0 : _a.replaceWith(newIcon);
162
- this.icon = newIcon;
163
- this.icon.classList.add(`${toolbarCSSPrefix}icon`);
161
+ if (newIcon) {
162
+ (_a = this.icon) === null || _a === void 0 ? void 0 : _a.replaceWith(newIcon);
163
+ this.icon = newIcon;
164
+ this.icon.classList.add(`${toolbarCSSPrefix}icon`);
165
+ }
166
+ else {
167
+ (_b = this.icon) === null || _b === void 0 ? void 0 : _b.remove();
168
+ }
164
169
  }
165
170
  setDisabled(disabled) {
166
171
  this.disabled = disabled;
@@ -26,8 +26,11 @@ export default class TextToolWidget extends BaseToolWidget {
26
26
  fillDropdown(dropdown) {
27
27
  const fontRow = document.createElement('div');
28
28
  const colorRow = document.createElement('div');
29
+ const sizeRow = document.createElement('div');
29
30
  const fontInput = document.createElement('select');
30
31
  const fontLabel = document.createElement('label');
32
+ const sizeInput = document.createElement('input');
33
+ const sizeLabel = document.createElement('label');
31
34
  const [colorInput, colorInputContainer, setColorInputValue] = makeColorInput(this.editor, color => {
32
35
  this.tool.setColor(color);
33
36
  });
@@ -40,10 +43,16 @@ export default class TextToolWidget extends BaseToolWidget {
40
43
  fontInput.appendChild(option);
41
44
  fontsInInput.add(fontName);
42
45
  };
46
+ sizeInput.setAttribute('type', 'number');
47
+ sizeInput.min = '1';
48
+ sizeInput.max = '128';
43
49
  fontLabel.innerText = this.localizationTable.fontLabel;
44
50
  colorLabel.innerText = this.localizationTable.colorLabel;
51
+ sizeLabel.innerText = this.localizationTable.textSize;
45
52
  colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
46
53
  colorLabel.setAttribute('for', colorInput.id);
54
+ sizeInput.id = `${toolbarCSSPrefix}-text-size-input-${TextToolWidget.idCounter++}`;
55
+ sizeLabel.setAttribute('for', sizeInput.id);
47
56
  addFontToInput('monospace');
48
57
  addFontToInput('serif');
49
58
  addFontToInput('sans-serif');
@@ -52,10 +61,18 @@ export default class TextToolWidget extends BaseToolWidget {
52
61
  fontInput.onchange = () => {
53
62
  this.tool.setFontFamily(fontInput.value);
54
63
  };
64
+ sizeInput.onchange = () => {
65
+ const size = parseInt(sizeInput.value);
66
+ if (!isNaN(size) && size > 0) {
67
+ this.tool.setFontSize(size);
68
+ }
69
+ };
55
70
  colorRow.appendChild(colorLabel);
56
71
  colorRow.appendChild(colorInputContainer);
57
72
  fontRow.appendChild(fontLabel);
58
73
  fontRow.appendChild(fontInput);
74
+ sizeRow.appendChild(sizeLabel);
75
+ sizeRow.appendChild(sizeInput);
59
76
  this.updateDropdownInputs = () => {
60
77
  const style = this.tool.getTextStyle();
61
78
  setColorInputValue(style.renderingStyle.fill);
@@ -63,14 +80,15 @@ export default class TextToolWidget extends BaseToolWidget {
63
80
  addFontToInput(style.fontFamily);
64
81
  }
65
82
  fontInput.value = style.fontFamily;
83
+ sizeInput.value = `${style.size}`;
66
84
  };
67
85
  this.updateDropdownInputs();
68
- dropdown.replaceChildren(colorRow, fontRow);
86
+ dropdown.replaceChildren(colorRow, sizeRow, fontRow);
69
87
  return true;
70
88
  }
71
89
  serializeState() {
72
90
  const textStyle = this.tool.getTextStyle();
73
- return Object.assign(Object.assign({}, super.serializeState()), { fontFamily: textStyle.fontFamily, color: textStyle.renderingStyle.fill.toHexString() });
91
+ return Object.assign(Object.assign({}, super.serializeState()), { fontFamily: textStyle.fontFamily, textSize: textStyle.size, color: textStyle.renderingStyle.fill.toHexString() });
74
92
  }
75
93
  deserializeFrom(state) {
76
94
  if (state.fontFamily && typeof (state.fontFamily) === 'string') {
@@ -79,6 +97,9 @@ export default class TextToolWidget extends BaseToolWidget {
79
97
  if (state.color && typeof (state.color) === 'string') {
80
98
  this.tool.setColor(Color4.fromHex(state.color));
81
99
  }
100
+ if (state.textSize && typeof (state.textSize) === 'number') {
101
+ this.tool.setFontSize(state.textSize);
102
+ }
82
103
  super.deserializeFrom(state);
83
104
  }
84
105
  }
@@ -24,15 +24,19 @@ export default class PanZoom extends BaseTool {
24
24
  private lastAngle;
25
25
  private lastDist;
26
26
  private lastScreenCenter;
27
+ private lastTimestamp;
28
+ private inertialScroller;
29
+ private velocity;
27
30
  constructor(editor: Editor, mode: PanZoomMode, description: string);
28
31
  computePinchData(p1: Pointer, p2: Pointer): PinchData;
29
32
  private allPointersAreOfType;
30
33
  onPointerDown({ allPointers: pointers }: PointerEvt): boolean;
34
+ private updateVelocity;
31
35
  private getCenterDelta;
32
36
  private handleTwoFingerMove;
33
37
  private handleOneFingerMove;
34
38
  onPointerMove({ allPointers }: PointerEvt): void;
35
- onPointerUp(_event: PointerEvt): void;
39
+ onPointerUp(event: PointerEvt): void;
36
40
  onGestureCancel(): void;
37
41
  private updateTransform;
38
42
  onWheel({ delta, screenPos }: WheelEvt): boolean;