js-draw 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +24 -0
  2. package/CHANGELOG.md +14 -1
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Color4.d.ts +4 -0
  5. package/dist/src/Color4.js +22 -0
  6. package/dist/src/Editor.d.ts +2 -1
  7. package/dist/src/Editor.js +10 -1
  8. package/dist/src/EditorImage.d.ts +1 -0
  9. package/dist/src/EditorImage.js +11 -0
  10. package/dist/src/commands/UnresolvedCommand.d.ts +14 -0
  11. package/dist/src/commands/UnresolvedCommand.js +22 -0
  12. package/dist/src/commands/uniteCommands.js +4 -2
  13. package/dist/src/components/AbstractComponent.d.ts +0 -1
  14. package/dist/src/components/AbstractComponent.js +36 -50
  15. package/dist/src/components/RestylableComponent.d.ts +24 -0
  16. package/dist/src/components/RestylableComponent.js +80 -0
  17. package/dist/src/components/Stroke.d.ts +8 -1
  18. package/dist/src/components/Stroke.js +49 -1
  19. package/dist/src/components/TextComponent.d.ts +10 -10
  20. package/dist/src/components/TextComponent.js +46 -13
  21. package/dist/src/components/lib.d.ts +2 -1
  22. package/dist/src/components/lib.js +2 -1
  23. package/dist/src/components/localization.d.ts +1 -0
  24. package/dist/src/components/localization.js +1 -0
  25. package/dist/src/rendering/TextRenderingStyle.d.ts +23 -0
  26. package/dist/src/rendering/TextRenderingStyle.js +20 -0
  27. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -1
  28. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +1 -1
  29. package/dist/src/rendering/renderers/DummyRenderer.d.ts +1 -1
  30. package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
  31. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +1 -1
  32. package/dist/src/toolbar/IconProvider.d.ts +2 -1
  33. package/dist/src/toolbar/IconProvider.js +10 -0
  34. package/dist/src/toolbar/localization.d.ts +1 -0
  35. package/dist/src/toolbar/localization.js +1 -0
  36. package/dist/src/toolbar/widgets/BaseWidget.js +10 -4
  37. package/dist/src/toolbar/widgets/InsertImageWidget.js +2 -1
  38. package/dist/src/toolbar/widgets/SelectionToolWidget.js +77 -1
  39. package/dist/src/tools/Pen.js +2 -2
  40. package/dist/src/tools/SelectionTool/SelectAllShortcutHandler.d.ts +8 -0
  41. package/dist/src/tools/SelectionTool/SelectAllShortcutHandler.js +22 -0
  42. package/dist/src/tools/SelectionTool/Selection.js +1 -1
  43. package/dist/src/tools/SelectionTool/SelectionTool.js +7 -10
  44. package/dist/src/tools/TextTool.d.ts +1 -1
  45. package/dist/src/tools/ToolController.js +2 -0
  46. package/dist/src/tools/lib.d.ts +1 -0
  47. package/dist/src/tools/lib.js +1 -0
  48. package/dist/src/tools/localization.d.ts +1 -0
  49. package/dist/src/tools/localization.js +1 -0
  50. package/package.json +1 -1
  51. package/src/Color4.test.ts +4 -0
  52. package/src/Color4.ts +26 -0
  53. package/src/Editor.toSVG.test.ts +1 -1
  54. package/src/Editor.ts +12 -1
  55. package/src/EditorImage.ts +13 -0
  56. package/src/SVGLoader.ts +2 -1
  57. package/src/commands/UnresolvedCommand.ts +37 -0
  58. package/src/commands/uniteCommands.ts +5 -2
  59. package/src/components/AbstractComponent.transformBy.test.ts +22 -0
  60. package/src/components/AbstractComponent.ts +41 -59
  61. package/src/components/RestylableComponent.ts +142 -0
  62. package/src/components/Stroke.test.ts +68 -0
  63. package/src/components/Stroke.ts +68 -2
  64. package/src/components/TextComponent.test.ts +56 -2
  65. package/src/components/TextComponent.ts +63 -25
  66. package/src/components/lib.ts +4 -1
  67. package/src/components/localization.ts +3 -0
  68. package/src/math/Rect2.test.ts +18 -6
  69. package/src/rendering/TextRenderingStyle.ts +38 -0
  70. package/src/rendering/renderers/AbstractRenderer.ts +1 -1
  71. package/src/rendering/renderers/CanvasRenderer.ts +2 -1
  72. package/src/rendering/renderers/DummyRenderer.ts +1 -1
  73. package/src/rendering/renderers/SVGRenderer.ts +1 -1
  74. package/src/rendering/renderers/TextOnlyRenderer.ts +1 -1
  75. package/src/toolbar/IconProvider.ts +12 -1
  76. package/src/toolbar/localization.ts +2 -0
  77. package/src/toolbar/toolbar.css +7 -0
  78. package/src/toolbar/widgets/BaseWidget.ts +12 -4
  79. package/src/toolbar/widgets/InsertImageWidget.ts +2 -1
  80. package/src/toolbar/widgets/SelectionToolWidget.ts +95 -1
  81. package/src/tools/PanZoom.test.ts +2 -1
  82. package/src/tools/PasteHandler.ts +1 -1
  83. package/src/tools/Pen.ts +2 -2
  84. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +28 -0
  85. package/src/tools/SelectionTool/Selection.ts +1 -1
  86. package/src/tools/SelectionTool/SelectionTool.ts +6 -9
  87. package/src/tools/TextTool.ts +2 -1
  88. package/src/tools/ToolController.ts +2 -0
  89. package/src/tools/lib.ts +1 -0
  90. package/src/tools/localization.ts +2 -0
@@ -1,24 +1,22 @@
1
+ import SerializableCommand from '../commands/SerializableCommand';
1
2
  import LineSegment2 from '../math/LineSegment2';
2
3
  import Mat33, { Mat33Array } from '../math/Mat33';
3
4
  import Rect2 from '../math/Rect2';
5
+ import Editor from '../Editor';
4
6
  import { Vec2 } from '../math/Vec2';
5
7
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
6
- import RenderingStyle, { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
8
+ import { TextStyle, textStyleFromJSON, textStyleToJSON } from '../rendering/TextRenderingStyle';
7
9
  import AbstractComponent from './AbstractComponent';
8
10
  import { ImageComponentLocalization } from './localization';
9
-
10
- export interface TextStyle {
11
- size: number;
12
- fontFamily: string;
13
- fontWeight?: string;
14
- fontVariant?: string;
15
- renderingStyle: RenderingStyle;
16
- }
11
+ import RestyleableComponent, { ComponentStyle, createRestyleComponentCommand } from './RestylableComponent';
17
12
 
18
13
  const componentTypeId = 'text';
19
- export default class TextComponent extends AbstractComponent {
14
+ export default class TextComponent extends AbstractComponent implements RestyleableComponent {
20
15
  protected contentBBox: Rect2;
21
16
 
17
+ // eslint-disable-next-line @typescript-eslint/prefer-as-const
18
+ readonly isRestylableComponent: true = true;
19
+
22
20
  public constructor(
23
21
  protected readonly textObjects: Array<string|TextComponent>,
24
22
  private transform: Mat33,
@@ -152,14 +150,57 @@ export default class TextComponent extends AbstractComponent {
152
150
  return false;
153
151
  }
154
152
 
155
- public getBaselinePos() {
156
- return this.transform.transformVec2(Vec2.zero);
153
+ public getStyle(): ComponentStyle {
154
+ return {
155
+ color: this.style.renderingStyle.fill,
156
+
157
+ // Make a copy
158
+ textStyle: {
159
+ ...this.style,
160
+ renderingStyle: {
161
+ ...this.style.renderingStyle,
162
+ },
163
+ },
164
+ };
165
+ }
166
+
167
+ public updateStyle(style: ComponentStyle): SerializableCommand {
168
+ return createRestyleComponentCommand(this.getStyle(), style, this);
169
+ }
170
+
171
+ public forceStyle(style: ComponentStyle, editor: Editor|null): void {
172
+ if (style.textStyle) {
173
+ this.style = style.textStyle;
174
+ } else if (style.color) {
175
+ this.style.renderingStyle = {
176
+ ...this.style.renderingStyle,
177
+ fill: style.color,
178
+ };
179
+ } else {
180
+ return;
181
+ }
182
+
183
+ for (const child of this.textObjects) {
184
+ if (child instanceof TextComponent) {
185
+ child.forceStyle(style, editor);
186
+ }
187
+ }
188
+
189
+ if (editor) {
190
+ editor.image.queueRerenderOf(this);
191
+ editor.queueRerender();
192
+ }
157
193
  }
158
194
 
195
+ // See this.getStyle
159
196
  public getTextStyle() {
160
197
  return this.style;
161
198
  }
162
199
 
200
+ public getBaselinePos() {
201
+ return this.transform.transformVec2(Vec2.zero);
202
+ }
203
+
163
204
  public getTransform(): Mat33 {
164
205
  return this.transform;
165
206
  }
@@ -191,13 +232,11 @@ export default class TextComponent extends AbstractComponent {
191
232
  return localizationTable.text(this.getText());
192
233
  }
193
234
 
235
+ // Do not rely on the output of `serializeToJSON` taking any particular format.
194
236
  protected serializeToJSON(): Record<string, any> {
195
- const serializableStyle = {
196
- ...this.style,
197
- renderingStyle: styleToJSON(this.style.renderingStyle),
198
- };
237
+ const serializableStyle = textStyleToJSON(this.style);
199
238
 
200
- const textObjects = this.textObjects.map(text => {
239
+ const serializedTextObjects = this.textObjects.map(text => {
201
240
  if (typeof text === 'string') {
202
241
  return {
203
242
  text,
@@ -210,20 +249,19 @@ export default class TextComponent extends AbstractComponent {
210
249
  });
211
250
 
212
251
  return {
213
- textObjects,
252
+ textObjects: serializedTextObjects,
214
253
  transform: this.transform.toArray(),
215
254
  style: serializableStyle,
216
255
  };
217
256
  }
218
257
 
258
+ // @internal
219
259
  public static deserializeFromString(json: any): TextComponent {
220
- const style: TextStyle = {
221
- renderingStyle: styleFromJSON(json.style.renderingStyle),
222
- size: json.style.size,
223
- fontWeight: json.style.fontWeight,
224
- fontVariant: json.style.fontVariant,
225
- fontFamily: json.style.fontFamily,
226
- };
260
+ if (typeof json === 'string') {
261
+ json = JSON.parse(json);
262
+ }
263
+
264
+ const style = textStyleFromJSON(json.style);
227
265
 
228
266
  const textObjects: Array<string|TextComponent> = json.textObjects.map((data: any) => {
229
267
  if ((data.text ?? null) !== null) {
@@ -8,12 +8,15 @@ export { default as AbstractComponent } from './AbstractComponent';
8
8
  import Stroke from './Stroke';
9
9
  import TextComponent from './TextComponent';
10
10
  import ImageComponent from './ImageComponent';
11
+ import RestyleableComponent, { createRestyleComponentCommand } from './RestylableComponent';
11
12
 
12
13
  export {
13
14
  Stroke,
14
15
  TextComponent as Text,
16
+ RestyleableComponent,
17
+ createRestyleComponentCommand,
15
18
 
16
- TextComponent as TextComponent,
19
+ TextComponent,
17
20
  Stroke as StrokeComponent,
18
21
  ImageComponent,
19
22
  };
@@ -4,12 +4,15 @@ export interface ImageComponentLocalization {
4
4
  imageNode: (description: string)=> string;
5
5
  stroke: string;
6
6
  svgObject: string;
7
+
8
+ restyledElements: string;
7
9
  }
8
10
 
9
11
  export const defaultComponentLocalization: ImageComponentLocalization = {
10
12
  unlabeledImageNode: 'Unlabeled image node',
11
13
  stroke: 'Stroke',
12
14
  svgObject: 'SVG Object',
15
+ restyledElements: 'Restyled elements',
13
16
  text: (text) => `Text object: ${text}`,
14
17
  imageNode: (description: string) => `Image: ${description}`,
15
18
  };
@@ -77,13 +77,23 @@ describe('Rect2', () => {
77
77
  expect(new Rect2(-2, -2, 4, 4).containsRect(new Rect2(-1, 0, 10, 1))).toBe(false);
78
78
  });
79
79
 
80
- it('a rectangle should contain itself', () => {
81
- const rect = new Rect2(1 / 3, 1 / 4, 1 / 5, 1 / 6);
82
- expect(rect.containsRect(rect)).toBe(true);
83
- });
80
+ describe('containsRect', () => {
81
+ it('a rectangle should contain itself', () => {
82
+ const rect = new Rect2(1 / 3, 1 / 4, 1 / 5, 1 / 6);
83
+ expect(rect.containsRect(rect)).toBe(true);
84
+ });
84
85
 
85
- it('empty rect should not contain a larger rect', () => {
86
- expect(Rect2.empty.containsRect(new Rect2(-1, -1, 3, 3))).toBe(false);
86
+ it('empty rect should not contain a larger rect', () => {
87
+ expect(Rect2.empty.containsRect(new Rect2(-1, -1, 3, 3))).toBe(false);
88
+ });
89
+
90
+ it('should correctly contain rectangles', () => {
91
+ const testRect = new Rect2(4, -10, 50, 100);
92
+ expect(testRect.containsRect(new Rect2(4.1, 0, 1, 1))).toBe(true);
93
+ expect(testRect.containsRect(new Rect2(48, 0, 1, 1))).toBe(true);
94
+ expect(testRect.containsRect(new Rect2(48, -9, 1, 1))).toBe(true);
95
+ expect(testRect.containsRect(new Rect2(48, -9, 1, 91))).toBe(true);
96
+ });
87
97
  });
88
98
 
89
99
  it('intersecting rectangles should be identified as intersecting', () => {
@@ -92,6 +102,8 @@ describe('Rect2', () => {
92
102
  expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(0, 0, 10, 10))).toBe(true);
93
103
  expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(3, 3, 10, 10))).toBe(false);
94
104
  expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(0.2, 0.1, 0, 0))).toBe(true);
105
+ expect(new Rect2(-100, -1, 200, 2).intersects(new Rect2(-5, -5, 10, 30))).toBe(true);
106
+ expect(new Rect2(-100, -1, 200, 2).intersects(new Rect2(-5, 50, 10, 30))).toBe(false);
95
107
  });
96
108
 
97
109
  it('intersecting rectangles should have their intersections correctly computed', () => {
@@ -0,0 +1,38 @@
1
+ import RenderingStyle, { styleFromJSON, styleToJSON } from './RenderingStyle';
2
+
3
+ export interface TextStyle {
4
+ size: number;
5
+ fontFamily: string;
6
+ fontWeight?: string;
7
+ fontVariant?: string;
8
+ renderingStyle: RenderingStyle;
9
+ }
10
+
11
+ export default TextStyle;
12
+
13
+ export const textStyleFromJSON = (json: any) => {
14
+ if (typeof json === 'string') {
15
+ json = JSON.parse(json);
16
+ }
17
+
18
+ if (typeof(json.fontFamily) !== 'string') {
19
+ throw new Error('Serialized textStyle missing string fontFamily attribute!');
20
+ }
21
+
22
+ const style: TextStyle = {
23
+ renderingStyle: styleFromJSON(json.renderingStyle),
24
+ size: json.size,
25
+ fontWeight: json.fontWeight,
26
+ fontVariant: json.fontVariant,
27
+ fontFamily: json.fontFamily,
28
+ };
29
+
30
+ return style;
31
+ };
32
+
33
+ export const textStyleToJSON = (style: TextStyle) => {
34
+ return {
35
+ ...style,
36
+ renderingStyle: styleToJSON(style.renderingStyle),
37
+ };
38
+ };
@@ -1,11 +1,11 @@
1
1
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
- import { TextStyle } from '../../components/TextComponent';
3
2
  import Mat33 from '../../math/Mat33';
4
3
  import Path, { PathCommand, PathCommandType } from '../../math/Path';
5
4
  import Rect2 from '../../math/Rect2';
6
5
  import { Point2, Vec2 } from '../../math/Vec2';
7
6
  import Viewport from '../../Viewport';
8
7
  import RenderingStyle, { stylesEqual } from '../RenderingStyle';
8
+ import TextStyle from '../TextRenderingStyle';
9
9
 
10
10
  export interface RenderablePathSpec {
11
11
  startPoint: Point2;
@@ -1,11 +1,12 @@
1
1
  import Color4 from '../../Color4';
2
- import TextComponent, { TextStyle } from '../../components/TextComponent';
2
+ import TextComponent from '../../components/TextComponent';
3
3
  import Mat33 from '../../math/Mat33';
4
4
  import Rect2 from '../../math/Rect2';
5
5
  import { Point2, Vec2 } from '../../math/Vec2';
6
6
  import Vec3 from '../../math/Vec3';
7
7
  import Viewport from '../../Viewport';
8
8
  import RenderingStyle from '../RenderingStyle';
9
+ import TextStyle from '../TextRenderingStyle';
9
10
  import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
10
11
 
11
12
  export default class CanvasRenderer extends AbstractRenderer {
@@ -1,10 +1,10 @@
1
- import { TextStyle } from '../../components/TextComponent';
2
1
  import Mat33 from '../../math/Mat33';
3
2
  import Rect2 from '../../math/Rect2';
4
3
  import { Point2, Vec2 } from '../../math/Vec2';
5
4
  import Vec3 from '../../math/Vec3';
6
5
  import Viewport from '../../Viewport';
7
6
  import RenderingStyle from '../RenderingStyle';
7
+ import TextStyle from '../TextRenderingStyle';
8
8
  import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
9
9
 
10
10
  // Renderer that outputs almost nothing. Useful for automated tests.
@@ -1,6 +1,5 @@
1
1
 
2
2
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
3
- import { TextStyle } from '../../components/TextComponent';
4
3
  import Mat33 from '../../math/Mat33';
5
4
  import Path from '../../math/Path';
6
5
  import Rect2 from '../../math/Rect2';
@@ -9,6 +8,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
9
8
  import { svgAttributesDataKey, SVGLoaderUnknownAttribute, SVGLoaderUnknownStyleAttribute, svgStyleAttributesDataKey } from '../../SVGLoader';
10
9
  import Viewport from '../../Viewport';
11
10
  import RenderingStyle from '../RenderingStyle';
11
+ import TextStyle from '../TextRenderingStyle';
12
12
  import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
13
13
 
14
14
  export const renderedStylesheetId = 'js-draw-style-sheet';
@@ -1,4 +1,3 @@
1
- import { TextStyle } from '../../components/TextComponent';
2
1
  import Mat33 from '../../math/Mat33';
3
2
  import Rect2 from '../../math/Rect2';
4
3
  import { Vec2 } from '../../math/Vec2';
@@ -6,6 +5,7 @@ import Vec3 from '../../math/Vec3';
6
5
  import Viewport from '../../Viewport';
7
6
  import { TextRendererLocalization } from '../localization';
8
7
  import RenderingStyle from '../RenderingStyle';
8
+ import TextStyle from '../TextRenderingStyle';
9
9
  import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
10
10
 
11
11
  // Outputs a description of what was rendered.
@@ -1,9 +1,9 @@
1
1
  import Color4 from '../Color4';
2
2
  import { ComponentBuilderFactory } from '../components/builders/types';
3
- import { TextStyle } from '../components/TextComponent';
4
3
  import EventDispatcher from '../EventDispatcher';
5
4
  import { Vec2 } from '../math/Vec2';
6
5
  import SVGRenderer from '../rendering/renderers/SVGRenderer';
6
+ import TextStyle from '../rendering/TextRenderingStyle';
7
7
  import Pen from '../tools/Pen';
8
8
  import { StrokeDataPoint } from '../types';
9
9
  import Viewport from '../Viewport';
@@ -609,6 +609,17 @@ export default class IconProvider {
609
609
  icon.setAttribute('viewBox', '0 0 100 100');
610
610
  return icon;
611
611
  }
612
+
613
+ public makeFormatSelectionIcon(): IconType {
614
+ return this.makeIconFromPath(`
615
+ M 5 10
616
+ L 5 20 L 10 20 L 10 15 L 20 15 L 20 40 L 15 40 L 15 45 L 35 45 L 35 40 L 30 40 L 30 15 L 40 15 L 40 20 L 45 20 L 45 15 L 45 10 L 5 10 z
617
+ M 90 10 C 90 10 86.5 13.8 86 14 C 86 14 76.2 24.8 76 25 L 60 25 L 60 65 C 75 70 85 70 90 65 L 90 25 L 80 25 L 76.7 25 L 90 10 z
618
+ M 60 25 L 55 25 L 50 30 L 60 25 z
619
+ M 10 55 L 10 90 L 41 90 L 41 86 L 45 86 L 45 55 L 10 55 z
620
+ M 42 87 L 42 93 L 48 93 L 48 87 L 42 87 z
621
+ `);
622
+ }
612
623
 
613
624
  public makeResizeViewportIcon(): IconType {
614
625
  return this.makeIconFromPath(`
@@ -28,6 +28,7 @@ export interface ToolbarLocalization {
28
28
  duplicateSelection: string;
29
29
  pickColorFromScreen: string;
30
30
  clickToPickColorAnnouncement: string;
31
+ reformatSelection: string;
31
32
  undo: string;
32
33
  redo: string;
33
34
  zoom: string;
@@ -52,6 +53,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
52
53
  handTool: 'Pan',
53
54
  zoom: 'Zoom',
54
55
  image: 'Image',
56
+ reformatSelection: 'Format selection',
55
57
  inputAltText: 'Alt text: ',
56
58
  chooseFile: 'Choose file: ',
57
59
  submit: 'Submit',
@@ -125,6 +125,13 @@
125
125
  padding: 15px;
126
126
  padding-top: 5px;
127
127
 
128
+ display: flex;
129
+ flex-wrap: wrap;
130
+ flex-direction: column;
131
+ max-height: 80vh;
132
+
133
+ max-width: fit-content;
134
+
128
135
  /* Prevent overlap/being displayed under the undo/redo buttons */
129
136
  z-index: 2;
130
137
  background-color: var(--primary-background-color);
@@ -110,13 +110,17 @@ export default abstract class BaseWidget {
110
110
 
111
111
  // If we didn't do anything with the event, send it to the editor.
112
112
  if (!handled) {
113
- this.editor.toolController.dispatchInputEvent({
113
+ handled = this.editor.toolController.dispatchInputEvent({
114
114
  kind: InputEvtType.KeyPressEvent,
115
115
  key: evt.key,
116
- ctrlKey: evt.ctrlKey,
116
+ ctrlKey: evt.ctrlKey || evt.metaKey,
117
117
  altKey: evt.altKey,
118
118
  });
119
119
  }
120
+
121
+ if (handled) {
122
+ evt.preventDefault();
123
+ }
120
124
  };
121
125
 
122
126
  button.onkeyup = evt => {
@@ -124,12 +128,16 @@ export default abstract class BaseWidget {
124
128
  return;
125
129
  }
126
130
 
127
- this.editor.toolController.dispatchInputEvent({
131
+ const handled = this.editor.toolController.dispatchInputEvent({
128
132
  kind: InputEvtType.KeyUpEvent,
129
133
  key: evt.key,
130
- ctrlKey: evt.ctrlKey,
134
+ ctrlKey: evt.ctrlKey || evt.metaKey,
131
135
  altKey: evt.altKey,
132
136
  });
137
+
138
+ if (handled) {
139
+ evt.preventDefault();
140
+ }
133
141
  };
134
142
 
135
143
  button.onclick = () => {
@@ -2,7 +2,8 @@ import ImageComponent from '../../components/ImageComponent';
2
2
  import Editor from '../../Editor';
3
3
  import Erase from '../../commands/Erase';
4
4
  import EditorImage from '../../EditorImage';
5
- import { SelectionTool, uniteCommands } from '../../lib';
5
+ import uniteCommands from '../../commands/uniteCommands';
6
+ import SelectionTool from '../../tools/SelectionTool/SelectionTool';
6
7
  import Mat33 from '../../math/Mat33';
7
8
  import fileToBase64 from '../../util/fileToBase64';
8
9
  import { ToolbarLocalization } from '../localization';
@@ -1,9 +1,96 @@
1
+ import Color4 from '../../Color4';
2
+ import { isRestylableComponent } from '../../components/RestylableComponent';
1
3
  import Editor from '../../Editor';
4
+ import uniteCommands from '../../commands/uniteCommands';
2
5
  import SelectionTool from '../../tools/SelectionTool/SelectionTool';
3
6
  import { EditorEventType, KeyPressEvent } from '../../types';
4
7
  import { ToolbarLocalization } from '../localization';
8
+ import makeColorInput from '../makeColorInput';
5
9
  import ActionButtonWidget from './ActionButtonWidget';
6
10
  import BaseToolWidget from './BaseToolWidget';
11
+ import BaseWidget from './BaseWidget';
12
+
13
+ class RestyleSelectionWidget extends BaseWidget {
14
+ private updateFormatData: ()=>void = () => {};
15
+
16
+ public constructor(editor: Editor, private selectionTool: SelectionTool, localizationTable?: ToolbarLocalization) {
17
+ super(editor, 'restyle-selection', localizationTable);
18
+
19
+ // Allow showing the dropdown even if this widget isn't selected yet
20
+ this.container.classList.add('dropdownShowable');
21
+
22
+ this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
23
+ if (toolEvt.kind !== EditorEventType.ToolUpdated) {
24
+ throw new Error('Invalid event type!');
25
+ }
26
+
27
+ if (toolEvt.tool === this.selectionTool) {
28
+ this.updateFormatData();
29
+ }
30
+ });
31
+ }
32
+
33
+ protected getTitle(): string {
34
+ return this.localizationTable.reformatSelection;
35
+ }
36
+
37
+ protected createIcon(){
38
+ return this.editor.icons.makeFormatSelectionIcon();
39
+ }
40
+
41
+ protected handleClick(): void {
42
+ this.setDropdownVisible(!this.isDropdownVisible());
43
+ }
44
+
45
+ protected fillDropdown(dropdown: HTMLElement): boolean {
46
+ const container = document.createElement('div');
47
+ const colorRow = document.createElement('div');
48
+ const colorLabel = document.createElement('label');
49
+ const [ colorInput, colorInputContainer, setColorInputValue ] = makeColorInput(this.editor, color => {
50
+ const selection = this.selectionTool.getSelection();
51
+
52
+ if (selection) {
53
+ const updateStyleCommands = [];
54
+
55
+ for (const elem of selection.getSelectedObjects()) {
56
+ if (isRestylableComponent(elem)) {
57
+ updateStyleCommands.push(elem.updateStyle({ color }));
58
+ }
59
+ }
60
+
61
+ const unitedCommand = uniteCommands(updateStyleCommands);
62
+ this.editor.dispatch(unitedCommand);
63
+ }
64
+ });
65
+
66
+ colorLabel.innerText = this.localizationTable.colorLabel;
67
+
68
+ this.updateFormatData = () => {
69
+ const selection = this.selectionTool.getSelection();
70
+ if (selection) {
71
+ colorInput.disabled = false;
72
+
73
+ const colors = [];
74
+ for (const elem of selection.getSelectedObjects()) {
75
+ if (isRestylableComponent(elem)) {
76
+ const color = elem.getStyle().color;
77
+ if (color) {
78
+ colors.push(color);
79
+ }
80
+ }
81
+ }
82
+ setColorInputValue(Color4.average(colors));
83
+ } else {
84
+ colorInput.disabled = true;
85
+ }
86
+ };
87
+
88
+ colorRow.replaceChildren(colorLabel, colorInputContainer);
89
+ container.replaceChildren(colorRow);
90
+ dropdown.replaceChildren(container);
91
+ return true;
92
+ }
93
+ }
7
94
 
8
95
  export default class SelectionToolWidget extends BaseToolWidget {
9
96
  public constructor(
@@ -41,15 +128,22 @@ export default class SelectionToolWidget extends BaseToolWidget {
41
128
  },
42
129
  localization,
43
130
  );
131
+ const restyleButton = new RestyleSelectionWidget(
132
+ editor,
133
+ this.tool,
134
+ localization,
135
+ );
44
136
 
45
137
  this.addSubWidget(resizeButton);
46
138
  this.addSubWidget(deleteButton);
47
139
  this.addSubWidget(duplicateButton);
140
+ this.addSubWidget(restyleButton);
48
141
 
49
142
  const updateDisabled = (disabled: boolean) => {
50
143
  resizeButton.setDisabled(disabled);
51
144
  deleteButton.setDisabled(disabled);
52
145
  duplicateButton.setDisabled(disabled);
146
+ restyleButton.setDisabled(disabled);
53
147
  };
54
148
  updateDisabled(true);
55
149
 
@@ -61,7 +155,7 @@ export default class SelectionToolWidget extends BaseToolWidget {
61
155
 
62
156
  if (toolEvt.tool === this.tool) {
63
157
  const selection = this.tool.getSelection();
64
- const hasSelection = selection && selection.region.area > 0;
158
+ const hasSelection = selection && selection.getSelectedItemCount() > 0;
65
159
 
66
160
  updateDisabled(!hasSelection);
67
161
  }
@@ -53,7 +53,8 @@ describe('PanZoom', () => {
53
53
  sendTouchEvent(editor, InputEvtType.PointerUpEvt, Vec2.of(100, 0));
54
54
 
55
55
  const updatedTranslation = editor.viewport.canvasToScreen(Vec2.zero);
56
- expect(updatedTranslation.minus(origTranslation).magnitude()).toBe(100);
56
+ expect(updatedTranslation.minus(origTranslation).magnitude()).toBeGreaterThanOrEqual(100);
57
+ expect(updatedTranslation.minus(origTranslation).magnitude()).toBeLessThan(110);
57
58
 
58
59
  await waitForTimeout(600); // ms
59
60
  jest.useFakeTimers();
@@ -6,8 +6,8 @@ import { Mat33 } from '../math/lib';
6
6
  import BaseTool from './BaseTool';
7
7
  import TextTool from './TextTool';
8
8
  import Color4 from '../Color4';
9
- import { TextStyle } from '../components/TextComponent';
10
9
  import ImageComponent from '../components/ImageComponent';
10
+ import TextStyle from '../rendering/TextRenderingStyle';
11
11
 
12
12
  /**
13
13
  * A tool that handles paste events (e.g. as triggered by ctrl+V).
package/src/tools/Pen.ts CHANGED
@@ -200,7 +200,7 @@ export default class Pen extends BaseTool {
200
200
  return true;
201
201
  }
202
202
 
203
- if (key === 'control') {
203
+ if (key === 'control' || key === 'meta') {
204
204
  this.ctrlKeyPressed = true;
205
205
  return true;
206
206
  }
@@ -216,7 +216,7 @@ export default class Pen extends BaseTool {
216
216
  public onKeyUp({ key }: KeyUpEvent): boolean {
217
217
  key = key.toLowerCase();
218
218
 
219
- if (key === 'control') {
219
+ if (key === 'control' || key === 'meta') {
220
220
  this.ctrlKeyPressed = false;
221
221
  return true;
222
222
  }
@@ -0,0 +1,28 @@
1
+ import Editor from '../../Editor';
2
+ import { KeyPressEvent } from '../../types';
3
+ import BaseTool from '../BaseTool';
4
+ import SelectionTool from './SelectionTool';
5
+
6
+ // Handles ctrl+a: Select all
7
+ export default class SelectAllShortcutHandler extends BaseTool {
8
+ public constructor(private editor: Editor) {
9
+ super(editor.notifier, editor.localization.selectAllTool);
10
+ }
11
+
12
+ // @internal
13
+ public onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean {
14
+ if (ctrlKey && key === 'a') {
15
+ const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool);
16
+
17
+ if (selectionTools.length > 0) {
18
+ const selectionTool = selectionTools[0];
19
+ selectionTool.setEnabled(true);
20
+ selectionTool.setSelection(this.editor.image.getAllElements());
21
+
22
+ return true;
23
+ }
24
+ }
25
+
26
+ return false;
27
+ }
28
+ }
@@ -265,7 +265,7 @@ export default class Selection {
265
265
  this.selection?.setTransform(this.fullTransform.inverse(), false);
266
266
  this.selection?.updateUI();
267
267
 
268
- await editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize);
268
+ await editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize, true);
269
269
  this.selection?.setTransform(Mat33.identity);
270
270
  this.selection?.recomputeRegion();
271
271
  this.selection?.updateUI();