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
@@ -2,8 +2,9 @@ import LineSegment2 from '../math/LineSegment2';
2
2
  import Mat33 from '../math/Mat33';
3
3
  import Rect2 from '../math/Rect2';
4
4
  import { Vec2 } from '../math/Vec2';
5
- import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
5
+ import { textStyleFromJSON, textStyleToJSON } from '../rendering/TextRenderingStyle';
6
6
  import AbstractComponent from './AbstractComponent';
7
+ import { createRestyleComponentCommand } from './RestylableComponent';
7
8
  const componentTypeId = 'text';
8
9
  export default class TextComponent extends AbstractComponent {
9
10
  constructor(textObjects, transform, style) {
@@ -11,6 +12,8 @@ export default class TextComponent extends AbstractComponent {
11
12
  this.textObjects = textObjects;
12
13
  this.transform = transform;
13
14
  this.style = style;
15
+ // eslint-disable-next-line @typescript-eslint/prefer-as-const
16
+ this.isRestylableComponent = true;
14
17
  this.recomputeBBox();
15
18
  // If this has no direct children, choose a style representative of this' content
16
19
  // (useful for estimating the style of the TextComponent).
@@ -117,12 +120,43 @@ export default class TextComponent extends AbstractComponent {
117
120
  }
118
121
  return false;
119
122
  }
120
- getBaselinePos() {
121
- return this.transform.transformVec2(Vec2.zero);
123
+ getStyle() {
124
+ return {
125
+ color: this.style.renderingStyle.fill,
126
+ // Make a copy
127
+ textStyle: Object.assign(Object.assign({}, this.style), { renderingStyle: Object.assign({}, this.style.renderingStyle) }),
128
+ };
129
+ }
130
+ updateStyle(style) {
131
+ return createRestyleComponentCommand(this.getStyle(), style, this);
132
+ }
133
+ forceStyle(style, editor) {
134
+ if (style.textStyle) {
135
+ this.style = style.textStyle;
136
+ }
137
+ else if (style.color) {
138
+ this.style.renderingStyle = Object.assign(Object.assign({}, this.style.renderingStyle), { fill: style.color });
139
+ }
140
+ else {
141
+ return;
142
+ }
143
+ for (const child of this.textObjects) {
144
+ if (child instanceof TextComponent) {
145
+ child.forceStyle(style, editor);
146
+ }
147
+ }
148
+ if (editor) {
149
+ editor.image.queueRerenderOf(this);
150
+ editor.queueRerender();
151
+ }
122
152
  }
153
+ // See this.getStyle
123
154
  getTextStyle() {
124
155
  return this.style;
125
156
  }
157
+ getBaselinePos() {
158
+ return this.transform.transformVec2(Vec2.zero);
159
+ }
126
160
  getTransform() {
127
161
  return this.transform;
128
162
  }
@@ -148,9 +182,10 @@ export default class TextComponent extends AbstractComponent {
148
182
  description(localizationTable) {
149
183
  return localizationTable.text(this.getText());
150
184
  }
185
+ // Do not rely on the output of `serializeToJSON` taking any particular format.
151
186
  serializeToJSON() {
152
- const serializableStyle = Object.assign(Object.assign({}, this.style), { renderingStyle: styleToJSON(this.style.renderingStyle) });
153
- const textObjects = this.textObjects.map(text => {
187
+ const serializableStyle = textStyleToJSON(this.style);
188
+ const serializedTextObjects = this.textObjects.map(text => {
154
189
  if (typeof text === 'string') {
155
190
  return {
156
191
  text,
@@ -163,19 +198,17 @@ export default class TextComponent extends AbstractComponent {
163
198
  }
164
199
  });
165
200
  return {
166
- textObjects,
201
+ textObjects: serializedTextObjects,
167
202
  transform: this.transform.toArray(),
168
203
  style: serializableStyle,
169
204
  };
170
205
  }
206
+ // @internal
171
207
  static deserializeFromString(json) {
172
- const style = {
173
- renderingStyle: styleFromJSON(json.style.renderingStyle),
174
- size: json.style.size,
175
- fontWeight: json.style.fontWeight,
176
- fontVariant: json.style.fontVariant,
177
- fontFamily: json.style.fontFamily,
178
- };
208
+ if (typeof json === 'string') {
209
+ json = JSON.parse(json);
210
+ }
211
+ const style = textStyleFromJSON(json.style);
179
212
  const textObjects = json.textObjects.map((data) => {
180
213
  var _a;
181
214
  if (((_a = data.text) !== null && _a !== void 0 ? _a : null) !== null) {
@@ -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
  };
@@ -347,6 +347,7 @@ export default class Path {
347
347
  // @param onlyAbsCommands - True if we should avoid converting absolute coordinates to relative offsets -- such
348
348
  // conversions can lead to smaller output strings, but also take time.
349
349
  static toString(startPoint, parts, onlyAbsCommands) {
350
+ var _a;
350
351
  const result = [];
351
352
  let prevPoint;
352
353
  const addCommand = (command, ...points) => {
@@ -382,7 +383,7 @@ export default class Path {
382
383
  commandString = `${command.toLowerCase()}${relativeCommandParts.join(' ')}`;
383
384
  }
384
385
  // Don't add no-ops.
385
- if (commandString === 'l0,0') {
386
+ if (commandString === 'l0,0' || commandString === 'm0,0') {
386
387
  return;
387
388
  }
388
389
  result.push(commandString);
@@ -390,9 +391,15 @@ export default class Path {
390
391
  prevPoint = points[points.length - 1];
391
392
  }
392
393
  };
393
- addCommand('M', startPoint);
394
+ // Don't add two moveTos in a row (this can happen if
395
+ // the start point corresponds to a moveTo _and_ the first command is
396
+ // also a moveTo)
397
+ if (((_a = parts[0]) === null || _a === void 0 ? void 0 : _a.kind) !== PathCommandType.MoveTo) {
398
+ addCommand('M', startPoint);
399
+ }
394
400
  let exhaustivenessCheck;
395
- for (const part of parts) {
401
+ for (let i = 0; i < parts.length; i++) {
402
+ const part = parts[i];
396
403
  switch (part.kind) {
397
404
  case PathCommandType.MoveTo:
398
405
  addCommand('M', part.point);
@@ -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,8 +1,35 @@
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
- type IconType = SVGSVGElement | HTMLImageElement;
5
+ export type IconType = HTMLImageElement | SVGElement;
6
+ /**
7
+ * Provides icons that can be used in the toolbar, etc.
8
+ * Extend this class and override methods to customize icons.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * class CustomIconProvider extends jsdraw.IconProvider {
13
+ * // Use '☺' instead of the default dropdown symbol.
14
+ * public makeDropdownIcon() {
15
+ * const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
16
+ * icon.innerHTML = `
17
+ * <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
18
+ * `;
19
+ * icon.setAttribute('viewBox', '0 0 100 100');
20
+ * return icon;
21
+ * }
22
+ * }
23
+ *
24
+ * const icons = new CustomIconProvider();
25
+ * const editor = new jsdraw.Editor(document.body, {
26
+ * iconProvider: icons,
27
+ * });
28
+ *
29
+ * // Add a toolbar that uses these icons
30
+ * editor.addToolbar();
31
+ * ```
32
+ */
6
33
  export default class IconProvider {
7
34
  makeUndoIcon(): IconType;
8
35
  makeRedoIcon(mirror?: boolean): IconType;
@@ -24,10 +51,10 @@ export default class IconProvider {
24
51
  makePenIcon(strokeSize: number, color: string | Color4, rounded?: boolean): IconType;
25
52
  makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): IconType;
26
53
  makePipetteIcon(color?: Color4): IconType;
54
+ makeFormatSelectionIcon(): IconType;
27
55
  makeResizeViewportIcon(): IconType;
28
56
  makeDuplicateSelectionIcon(): IconType;
29
57
  makePasteIcon(): IconType;
30
58
  makeDeleteSelectionIcon(): IconType;
31
59
  makeSaveIcon(): IconType;
32
60
  }
33
- export {};
@@ -24,8 +24,33 @@ const checkerboardPatternDef = `
24
24
  </pattern>
25
25
  `;
26
26
  const checkerboardPatternRef = 'url(#checkerboard)';
27
- // Provides icons that can be used in the toolbar, etc.
28
- // Extend this class and override methods to customize icons.
27
+ /**
28
+ * Provides icons that can be used in the toolbar, etc.
29
+ * Extend this class and override methods to customize icons.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * class CustomIconProvider extends jsdraw.IconProvider {
34
+ * // Use '☺' instead of the default dropdown symbol.
35
+ * public makeDropdownIcon() {
36
+ * const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
37
+ * icon.innerHTML = `
38
+ * <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
39
+ * `;
40
+ * icon.setAttribute('viewBox', '0 0 100 100');
41
+ * return icon;
42
+ * }
43
+ * }
44
+ *
45
+ * const icons = new CustomIconProvider();
46
+ * const editor = new jsdraw.Editor(document.body, {
47
+ * iconProvider: icons,
48
+ * });
49
+ *
50
+ * // Add a toolbar that uses these icons
51
+ * editor.addToolbar();
52
+ * ```
53
+ */
29
54
  export default class IconProvider {
30
55
  makeUndoIcon() {
31
56
  return this.makeRedoIcon(true);
@@ -512,6 +537,16 @@ export default class IconProvider {
512
537
  icon.setAttribute('viewBox', '0 0 100 100');
513
538
  return icon;
514
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
+ }
515
550
  makeResizeViewportIcon() {
516
551
  return this.makeIconFromPath(`
517
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
+ }
@@ -23,6 +23,12 @@ export default class Selection {
23
23
  getTransform(): Mat33;
24
24
  get preTransformRegion(): Rect2;
25
25
  get region(): Rect2;
26
+ /**
27
+ * Computes and returns the bounding box of the selection without
28
+ * any additional padding. Computes directly from the elements that are selected.
29
+ * @internal
30
+ */
31
+ computeTightBoundingBox(): Rect2;
26
32
  get regionRotation(): number;
27
33
  get preTransformedScreenRegion(): Rect2;
28
34
  get preTransformedScreenRegionRotation(): number;