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
@@ -7,4 +7,5 @@ export { default as AbstractComponent } from './AbstractComponent';
7
7
  import Stroke from './Stroke';
8
8
  import TextComponent from './TextComponent';
9
9
  import ImageComponent from './ImageComponent';
10
- export { Stroke, TextComponent as Text, TextComponent as TextComponent, Stroke as StrokeComponent, ImageComponent, };
10
+ import RestyleableComponent, { createRestyleComponentCommand } from './RestylableComponent';
11
+ export { Stroke, TextComponent as Text, RestyleableComponent, createRestyleComponentCommand, TextComponent, Stroke as StrokeComponent, ImageComponent, };
@@ -7,4 +7,5 @@ export { default as AbstractComponent } from './AbstractComponent';
7
7
  import Stroke from './Stroke';
8
8
  import TextComponent from './TextComponent';
9
9
  import ImageComponent from './ImageComponent';
10
- export { Stroke, TextComponent as Text, TextComponent as TextComponent, Stroke as StrokeComponent, ImageComponent, };
10
+ import { createRestyleComponentCommand } from './RestylableComponent';
11
+ export { Stroke, TextComponent as Text, createRestyleComponentCommand, TextComponent, Stroke as StrokeComponent, ImageComponent, };
@@ -4,5 +4,6 @@ export interface ImageComponentLocalization {
4
4
  imageNode: (description: string) => string;
5
5
  stroke: string;
6
6
  svgObject: string;
7
+ restyledElements: string;
7
8
  }
8
9
  export declare const defaultComponentLocalization: ImageComponentLocalization;
@@ -2,6 +2,7 @@ export const defaultComponentLocalization = {
2
2
  unlabeledImageNode: 'Unlabeled image node',
3
3
  stroke: 'Stroke',
4
4
  svgObject: 'SVG Object',
5
+ restyledElements: 'Restyled elements',
5
6
  text: (text) => `Text object: ${text}`,
6
7
  imageNode: (description) => `Image: ${description}`,
7
8
  };
@@ -0,0 +1,23 @@
1
+ import RenderingStyle from './RenderingStyle';
2
+ export interface TextStyle {
3
+ size: number;
4
+ fontFamily: string;
5
+ fontWeight?: string;
6
+ fontVariant?: string;
7
+ renderingStyle: RenderingStyle;
8
+ }
9
+ export default TextStyle;
10
+ export declare const textStyleFromJSON: (json: any) => TextStyle;
11
+ export declare const textStyleToJSON: (style: TextStyle) => {
12
+ renderingStyle: {
13
+ fill: string;
14
+ stroke: {
15
+ color: string;
16
+ width: number;
17
+ } | undefined;
18
+ };
19
+ size: number;
20
+ fontFamily: string;
21
+ fontWeight?: string | undefined;
22
+ fontVariant?: string | undefined;
23
+ };
@@ -0,0 +1,20 @@
1
+ import { styleFromJSON, styleToJSON } from './RenderingStyle';
2
+ export const textStyleFromJSON = (json) => {
3
+ if (typeof json === 'string') {
4
+ json = JSON.parse(json);
5
+ }
6
+ if (typeof (json.fontFamily) !== 'string') {
7
+ throw new Error('Serialized textStyle missing string fontFamily attribute!');
8
+ }
9
+ const style = {
10
+ renderingStyle: styleFromJSON(json.renderingStyle),
11
+ size: json.size,
12
+ fontWeight: json.fontWeight,
13
+ fontVariant: json.fontVariant,
14
+ fontFamily: json.fontFamily,
15
+ };
16
+ return style;
17
+ };
18
+ export const textStyleToJSON = (style) => {
19
+ return Object.assign(Object.assign({}, style), { renderingStyle: styleToJSON(style.renderingStyle) });
20
+ };
@@ -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 } 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 from '../RenderingStyle';
8
+ import TextStyle from '../TextRenderingStyle';
9
9
  export interface RenderablePathSpec {
10
10
  startPoint: Point2;
11
11
  commands: PathCommand[];
@@ -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, RenderablePathSpec } from './AbstractRenderer';
9
9
  export default class CanvasRenderer extends AbstractRenderer {
10
10
  private ctx;
@@ -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
  export default class DummyRenderer extends AbstractRenderer {
10
10
  clearedCount: number;
@@ -1,10 +1,10 @@
1
1
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
- import { TextStyle } from '../../components/TextComponent';
3
2
  import Mat33 from '../../math/Mat33';
4
3
  import Rect2 from '../../math/Rect2';
5
4
  import { Point2, Vec2 } from '../../math/Vec2';
6
5
  import Viewport from '../../Viewport';
7
6
  import RenderingStyle from '../RenderingStyle';
7
+ import TextStyle from '../TextRenderingStyle';
8
8
  import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
9
9
  export declare const renderedStylesheetId = "js-draw-style-sheet";
10
10
  export default class SVGRenderer 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 Vec3 from '../../math/Vec3';
5
4
  import Viewport from '../../Viewport';
6
5
  import { TextRendererLocalization } from '../localization';
7
6
  import RenderingStyle from '../RenderingStyle';
7
+ import TextStyle from '../TextRenderingStyle';
8
8
  import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
9
9
  export default class TextOnlyRenderer extends AbstractRenderer {
10
10
  private localizationTable;
@@ -1,6 +1,6 @@
1
1
  import Color4 from '../Color4';
2
2
  import { ComponentBuilderFactory } from '../components/builders/types';
3
- import { TextStyle } from '../components/TextComponent';
3
+ import TextStyle from '../rendering/TextRenderingStyle';
4
4
  import Pen from '../tools/Pen';
5
5
  export type IconType = HTMLImageElement | SVGElement;
6
6
  /**
@@ -51,6 +51,7 @@ export default class IconProvider {
51
51
  makePenIcon(strokeSize: number, color: string | Color4, rounded?: boolean): IconType;
52
52
  makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): IconType;
53
53
  makePipetteIcon(color?: Color4): IconType;
54
+ makeFormatSelectionIcon(): IconType;
54
55
  makeResizeViewportIcon(): IconType;
55
56
  makeDuplicateSelectionIcon(): IconType;
56
57
  makePasteIcon(): IconType;
@@ -537,6 +537,16 @@ export default class IconProvider {
537
537
  icon.setAttribute('viewBox', '0 0 100 100');
538
538
  return icon;
539
539
  }
540
+ makeFormatSelectionIcon() {
541
+ return this.makeIconFromPath(`
542
+ M 5 10
543
+ 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
544
+ 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
545
+ M 60 25 L 55 25 L 50 30 L 60 25 z
546
+ M 10 55 L 10 90 L 41 90 L 41 86 L 45 86 L 45 55 L 10 55 z
547
+ M 42 87 L 42 93 L 48 93 L 48 87 L 42 87 z
548
+ `);
549
+ }
540
550
  makeResizeViewportIcon() {
541
551
  return this.makeIconFromPath(`
542
552
  M 75 5 75 10 90 10 90 25 95 25 95 5 75 5 z
@@ -26,6 +26,7 @@ export interface ToolbarLocalization {
26
26
  duplicateSelection: string;
27
27
  pickColorFromScreen: string;
28
28
  clickToPickColorAnnouncement: string;
29
+ reformatSelection: string;
29
30
  undo: string;
30
31
  redo: string;
31
32
  zoom: string;
@@ -5,6 +5,7 @@ export const defaultToolbarLocalization = {
5
5
  handTool: 'Pan',
6
6
  zoom: 'Zoom',
7
7
  image: 'Image',
8
+ reformatSelection: 'Format selection',
8
9
  inputAltText: 'Alt text: ',
9
10
  chooseFile: 'Choose file: ',
10
11
  submit: 'Submit',
@@ -88,24 +88,30 @@ export default class BaseWidget {
88
88
  }
89
89
  // If we didn't do anything with the event, send it to the editor.
90
90
  if (!handled) {
91
- this.editor.toolController.dispatchInputEvent({
91
+ handled = this.editor.toolController.dispatchInputEvent({
92
92
  kind: InputEvtType.KeyPressEvent,
93
93
  key: evt.key,
94
- ctrlKey: evt.ctrlKey,
94
+ ctrlKey: evt.ctrlKey || evt.metaKey,
95
95
  altKey: evt.altKey,
96
96
  });
97
97
  }
98
+ if (handled) {
99
+ evt.preventDefault();
100
+ }
98
101
  };
99
102
  button.onkeyup = evt => {
100
103
  if (evt.key in clickTriggers) {
101
104
  return;
102
105
  }
103
- this.editor.toolController.dispatchInputEvent({
106
+ const handled = this.editor.toolController.dispatchInputEvent({
104
107
  kind: InputEvtType.KeyUpEvent,
105
108
  key: evt.key,
106
- ctrlKey: evt.ctrlKey,
109
+ ctrlKey: evt.ctrlKey || evt.metaKey,
107
110
  altKey: evt.altKey,
108
111
  });
112
+ if (handled) {
113
+ evt.preventDefault();
114
+ }
109
115
  };
110
116
  button.onclick = () => {
111
117
  if (!this.disabled) {
@@ -10,7 +10,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import ImageComponent from '../../components/ImageComponent';
11
11
  import Erase from '../../commands/Erase';
12
12
  import EditorImage from '../../EditorImage';
13
- import { SelectionTool, uniteCommands } from '../../lib';
13
+ import uniteCommands from '../../commands/uniteCommands';
14
+ import SelectionTool from '../../tools/SelectionTool/SelectionTool';
14
15
  import Mat33 from '../../math/Mat33';
15
16
  import fileToBase64 from '../../util/fileToBase64';
16
17
  import ActionButtonWidget from './ActionButtonWidget';
@@ -7,9 +7,82 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ import Color4 from '../../Color4';
11
+ import { isRestylableComponent } from '../../components/RestylableComponent';
12
+ import uniteCommands from '../../commands/uniteCommands';
10
13
  import { EditorEventType } from '../../types';
14
+ import makeColorInput from '../makeColorInput';
11
15
  import ActionButtonWidget from './ActionButtonWidget';
12
16
  import BaseToolWidget from './BaseToolWidget';
17
+ import BaseWidget from './BaseWidget';
18
+ class RestyleSelectionWidget extends BaseWidget {
19
+ constructor(editor, selectionTool, localizationTable) {
20
+ super(editor, 'restyle-selection', localizationTable);
21
+ this.selectionTool = selectionTool;
22
+ this.updateFormatData = () => { };
23
+ // Allow showing the dropdown even if this widget isn't selected yet
24
+ this.container.classList.add('dropdownShowable');
25
+ this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
26
+ if (toolEvt.kind !== EditorEventType.ToolUpdated) {
27
+ throw new Error('Invalid event type!');
28
+ }
29
+ if (toolEvt.tool === this.selectionTool) {
30
+ this.updateFormatData();
31
+ }
32
+ });
33
+ }
34
+ getTitle() {
35
+ return this.localizationTable.reformatSelection;
36
+ }
37
+ createIcon() {
38
+ return this.editor.icons.makeFormatSelectionIcon();
39
+ }
40
+ handleClick() {
41
+ this.setDropdownVisible(!this.isDropdownVisible());
42
+ }
43
+ fillDropdown(dropdown) {
44
+ const container = document.createElement('div');
45
+ const colorRow = document.createElement('div');
46
+ const colorLabel = document.createElement('label');
47
+ const [colorInput, colorInputContainer, setColorInputValue] = makeColorInput(this.editor, color => {
48
+ const selection = this.selectionTool.getSelection();
49
+ if (selection) {
50
+ const updateStyleCommands = [];
51
+ for (const elem of selection.getSelectedObjects()) {
52
+ if (isRestylableComponent(elem)) {
53
+ updateStyleCommands.push(elem.updateStyle({ color }));
54
+ }
55
+ }
56
+ const unitedCommand = uniteCommands(updateStyleCommands);
57
+ this.editor.dispatch(unitedCommand);
58
+ }
59
+ });
60
+ colorLabel.innerText = this.localizationTable.colorLabel;
61
+ this.updateFormatData = () => {
62
+ const selection = this.selectionTool.getSelection();
63
+ if (selection) {
64
+ colorInput.disabled = false;
65
+ const colors = [];
66
+ for (const elem of selection.getSelectedObjects()) {
67
+ if (isRestylableComponent(elem)) {
68
+ const color = elem.getStyle().color;
69
+ if (color) {
70
+ colors.push(color);
71
+ }
72
+ }
73
+ }
74
+ setColorInputValue(Color4.average(colors));
75
+ }
76
+ else {
77
+ colorInput.disabled = true;
78
+ }
79
+ };
80
+ colorRow.replaceChildren(colorLabel, colorInputContainer);
81
+ container.replaceChildren(colorRow);
82
+ dropdown.replaceChildren(container);
83
+ return true;
84
+ }
85
+ }
13
86
  export default class SelectionToolWidget extends BaseToolWidget {
14
87
  constructor(editor, tool, localization) {
15
88
  super(editor, tool, 'selection-tool-widget', localization);
@@ -26,13 +99,16 @@ export default class SelectionToolWidget extends BaseToolWidget {
26
99
  const selection = this.tool.getSelection();
27
100
  this.editor.dispatch(yield selection.duplicateSelectedObjects());
28
101
  }), localization);
102
+ const restyleButton = new RestyleSelectionWidget(editor, this.tool, localization);
29
103
  this.addSubWidget(resizeButton);
30
104
  this.addSubWidget(deleteButton);
31
105
  this.addSubWidget(duplicateButton);
106
+ this.addSubWidget(restyleButton);
32
107
  const updateDisabled = (disabled) => {
33
108
  resizeButton.setDisabled(disabled);
34
109
  deleteButton.setDisabled(disabled);
35
110
  duplicateButton.setDisabled(disabled);
111
+ restyleButton.setDisabled(disabled);
36
112
  };
37
113
  updateDisabled(true);
38
114
  // Enable/disable actions based on whether items are selected
@@ -42,7 +118,7 @@ export default class SelectionToolWidget extends BaseToolWidget {
42
118
  }
43
119
  if (toolEvt.tool === this.tool) {
44
120
  const selection = this.tool.getSelection();
45
- const hasSelection = selection && selection.region.area > 0;
121
+ const hasSelection = selection && selection.getSelectedItemCount() > 0;
46
122
  updateDisabled(!hasSelection);
47
123
  }
48
124
  });
@@ -152,7 +152,7 @@ export default class Pen extends BaseTool {
152
152
  this.setThickness(newThickness);
153
153
  return true;
154
154
  }
155
- if (key === 'control') {
155
+ if (key === 'control' || key === 'meta') {
156
156
  this.ctrlKeyPressed = true;
157
157
  return true;
158
158
  }
@@ -164,7 +164,7 @@ export default class Pen extends BaseTool {
164
164
  }
165
165
  onKeyUp({ key }) {
166
166
  key = key.toLowerCase();
167
- if (key === 'control') {
167
+ if (key === 'control' || key === 'meta') {
168
168
  this.ctrlKeyPressed = false;
169
169
  return true;
170
170
  }
@@ -0,0 +1,8 @@
1
+ import Editor from '../../Editor';
2
+ import { KeyPressEvent } from '../../types';
3
+ import BaseTool from '../BaseTool';
4
+ export default class SelectAllShortcutHandler extends BaseTool {
5
+ private editor;
6
+ constructor(editor: Editor);
7
+ onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean;
8
+ }
@@ -0,0 +1,22 @@
1
+ import BaseTool from '../BaseTool';
2
+ import SelectionTool from './SelectionTool';
3
+ // Handles ctrl+a: Select all
4
+ export default class SelectAllShortcutHandler extends BaseTool {
5
+ constructor(editor) {
6
+ super(editor.notifier, editor.localization.selectAllTool);
7
+ this.editor = editor;
8
+ }
9
+ // @internal
10
+ onKeyPress({ key, ctrlKey }) {
11
+ if (ctrlKey && key === 'a') {
12
+ const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool);
13
+ if (selectionTools.length > 0) {
14
+ const selectionTool = selectionTools[0];
15
+ selectionTool.setEnabled(true);
16
+ selectionTool.setSelection(this.editor.image.getAllElements());
17
+ return true;
18
+ }
19
+ }
20
+ return false;
21
+ }
22
+ }
@@ -440,7 +440,7 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
440
440
  this.resolveToElems(editor);
441
441
  (_b = this.selection) === null || _b === void 0 ? void 0 : _b.setTransform(this.fullTransform.inverse(), false);
442
442
  (_c = this.selection) === null || _c === void 0 ? void 0 : _c.updateUI();
443
- yield editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize);
443
+ yield editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize, true);
444
444
  (_d = this.selection) === null || _d === void 0 ? void 0 : _d.setTransform(Mat33.identity);
445
445
  (_e = this.selection) === null || _e === void 0 ? void 0 : _e.recomputeRegion();
446
446
  (_f = this.selection) === null || _f === void 0 ? void 0 : _f.updateUI();
@@ -158,7 +158,7 @@ export default class SelectionTool extends BaseTool {
158
158
  }
159
159
  }
160
160
  onGestureCancel() {
161
- var _a, _b, _c;
161
+ var _a, _b, _c, _d;
162
162
  if (this.selectionBoxHandlingEvt) {
163
163
  (_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.onDragCancel();
164
164
  }
@@ -167,11 +167,13 @@ export default class SelectionTool extends BaseTool {
167
167
  (_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.cancelSelection();
168
168
  this.selectionBox = this.prevSelectionBox;
169
169
  (_c = this.selectionBox) === null || _c === void 0 ? void 0 : _c.addTo(this.handleOverlay);
170
+ (_d = this.selectionBox) === null || _d === void 0 ? void 0 : _d.recomputeRegion();
171
+ this.prevSelectionBox = null;
170
172
  }
171
173
  this.expandingSelectionBox = false;
172
174
  }
173
175
  onKeyPress(event) {
174
- if (event.key === 'Control') {
176
+ if (event.key === 'Control' || event.key === 'Meta') {
175
177
  this.ctrlKeyPressed = true;
176
178
  return true;
177
179
  }
@@ -181,8 +183,7 @@ export default class SelectionTool extends BaseTool {
181
183
  return true;
182
184
  }
183
185
  else if (event.key === 'a' && event.ctrlKey) {
184
- // Handle ctrl+A on key up.
185
- // Return early to prevent 'a' from moving the selection/view.
186
+ this.setSelection(this.editor.image.getAllElements());
186
187
  return true;
187
188
  }
188
189
  else if (event.ctrlKey) {
@@ -268,7 +269,7 @@ export default class SelectionTool extends BaseTool {
268
269
  return handled;
269
270
  }
270
271
  onKeyUp(evt) {
271
- if (evt.key === 'Control') {
272
+ if (evt.key === 'Control' || evt.key === 'Meta') {
272
273
  this.ctrlKeyPressed = false;
273
274
  return true;
274
275
  }
@@ -283,10 +284,6 @@ export default class SelectionTool extends BaseTool {
283
284
  });
284
285
  return true;
285
286
  }
286
- else if (evt.key === 'a') {
287
- this.setSelection(this.editor.image.getAllElements());
288
- return true;
289
- }
290
287
  }
291
288
  if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
292
289
  this.selectionBox.finalizeTransform();
@@ -394,5 +391,5 @@ SelectionTool.handleableKeys = [
394
391
  'e', 'j', 'ArrowDown',
395
392
  'r', 'R',
396
393
  'i', 'I', 'o', 'O',
397
- 'Control',
394
+ 'Control', 'Meta',
398
395
  ];
@@ -1,9 +1,9 @@
1
1
  import Color4 from '../Color4';
2
- import { TextStyle } from '../components/TextComponent';
3
2
  import Editor from '../Editor';
4
3
  import { PointerEvt } from '../types';
5
4
  import BaseTool from './BaseTool';
6
5
  import { ToolLocalization } from './localization';
6
+ import TextStyle from '../rendering/TextRenderingStyle';
7
7
  export default class TextTool extends BaseTool {
8
8
  private editor;
9
9
  private localizationTable;
@@ -13,6 +13,7 @@ import PasteHandler from './PasteHandler';
13
13
  import ToolbarShortcutHandler from './ToolbarShortcutHandler';
14
14
  import { makePressureSensitiveFreehandLineBuilder } from '../components/builders/PressureSensitiveFreehandLineBuilder';
15
15
  import FindTool from './FindTool';
16
+ import SelectAllShortcutHandler from './SelectionTool/SelectAllShortcutHandler';
16
17
  export default class ToolController {
17
18
  /** @internal */
18
19
  constructor(editor, localization) {
@@ -43,6 +44,7 @@ export default class ToolController {
43
44
  new ToolSwitcherShortcut(editor),
44
45
  new FindTool(editor),
45
46
  new PasteHandler(editor),
47
+ new SelectAllShortcutHandler(editor),
46
48
  ];
47
49
  primaryTools.forEach(tool => tool.setToolGroup(primaryToolGroup));
48
50
  panZoomTool.setEnabled(true);
@@ -10,6 +10,7 @@ export { default as PanZoomTool, PanZoomMode } from './PanZoom';
10
10
  export { default as PenTool, PenStyle } from './Pen';
11
11
  export { default as TextTool } from './TextTool';
12
12
  export { default as SelectionTool } from './SelectionTool/SelectionTool';
13
+ export { default as SelectAllShortcutHandler } from './SelectionTool/SelectAllShortcutHandler';
13
14
  export { default as EraserTool } from './Eraser';
14
15
  export { default as PasteHandler } from './PasteHandler';
15
16
  export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
@@ -10,6 +10,7 @@ export { default as PanZoomTool, PanZoomMode } from './PanZoom';
10
10
  export { default as PenTool } from './Pen';
11
11
  export { default as TextTool } from './TextTool';
12
12
  export { default as SelectionTool } from './SelectionTool/SelectionTool';
13
+ export { default as SelectAllShortcutHandler } from './SelectionTool/SelectAllShortcutHandler';
13
14
  export { default as EraserTool } from './Eraser';
14
15
  export { default as PasteHandler } from './PasteHandler';
15
16
  export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
@@ -2,6 +2,7 @@ export interface ToolLocalization {
2
2
  keyboardPanZoom: string;
3
3
  penTool: (penId: number) => string;
4
4
  selectionTool: string;
5
+ selectAllTool: string;
5
6
  eraserTool: string;
6
7
  touchPanTool: string;
7
8
  twoFingerPanZoomTool: string;
@@ -1,6 +1,7 @@
1
1
  export const defaultToolLocalization = {
2
2
  penTool: (penId) => `Pen ${penId}`,
3
3
  selectionTool: 'Selection',
4
+ selectAllTool: 'Select all shortcut',
4
5
  eraserTool: 'Eraser',
5
6
  touchPanTool: 'Touch panning',
6
7
  twoFingerPanZoomTool: 'Panning and zooming',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "./dist/src/lib.d.ts",
6
6
  "types": "./dist/src/lib.js",
@@ -27,4 +27,8 @@ describe('Color4', () => {
27
27
  Color4.ofRGB(0.7, 0.3, 0)
28
28
  );
29
29
  });
30
+
31
+ it('should mix red with nothing and get red', () => {
32
+ expect(Color4.average([ Color4.red ])).objEq(Color4.red);
33
+ });
30
34
  });
package/src/Color4.ts CHANGED
@@ -151,6 +151,32 @@ export default class Color4 {
151
151
  );
152
152
  }
153
153
 
154
+ /**
155
+ * @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
156
+ */
157
+ public static average(colors: Color4[]) {
158
+ let averageA = 0;
159
+ let averageR = 0;
160
+ let averageG = 0;
161
+ let averageB = 0;
162
+
163
+ for (const color of colors) {
164
+ averageA += color.a;
165
+ averageR += color.r;
166
+ averageG += color.g;
167
+ averageB += color.b;
168
+ }
169
+
170
+ if (colors.length > 0) {
171
+ averageA /= colors.length;
172
+ averageR /= colors.length;
173
+ averageG /= colors.length;
174
+ averageB /= colors.length;
175
+ }
176
+
177
+ return new Color4(averageR, averageG, averageB, averageA);
178
+ }
179
+
154
180
  private hexString: string|null = null;
155
181
 
156
182
  /**
@@ -1,5 +1,5 @@
1
- import { TextStyle } from './components/TextComponent';
2
1
  import { Color4, Mat33, Rect2, TextComponent, EditorImage, Vec2 } from './lib';
2
+ import TextStyle from './rendering/TextRenderingStyle';
3
3
  import SVGLoader from './SVGLoader';
4
4
  import createEditor from './testing/createEditor';
5
5
 
package/src/Editor.ts CHANGED
@@ -704,8 +704,14 @@ export class Editor {
704
704
  return this.asyncApplyOrUnapplyCommands(commands, true, chunkSize);
705
705
  }
706
706
 
707
+ // If `unapplyInReverseOrder`, commands are reversed before unapplying.
707
708
  // @see {@link #asyncApplyOrUnapplyCommands }
708
- public asyncUnapplyCommands(commands: Command[], chunkSize: number) {
709
+ public asyncUnapplyCommands(commands: Command[], chunkSize: number, unapplyInReverseOrder: boolean = false) {
710
+ if (unapplyInReverseOrder) {
711
+ commands = [ ...commands ]; // copy
712
+ commands.reverse();
713
+ }
714
+
709
715
  return this.asyncApplyOrUnapplyCommands(commands, false, chunkSize);
710
716
  }
711
717
 
@@ -745,6 +751,11 @@ export class Editor {
745
751
  });
746
752
  }
747
753
 
754
+ // @internal
755
+ public isRerenderQueued() {
756
+ return this.rerenderQueued;
757
+ }
758
+
748
759
  public rerender(showImageBounds: boolean = true) {
749
760
  this.display.startRerender();
750
761