js-draw 0.13.1 → 0.15.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 (100) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +8 -0
  2. package/CHANGELOG.md +15 -0
  3. package/README.md +1 -1
  4. package/dist/bundle.js +1 -1
  5. package/dist/src/Color4.d.ts +4 -0
  6. package/dist/src/Color4.js +22 -0
  7. package/dist/src/Editor.d.ts +2 -1
  8. package/dist/src/Editor.js +14 -5
  9. package/dist/src/EditorImage.d.ts +1 -0
  10. package/dist/src/EditorImage.js +11 -0
  11. package/dist/src/SVGLoader.js +8 -2
  12. package/dist/src/Viewport.d.ts +1 -0
  13. package/dist/src/Viewport.js +6 -3
  14. package/dist/src/commands/UnresolvedCommand.d.ts +14 -0
  15. package/dist/src/commands/UnresolvedCommand.js +22 -0
  16. package/dist/src/commands/uniteCommands.js +4 -2
  17. package/dist/src/components/AbstractComponent.d.ts +0 -1
  18. package/dist/src/components/AbstractComponent.js +30 -50
  19. package/dist/src/components/RestylableComponent.d.ts +24 -0
  20. package/dist/src/components/RestylableComponent.js +80 -0
  21. package/dist/src/components/Stroke.d.ts +8 -1
  22. package/dist/src/components/Stroke.js +49 -1
  23. package/dist/src/components/TextComponent.d.ts +10 -10
  24. package/dist/src/components/TextComponent.js +46 -13
  25. package/dist/src/components/lib.d.ts +2 -1
  26. package/dist/src/components/lib.js +2 -1
  27. package/dist/src/components/localization.d.ts +1 -0
  28. package/dist/src/components/localization.js +1 -0
  29. package/dist/src/math/Path.js +10 -3
  30. package/dist/src/rendering/TextRenderingStyle.d.ts +23 -0
  31. package/dist/src/rendering/TextRenderingStyle.js +20 -0
  32. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -1
  33. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +1 -1
  34. package/dist/src/rendering/renderers/DummyRenderer.d.ts +1 -1
  35. package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
  36. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +1 -1
  37. package/dist/src/toolbar/IconProvider.d.ts +30 -3
  38. package/dist/src/toolbar/IconProvider.js +37 -2
  39. package/dist/src/toolbar/localization.d.ts +1 -0
  40. package/dist/src/toolbar/localization.js +1 -0
  41. package/dist/src/toolbar/widgets/BaseWidget.js +10 -4
  42. package/dist/src/toolbar/widgets/InsertImageWidget.js +2 -1
  43. package/dist/src/toolbar/widgets/SelectionToolWidget.js +77 -1
  44. package/dist/src/tools/Pen.js +2 -2
  45. package/dist/src/tools/SelectionTool/SelectAllShortcutHandler.d.ts +8 -0
  46. package/dist/src/tools/SelectionTool/SelectAllShortcutHandler.js +22 -0
  47. package/dist/src/tools/SelectionTool/Selection.d.ts +6 -0
  48. package/dist/src/tools/SelectionTool/Selection.js +13 -4
  49. package/dist/src/tools/SelectionTool/SelectionTool.js +9 -12
  50. package/dist/src/tools/SelectionTool/TransformMode.js +1 -1
  51. package/dist/src/tools/TextTool.d.ts +1 -1
  52. package/dist/src/tools/ToolController.js +2 -0
  53. package/dist/src/tools/lib.d.ts +1 -0
  54. package/dist/src/tools/lib.js +1 -0
  55. package/dist/src/tools/localization.d.ts +1 -0
  56. package/dist/src/tools/localization.js +1 -0
  57. package/package.json +1 -1
  58. package/src/Color4.test.ts +4 -0
  59. package/src/Color4.ts +26 -0
  60. package/src/Editor.toSVG.test.ts +1 -1
  61. package/src/Editor.ts +16 -5
  62. package/src/EditorImage.ts +13 -0
  63. package/src/SVGLoader.ts +11 -3
  64. package/src/Viewport.ts +7 -3
  65. package/src/commands/UnresolvedCommand.ts +37 -0
  66. package/src/commands/uniteCommands.ts +5 -2
  67. package/src/components/AbstractComponent.ts +36 -61
  68. package/src/components/RestylableComponent.ts +142 -0
  69. package/src/components/Stroke.test.ts +68 -0
  70. package/src/components/Stroke.ts +68 -2
  71. package/src/components/TextComponent.test.ts +56 -2
  72. package/src/components/TextComponent.ts +63 -25
  73. package/src/components/lib.ts +4 -1
  74. package/src/components/localization.ts +3 -0
  75. package/src/math/Path.toString.test.ts +10 -0
  76. package/src/math/Path.ts +11 -3
  77. package/src/math/Rect2.test.ts +18 -6
  78. package/src/rendering/TextRenderingStyle.ts +38 -0
  79. package/src/rendering/renderers/AbstractRenderer.ts +1 -1
  80. package/src/rendering/renderers/CanvasRenderer.ts +2 -1
  81. package/src/rendering/renderers/DummyRenderer.ts +1 -1
  82. package/src/rendering/renderers/SVGRenderer.ts +1 -1
  83. package/src/rendering/renderers/TextOnlyRenderer.ts +1 -1
  84. package/src/toolbar/IconProvider.ts +40 -7
  85. package/src/toolbar/localization.ts +2 -0
  86. package/src/toolbar/toolbar.css +3 -0
  87. package/src/toolbar/widgets/BaseWidget.ts +12 -4
  88. package/src/toolbar/widgets/InsertImageWidget.ts +2 -1
  89. package/src/toolbar/widgets/SelectionToolWidget.ts +95 -1
  90. package/src/tools/PanZoom.test.ts +2 -1
  91. package/src/tools/PasteHandler.ts +1 -1
  92. package/src/tools/Pen.ts +2 -2
  93. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +28 -0
  94. package/src/tools/SelectionTool/Selection.ts +17 -6
  95. package/src/tools/SelectionTool/SelectionTool.ts +9 -13
  96. package/src/tools/SelectionTool/TransformMode.ts +1 -1
  97. package/src/tools/TextTool.ts +2 -1
  98. package/src/tools/ToolController.ts +2 -0
  99. package/src/tools/lib.ts +1 -0
  100. package/src/tools/localization.ts +2 -0
@@ -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,17 +1,14 @@
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';
10
10
 
11
- // Provides a default set of icons for the editor.
12
- // Many of the icons were created with Inkscape.
13
-
14
- type IconType = SVGSVGElement|HTMLImageElement;
11
+ export type IconType = HTMLImageElement|SVGElement;
15
12
 
16
13
  const svgNamespace = 'http://www.w3.org/2000/svg';
17
14
  const iconColorFill = `
@@ -35,8 +32,33 @@ const checkerboardPatternDef = `
35
32
  `;
36
33
  const checkerboardPatternRef = 'url(#checkerboard)';
37
34
 
38
- // Provides icons that can be used in the toolbar, etc.
39
- // Extend this class and override methods to customize icons.
35
+ /**
36
+ * Provides icons that can be used in the toolbar, etc.
37
+ * Extend this class and override methods to customize icons.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * class CustomIconProvider extends jsdraw.IconProvider {
42
+ * // Use '☺' instead of the default dropdown symbol.
43
+ * public makeDropdownIcon() {
44
+ * const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
45
+ * icon.innerHTML = `
46
+ * <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
47
+ * `;
48
+ * icon.setAttribute('viewBox', '0 0 100 100');
49
+ * return icon;
50
+ * }
51
+ * }
52
+ *
53
+ * const icons = new CustomIconProvider();
54
+ * const editor = new jsdraw.Editor(document.body, {
55
+ * iconProvider: icons,
56
+ * });
57
+ *
58
+ * // Add a toolbar that uses these icons
59
+ * editor.addToolbar();
60
+ * ```
61
+ */
40
62
  export default class IconProvider {
41
63
 
42
64
  public makeUndoIcon(): IconType {
@@ -587,6 +609,17 @@ export default class IconProvider {
587
609
  icon.setAttribute('viewBox', '0 0 100 100');
588
610
  return icon;
589
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
+ }
590
623
 
591
624
  public makeResizeViewportIcon(): IconType {
592
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',
@@ -33,6 +33,7 @@
33
33
 
34
34
  .toolbar-dropdown .toolbar-button > .toolbar-icon {
35
35
  max-width: 50px;
36
+ width: 100%;
36
37
  }
37
38
 
38
39
  .toolbar-button.disabled {
@@ -89,6 +90,8 @@
89
90
 
90
91
  .toolbar-root .toolbar-icon {
91
92
  flex-shrink: 1;
93
+
94
+ width: 100%;
92
95
  min-width: 30px;
93
96
  min-height: 30px;
94
97
  }
@@ -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
+ }
@@ -121,6 +121,21 @@ export default class Selection {
121
121
  return this.originalRegion.transformedBoundingBox(scaleAndTranslateMat);
122
122
  }
123
123
 
124
+ /**
125
+ * Computes and returns the bounding box of the selection without
126
+ * any additional padding. Computes directly from the elements that are selected.
127
+ * @internal
128
+ */
129
+ public computeTightBoundingBox() {
130
+ const bbox = this.selectedElems.reduce((
131
+ accumulator: Rect2|null, elem: AbstractComponent
132
+ ): Rect2 => {
133
+ return (accumulator ?? elem.getBBox()).union(elem.getBBox());
134
+ }, null);
135
+
136
+ return bbox ?? Rect2.empty;
137
+ }
138
+
124
139
  public get regionRotation(): number {
125
140
  return this.transform.transformVec3(Vec2.unitX).angle();
126
141
  }
@@ -250,7 +265,7 @@ export default class Selection {
250
265
  this.selection?.setTransform(this.fullTransform.inverse(), false);
251
266
  this.selection?.updateUI();
252
267
 
253
- await editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize);
268
+ await editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize, true);
254
269
  this.selection?.setTransform(Mat33.identity);
255
270
  this.selection?.recomputeRegion();
256
271
  this.selection?.updateUI();
@@ -328,11 +343,7 @@ export default class Selection {
328
343
  // Recompute this' region from the selected elements.
329
344
  // Returns false if the selection is empty.
330
345
  public recomputeRegion(): boolean {
331
- const newRegion = this.selectedElems.reduce((
332
- accumulator: Rect2|null, elem: AbstractComponent
333
- ): Rect2 => {
334
- return (accumulator ?? elem.getBBox()).union(elem.getBBox());
335
- }, null);
346
+ const newRegion = this.computeTightBoundingBox();
336
347
 
337
348
  if (!newRegion) {
338
349
  this.cancelSelection();
@@ -62,12 +62,13 @@ export default class SelectionTool extends BaseTool {
62
62
  private snapSelectionToGrid() {
63
63
  if (!this.selectionBox) throw new Error('No selection to snap!');
64
64
 
65
- const topLeftOfBBox = this.selectionBox.region.topLeft;
66
- const snapDistance =
67
- this.editor.viewport.snapToGrid(topLeftOfBBox).minus(topLeftOfBBox);
65
+ // Snap the top left corner of what we have selected.
66
+ const topLeftOfBBox = this.selectionBox.computeTightBoundingBox().topLeft;
67
+ const snappedTopLeft = this.editor.viewport.snapToGrid(topLeftOfBBox);
68
+ const snapDelta = snappedTopLeft.minus(topLeftOfBBox);
68
69
 
69
70
  const oldTransform = this.selectionBox.getTransform();
70
- this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(snapDistance)));
71
+ this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(snapDelta)));
71
72
  this.selectionBox.finalizeTransform();
72
73
  }
73
74
 
@@ -211,10 +212,10 @@ export default class SelectionTool extends BaseTool {
211
212
  'e', 'j', 'ArrowDown',
212
213
  'r', 'R',
213
214
  'i', 'I', 'o', 'O',
214
- 'Control',
215
+ 'Control', 'Meta',
215
216
  ];
216
217
  public onKeyPress(event: KeyPressEvent): boolean {
217
- if (event.key === 'Control') {
218
+ if (event.key === 'Control' || event.key === 'Meta') {
218
219
  this.ctrlKeyPressed = true;
219
220
  return true;
220
221
  }
@@ -225,8 +226,7 @@ export default class SelectionTool extends BaseTool {
225
226
  return true;
226
227
  }
227
228
  else if (event.key === 'a' && event.ctrlKey) {
228
- // Handle ctrl+A on key up.
229
- // Return early to prevent 'a' from moving the selection/view.
229
+ this.setSelection(this.editor.image.getAllElements());
230
230
  return true;
231
231
  }
232
232
  else if (event.ctrlKey) {
@@ -334,7 +334,7 @@ export default class SelectionTool extends BaseTool {
334
334
  }
335
335
 
336
336
  public onKeyUp(evt: KeyUpEvent) {
337
- if (evt.key === 'Control') {
337
+ if (evt.key === 'Control' || evt.key === 'Meta') {
338
338
  this.ctrlKeyPressed = false;
339
339
  return true;
340
340
  }
@@ -350,10 +350,6 @@ export default class SelectionTool extends BaseTool {
350
350
  });
351
351
  return true;
352
352
  }
353
- else if (evt.key === 'a') {
354
- this.setSelection(this.editor.image.getAllElements());
355
- return true;
356
- }
357
353
  }
358
354
 
359
355
  if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
@@ -62,7 +62,7 @@ export class ResizeTransformer {
62
62
  // long decimal representations => large file sizes.
63
63
  scale = scale.map(component => Viewport.roundScaleRatio(component, 2));
64
64
 
65
- if (scale.x > 0 && scale.y > 0) {
65
+ if (scale.x !== 0 && scale.y !== 0) {
66
66
  const origin = this.editor.viewport.roundPoint(this.selection.preTransformRegion.topLeft);
67
67
  this.selection.setTransform(Mat33.scaling2D(scale, origin));
68
68
  }
@@ -1,5 +1,5 @@
1
1
  import Color4 from '../Color4';
2
- import TextComponent, { TextStyle } from '../components/TextComponent';
2
+ import TextComponent from '../components/TextComponent';
3
3
  import Editor from '../Editor';
4
4
  import EditorImage from '../EditorImage';
5
5
  import Rect2 from '../math/Rect2';
@@ -11,6 +11,7 @@ import BaseTool from './BaseTool';
11
11
  import { ToolLocalization } from './localization';
12
12
  import Erase from '../commands/Erase';
13
13
  import uniteCommands from '../commands/uniteCommands';
14
+ import TextStyle from '../rendering/TextRenderingStyle';
14
15
 
15
16
  const overlayCssClass = 'textEditorOverlay';
16
17
  export default class TextTool extends BaseTool {
@@ -16,6 +16,7 @@ import PasteHandler from './PasteHandler';
16
16
  import ToolbarShortcutHandler from './ToolbarShortcutHandler';
17
17
  import { makePressureSensitiveFreehandLineBuilder } from '../components/builders/PressureSensitiveFreehandLineBuilder';
18
18
  import FindTool from './FindTool';
19
+ import SelectAllShortcutHandler from './SelectionTool/SelectAllShortcutHandler';
19
20
 
20
21
  export default class ToolController {
21
22
  private tools: BaseTool[];
@@ -53,6 +54,7 @@ export default class ToolController {
53
54
  new ToolSwitcherShortcut(editor),
54
55
  new FindTool(editor),
55
56
  new PasteHandler(editor),
57
+ new SelectAllShortcutHandler(editor),
56
58
  ];
57
59
  primaryTools.forEach(tool => tool.setToolGroup(primaryToolGroup));
58
60
  panZoomTool.setEnabled(true);
package/src/tools/lib.ts CHANGED
@@ -14,6 +14,7 @@ export { default as PanZoomTool, PanZoomMode } from './PanZoom';
14
14
  export { default as PenTool, PenStyle } from './Pen';
15
15
  export { default as TextTool } from './TextTool';
16
16
  export { default as SelectionTool } from './SelectionTool/SelectionTool';
17
+ export { default as SelectAllShortcutHandler } from './SelectionTool/SelectAllShortcutHandler';
17
18
  export { default as EraserTool } from './Eraser';
18
19
  export { default as PasteHandler } from './PasteHandler';
19
20
 
@@ -3,6 +3,7 @@ export interface ToolLocalization {
3
3
  keyboardPanZoom: string;
4
4
  penTool: (penId: number)=>string;
5
5
  selectionTool: string;
6
+ selectAllTool: string;
6
7
  eraserTool: string;
7
8
  touchPanTool: string;
8
9
  twoFingerPanZoomTool: string;
@@ -34,6 +35,7 @@ export interface ToolLocalization {
34
35
  export const defaultToolLocalization: ToolLocalization = {
35
36
  penTool: (penId) => `Pen ${penId}`,
36
37
  selectionTool: 'Selection',
38
+ selectAllTool: 'Select all shortcut',
37
39
  eraserTool: 'Eraser',
38
40
  touchPanTool: 'Touch panning',
39
41
  twoFingerPanZoomTool: 'Panning and zooming',