js-draw 0.22.0 → 0.23.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 (67) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/bundle.js +3 -3
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/src/Pointer.d.ts +3 -1
  5. package/dist/cjs/src/Pointer.js +27 -2
  6. package/dist/cjs/src/commands/Duplicate.d.ts +24 -0
  7. package/dist/cjs/src/commands/Duplicate.js +26 -0
  8. package/dist/cjs/src/commands/Erase.d.ts +20 -0
  9. package/dist/cjs/src/commands/Erase.js +20 -0
  10. package/dist/cjs/src/commands/invertCommand.js +6 -0
  11. package/dist/cjs/src/commands/uniteCommands.js +14 -13
  12. package/dist/cjs/src/components/BackgroundComponent.js +9 -2
  13. package/dist/cjs/src/components/ImageComponent.d.ts +6 -6
  14. package/dist/cjs/src/components/ImageComponent.js +17 -12
  15. package/dist/cjs/src/components/lib.d.ts +1 -1
  16. package/dist/cjs/src/components/lib.js +2 -2
  17. package/dist/cjs/src/components/localization.d.ts +1 -0
  18. package/dist/cjs/src/components/localization.js +1 -0
  19. package/dist/cjs/src/math/Vec2.d.ts +20 -0
  20. package/dist/cjs/src/math/Vec2.js +20 -0
  21. package/dist/cjs/src/rendering/renderers/AbstractRenderer.d.ts +17 -0
  22. package/dist/cjs/src/rendering/renderers/AbstractRenderer.js +21 -3
  23. package/dist/cjs/src/rendering/renderers/CanvasRenderer.js +12 -7
  24. package/dist/cjs/src/toolbar/localization.d.ts +1 -0
  25. package/dist/cjs/src/toolbar/localization.js +1 -0
  26. package/dist/cjs/src/toolbar/makeColorInput.js +8 -0
  27. package/dist/cjs/src/toolbar/widgets/BaseWidget.d.ts +1 -0
  28. package/dist/cjs/src/toolbar/widgets/BaseWidget.js +29 -6
  29. package/dist/cjs/src/tools/Pen.d.ts +4 -0
  30. package/dist/cjs/src/tools/Pen.js +24 -1
  31. package/dist/cjs/src/tools/SelectionTool/SelectionTool.d.ts +1 -0
  32. package/dist/cjs/src/tools/SelectionTool/SelectionTool.js +8 -0
  33. package/dist/cjs/src/util/waitForAll.d.ts +6 -0
  34. package/dist/cjs/src/util/waitForAll.js +17 -0
  35. package/dist/mjs/src/Pointer.d.ts +3 -1
  36. package/dist/mjs/src/Pointer.mjs +27 -2
  37. package/dist/mjs/src/commands/Duplicate.d.ts +24 -0
  38. package/dist/mjs/src/commands/Duplicate.mjs +26 -0
  39. package/dist/mjs/src/commands/Erase.d.ts +20 -0
  40. package/dist/mjs/src/commands/Erase.mjs +20 -0
  41. package/dist/mjs/src/commands/invertCommand.mjs +6 -0
  42. package/dist/mjs/src/commands/uniteCommands.mjs +14 -13
  43. package/dist/mjs/src/components/BackgroundComponent.mjs +9 -2
  44. package/dist/mjs/src/components/ImageComponent.d.ts +6 -6
  45. package/dist/mjs/src/components/ImageComponent.mjs +17 -12
  46. package/dist/mjs/src/components/lib.d.ts +1 -1
  47. package/dist/mjs/src/components/lib.mjs +3 -1
  48. package/dist/mjs/src/components/localization.d.ts +1 -0
  49. package/dist/mjs/src/components/localization.mjs +1 -0
  50. package/dist/mjs/src/math/Vec2.d.ts +20 -0
  51. package/dist/mjs/src/math/Vec2.mjs +20 -0
  52. package/dist/mjs/src/rendering/renderers/AbstractRenderer.d.ts +17 -0
  53. package/dist/mjs/src/rendering/renderers/AbstractRenderer.mjs +21 -3
  54. package/dist/mjs/src/rendering/renderers/CanvasRenderer.mjs +12 -7
  55. package/dist/mjs/src/toolbar/localization.d.ts +1 -0
  56. package/dist/mjs/src/toolbar/localization.mjs +1 -0
  57. package/dist/mjs/src/toolbar/makeColorInput.mjs +8 -0
  58. package/dist/mjs/src/toolbar/widgets/BaseWidget.d.ts +1 -0
  59. package/dist/mjs/src/toolbar/widgets/BaseWidget.mjs +29 -6
  60. package/dist/mjs/src/tools/Pen.d.ts +4 -0
  61. package/dist/mjs/src/tools/Pen.mjs +24 -1
  62. package/dist/mjs/src/tools/SelectionTool/SelectionTool.d.ts +1 -0
  63. package/dist/mjs/src/tools/SelectionTool/SelectionTool.mjs +8 -0
  64. package/dist/mjs/src/util/waitForAll.d.ts +6 -0
  65. package/dist/mjs/src/util/waitForAll.mjs +15 -0
  66. package/package.json +12 -12
  67. package/src/toolbar/toolbar.css +35 -1
@@ -1,3 +1,4 @@
1
+ import waitForAll from '../util/waitForAll.mjs';
1
2
  import Command from './Command.mjs';
2
3
  import SerializableCommand from './SerializableCommand.mjs';
3
4
  class NonSerializableUnion extends Command {
@@ -6,21 +7,10 @@ class NonSerializableUnion extends Command {
6
7
  this.commands = commands;
7
8
  this.applyChunkSize = applyChunkSize;
8
9
  }
9
- static waitForAll(commands) {
10
- // If any are Promises...
11
- if (commands.some(command => command && command['then'])) {
12
- console.log('waiting...');
13
- // Wait for all commands to finish.
14
- return Promise.all(commands)
15
- // Ensure we return a Promise<void> and not a Promise<void[]>
16
- .then(() => { });
17
- }
18
- return;
19
- }
20
10
  apply(editor) {
21
11
  if (this.applyChunkSize === undefined) {
22
12
  const results = this.commands.map(cmd => cmd.apply(editor));
23
- return NonSerializableUnion.waitForAll(results);
13
+ return waitForAll(results);
24
14
  }
25
15
  else {
26
16
  return editor.asyncApplyCommands(this.commands, this.applyChunkSize);
@@ -31,12 +21,15 @@ class NonSerializableUnion extends Command {
31
21
  commands.reverse();
32
22
  if (this.applyChunkSize === undefined) {
33
23
  const results = commands.map(cmd => cmd.unapply(editor));
34
- return NonSerializableUnion.waitForAll(results);
24
+ return waitForAll(results);
35
25
  }
36
26
  else {
37
27
  return editor.asyncUnapplyCommands(commands, this.applyChunkSize, false);
38
28
  }
39
29
  }
30
+ onDrop(editor) {
31
+ this.commands.forEach(command => command.onDrop(editor));
32
+ }
40
33
  description(editor, localizationTable) {
41
34
  const descriptions = [];
42
35
  let lastDescription = null;
@@ -68,17 +61,25 @@ class SerializableUnion extends SerializableCommand {
68
61
  this.nonserializableCommand = new NonSerializableUnion(commands, applyChunkSize);
69
62
  }
70
63
  serializeToJSON() {
64
+ if (this.serializedData) {
65
+ return this.serializedData;
66
+ }
71
67
  return {
72
68
  applyChunkSize: this.applyChunkSize,
73
69
  data: this.commands.map(command => command.serialize()),
74
70
  };
75
71
  }
76
72
  apply(editor) {
73
+ // Cache this' serialized form -- applying this may change how commands serialize.
74
+ this.serializedData = this.serializeToJSON();
77
75
  return this.nonserializableCommand.apply(editor);
78
76
  }
79
77
  unapply(editor) {
80
78
  return this.nonserializableCommand.unapply(editor);
81
79
  }
80
+ onDrop(editor) {
81
+ this.nonserializableCommand.onDrop(editor);
82
+ }
82
83
  description(editor, localizationTable) {
83
84
  return this.nonserializableCommand.description(editor, localizationTable);
84
85
  }
@@ -167,7 +167,7 @@ export default class BackgroundComponent extends AbstractComponent {
167
167
  if (this.backgroundType === BackgroundType.None) {
168
168
  return;
169
169
  }
170
- const clip = true;
170
+ const clip = this.backgroundType === BackgroundType.Grid;
171
171
  canvas.startObject(this.contentBBox, clip);
172
172
  if (this.backgroundType === BackgroundType.SolidColor || this.backgroundType === BackgroundType.Grid) {
173
173
  // If the rectangle for this region contains the visible rect,
@@ -235,9 +235,16 @@ export default class BackgroundComponent extends AbstractComponent {
235
235
  if (this.backgroundType === BackgroundType.SolidColor) {
236
236
  return localizationTable.filledBackgroundWithColor(this.mainColor.toString());
237
237
  }
238
- else {
238
+ else if (this.backgroundType === BackgroundType.None) {
239
239
  return localizationTable.emptyBackground;
240
240
  }
241
+ else if (this.backgroundType === BackgroundType.Grid) {
242
+ return localizationTable.gridBackground;
243
+ }
244
+ else {
245
+ const exhaustivenessCheck = this.backgroundType;
246
+ return exhaustivenessCheck;
247
+ }
241
248
  }
242
249
  createClone() {
243
250
  return new BackgroundComponent(this.backgroundType, this.mainColor);
@@ -14,6 +14,12 @@ export default class ImageComponent extends AbstractComponent {
14
14
  render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
15
15
  getProportionalRenderingTime(): number;
16
16
  intersects(lineSegment: LineSegment2): boolean;
17
+ protected applyTransformation(affineTransfm: Mat33): void;
18
+ description(localizationTable: ImageComponentLocalization): string;
19
+ getAltText(): string | undefined;
20
+ getURL(): string;
21
+ getTransformation(): Mat33;
22
+ protected createClone(): AbstractComponent;
17
23
  protected serializeToJSON(): {
18
24
  src: string;
19
25
  label: string | undefined;
@@ -21,11 +27,5 @@ export default class ImageComponent extends AbstractComponent {
21
27
  height: number;
22
28
  transform: Mat33Array;
23
29
  };
24
- protected applyTransformation(affineTransfm: Mat33): void;
25
- description(localizationTable: ImageComponentLocalization): string;
26
- getAltText(): string | undefined;
27
- getURL(): string;
28
- getTransformation(): Mat33;
29
- protected createClone(): AbstractComponent;
30
30
  static deserializeFromJSON(data: any): ImageComponent;
31
31
  }
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import Mat33 from '../math/Mat33.mjs';
11
11
  import Rect2 from '../math/Rect2.mjs';
12
+ import { assertIsNumber, assertIsNumberArray } from '../util/assertions.mjs';
12
13
  import AbstractComponent from './AbstractComponent.mjs';
13
14
  // Represents a raster image.
14
15
  export default class ImageComponent extends AbstractComponent {
@@ -98,16 +99,6 @@ export default class ImageComponent extends AbstractComponent {
98
99
  }
99
100
  return false;
100
101
  }
101
- serializeToJSON() {
102
- return {
103
- src: this.image.base64Url,
104
- label: this.image.label,
105
- // Store the width and height for bounding box computations while the image is loading.
106
- width: this.image.image.width,
107
- height: this.image.image.height,
108
- transform: this.image.transform.toArray(),
109
- };
110
- }
111
102
  applyTransformation(affineTransfm) {
112
103
  this.image.transform = affineTransfm.rightMul(this.image.transform);
113
104
  this.recomputeBBox();
@@ -127,19 +118,33 @@ export default class ImageComponent extends AbstractComponent {
127
118
  createClone() {
128
119
  return new ImageComponent(Object.assign({}, this.image));
129
120
  }
121
+ serializeToJSON() {
122
+ return {
123
+ src: this.image.base64Url,
124
+ label: this.image.label,
125
+ // Store the width and height for bounding box computations while the image is loading.
126
+ width: this.image.image.width,
127
+ height: this.image.image.height,
128
+ transform: this.image.transform.toArray(),
129
+ };
130
+ }
130
131
  static deserializeFromJSON(data) {
131
132
  if (!(typeof data.src === 'string')) {
132
133
  throw new Error(`${data} has invalid format! Expected src property.`);
133
134
  }
135
+ assertIsNumberArray(data.transform);
136
+ assertIsNumber(data.width);
137
+ assertIsNumber(data.height);
134
138
  const image = new Image();
135
139
  image.src = data.src;
136
140
  image.width = data.width;
137
141
  image.height = data.height;
142
+ const transform = new Mat33(...data.transform);
138
143
  return new ImageComponent({
139
144
  image: image,
140
- base64Url: image.src,
145
+ base64Url: data.src,
141
146
  label: data.label,
142
- transform: new Mat33(...data.transform),
147
+ transform,
143
148
  });
144
149
  }
145
150
  }
@@ -10,4 +10,4 @@ import ImageComponent from './ImageComponent';
10
10
  import RestyleableComponent from './RestylableComponent';
11
11
  import { createRestyleComponentCommand, isRestylableComponent, ComponentStyle as RestyleableComponentStyle } from './RestylableComponent';
12
12
  import BackgroundComponent from './BackgroundComponent';
13
- export { Stroke, TextComponent as Text, RestyleableComponent, createRestyleComponentCommand, isRestylableComponent, RestyleableComponentStyle, TextComponent, Stroke as StrokeComponent, BackgroundComponent, ImageComponent, };
13
+ export { Stroke, RestyleableComponent, createRestyleComponentCommand, isRestylableComponent, RestyleableComponentStyle, TextComponent, TextComponent as Text, Stroke as StrokeComponent, BackgroundComponent, ImageComponent, };
@@ -9,4 +9,6 @@ import TextComponent from './TextComponent.mjs';
9
9
  import ImageComponent from './ImageComponent.mjs';
10
10
  import { createRestyleComponentCommand, isRestylableComponent } from './RestylableComponent.mjs';
11
11
  import BackgroundComponent from './BackgroundComponent.mjs';
12
- export { Stroke, TextComponent as Text, createRestyleComponentCommand, isRestylableComponent, TextComponent, Stroke as StrokeComponent, BackgroundComponent, ImageComponent, };
12
+ export { Stroke, createRestyleComponentCommand, isRestylableComponent, TextComponent,
13
+ // @deprecated
14
+ TextComponent as Text, Stroke as StrokeComponent, BackgroundComponent, ImageComponent, };
@@ -5,6 +5,7 @@ export interface ImageComponentLocalization {
5
5
  stroke: string;
6
6
  svgObject: string;
7
7
  emptyBackground: string;
8
+ gridBackground: string;
8
9
  filledBackgroundWithColor: (color: string) => string;
9
10
  restyledElement: (elementDescription: string) => string;
10
11
  }
@@ -3,6 +3,7 @@ export const defaultComponentLocalization = {
3
3
  stroke: 'Stroke',
4
4
  svgObject: 'SVG Object',
5
5
  emptyBackground: 'Empty background',
6
+ gridBackground: 'Grid background',
6
7
  filledBackgroundWithColor: (color) => `Filled background (${color})`,
7
8
  text: (text) => `Text object: ${text}`,
8
9
  imageNode: (description) => `Image: ${description}`,
@@ -1,12 +1,32 @@
1
1
  import Vec3 from './Vec3';
2
2
  export declare namespace Vec2 {
3
+ /**
4
+ * Creates a `Vec2` from an x and y coordinate.
5
+ *
6
+ * For example,
7
+ * ```ts
8
+ * const v = Vec2.of(3, 4); // x=3, y=4.
9
+ * ```
10
+ */
3
11
  const of: (x: number, y: number) => Vec2;
12
+ /**
13
+ * Creates a `Vec2` from an object containing x and y coordinates.
14
+ *
15
+ * For example,
16
+ * ```ts
17
+ * const v1 = Vec2.ofXY({ x: 3, y: 4.5 });
18
+ * const v2 = Vec2.ofXY({ x: -123.4, y: 1 });
19
+ * ```
20
+ */
4
21
  const ofXY: ({ x, y }: {
5
22
  x: number;
6
23
  y: number;
7
24
  }) => Vec2;
25
+ /** A vector of length 1 in the X direction (→). */
8
26
  const unitX: Vec3;
27
+ /** A vector of length 1 in the Y direction (↑). */
9
28
  const unitY: Vec3;
29
+ /** The zero vector: A vector with x=0, y=0. */
10
30
  const zero: Vec3;
11
31
  }
12
32
  export type Point2 = Vec3;
@@ -1,13 +1,33 @@
1
1
  import Vec3 from './Vec3.mjs';
2
2
  export var Vec2;
3
3
  (function (Vec2) {
4
+ /**
5
+ * Creates a `Vec2` from an x and y coordinate.
6
+ *
7
+ * For example,
8
+ * ```ts
9
+ * const v = Vec2.of(3, 4); // x=3, y=4.
10
+ * ```
11
+ */
4
12
  Vec2.of = (x, y) => {
5
13
  return Vec3.of(x, y, 0);
6
14
  };
15
+ /**
16
+ * Creates a `Vec2` from an object containing x and y coordinates.
17
+ *
18
+ * For example,
19
+ * ```ts
20
+ * const v1 = Vec2.ofXY({ x: 3, y: 4.5 });
21
+ * const v2 = Vec2.ofXY({ x: -123.4, y: 1 });
22
+ * ```
23
+ */
7
24
  Vec2.ofXY = ({ x, y }) => {
8
25
  return Vec3.of(x, y, 0);
9
26
  };
27
+ /** A vector of length 1 in the X direction (→). */
10
28
  Vec2.unitX = Vec2.of(1, 0);
29
+ /** A vector of length 1 in the Y direction (↑). */
11
30
  Vec2.unitY = Vec2.of(0, 1);
31
+ /** The zero vector: A vector with x=0, y=0. */
12
32
  Vec2.zero = Vec2.of(0, 0);
13
33
  })(Vec2 || (Vec2 = {}));
@@ -19,6 +19,11 @@ export interface RenderableImage {
19
19
  base64Url: string;
20
20
  label?: string;
21
21
  }
22
+ /**
23
+ * Abstract base class for renderers.
24
+ *
25
+ * @see {@link EditorImage.render}
26
+ */
22
27
  export default abstract class AbstractRenderer {
23
28
  private viewport;
24
29
  private selfTransform;
@@ -40,9 +45,21 @@ export default abstract class AbstractRenderer {
40
45
  protected objectLevel: number;
41
46
  private currentPaths;
42
47
  private flushPath;
48
+ /**
49
+ * Draws a styled path. If within an object started by {@link startObject},
50
+ * the resultant path may not be visible until {@link endObject} is called.
51
+ */
43
52
  drawPath(path: RenderablePathSpec): void;
44
53
  drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void;
54
+ /** Draws a filled rectangle. */
45
55
  fillRect(rect: Rect2, fill: Color4): void;
56
+ /**
57
+ * This should be called whenever a new object is being drawn.
58
+ *
59
+ * @param _boundingBox The bounding box of the object to be drawn.
60
+ * @param _clip Whether content outside `_boundingBox` should be drawn. Renderers
61
+ * that override this method are not required to support `_clip`.
62
+ */
46
63
  startObject(_boundingBox: Rect2, _clip?: boolean): void;
47
64
  /**
48
65
  * Notes the end of an object.
@@ -1,6 +1,11 @@
1
1
  import Path, { PathCommandType } from '../../math/Path.mjs';
2
2
  import { Vec2 } from '../../math/Vec2.mjs';
3
3
  import { stylesEqual } from '../RenderingStyle.mjs';
4
+ /**
5
+ * Abstract base class for renderers.
6
+ *
7
+ * @see {@link EditorImage.render}
8
+ */
4
9
  export default class AbstractRenderer {
5
10
  constructor(viewport) {
6
11
  this.viewport = viewport;
@@ -49,7 +54,12 @@ export default class AbstractRenderer {
49
54
  if (lastStyle) {
50
55
  this.endPath(lastStyle);
51
56
  }
57
+ this.currentPaths = [];
52
58
  }
59
+ /**
60
+ * Draws a styled path. If within an object started by {@link startObject},
61
+ * the resultant path may not be visible until {@link endObject} is called.
62
+ */
53
63
  drawPath(path) {
54
64
  // If we're being called outside of an object,
55
65
  // we can't delay rendering
@@ -70,14 +80,22 @@ export default class AbstractRenderer {
70
80
  const path = Path.fromRect(rect, lineWidth);
71
81
  this.drawPath(path.toRenderable(lineFill));
72
82
  }
73
- // Fills a rectangle.
83
+ /** Draws a filled rectangle. */
74
84
  fillRect(rect, fill) {
75
85
  const path = Path.fromRect(rect);
76
86
  this.drawPath(path.toRenderable({ fill }));
77
87
  }
78
- // Note the start of an object with the given bounding box.
79
- // Renderers are not required to support [clip]
88
+ /**
89
+ * This should be called whenever a new object is being drawn.
90
+ *
91
+ * @param _boundingBox The bounding box of the object to be drawn.
92
+ * @param _clip Whether content outside `_boundingBox` should be drawn. Renderers
93
+ * that override this method are not required to support `_clip`.
94
+ */
80
95
  startObject(_boundingBox, _clip) {
96
+ if (this.objectLevel > 0) {
97
+ this.flushPath();
98
+ }
81
99
  this.currentPaths = [];
82
100
  this.objectLevel++;
83
101
  }
@@ -175,14 +175,19 @@ export default class CanvasRenderer extends AbstractRenderer {
175
175
  super.startObject(boundingBox);
176
176
  this.currentObjectBBox = boundingBox;
177
177
  if (!this.ignoringObject && clip) {
178
- this.clipLevels.push(this.objectLevel);
179
- this.ctx.save();
180
- this.ctx.beginPath();
181
- for (const corner of boundingBox.corners) {
182
- const screenCorner = this.canvasToScreen(corner);
183
- this.ctx.lineTo(screenCorner.x, screenCorner.y);
178
+ // Don't clip if it would only remove content already trimmed by
179
+ // the edge of the screen.
180
+ const clippedIsOutsideScreen = boundingBox.containsRect(this.getViewport().visibleRect);
181
+ if (!clippedIsOutsideScreen) {
182
+ this.clipLevels.push(this.objectLevel);
183
+ this.ctx.save();
184
+ this.ctx.beginPath();
185
+ for (const corner of boundingBox.corners) {
186
+ const screenCorner = this.canvasToScreen(corner);
187
+ this.ctx.lineTo(screenCorner.x, screenCorner.y);
188
+ }
189
+ this.ctx.clip();
184
190
  }
185
- this.ctx.clip();
186
191
  }
187
192
  }
188
193
  endObject() {
@@ -26,6 +26,7 @@ export interface ToolbarLocalization {
26
26
  duplicateSelection: string;
27
27
  pickColorFromScreen: string;
28
28
  clickToPickColorAnnouncement: string;
29
+ colorSelectionCanceledAnnouncement: string;
29
30
  reformatSelection: string;
30
31
  undo: string;
31
32
  redo: string;
@@ -23,6 +23,7 @@ export const defaultToolbarLocalization = {
23
23
  selectPenType: 'Pen type: ',
24
24
  pickColorFromScreen: 'Pick color from screen',
25
25
  clickToPickColorAnnouncement: 'Click on the screen to pick a color',
26
+ colorSelectionCanceledAnnouncement: 'Color selection canceled',
26
27
  selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
27
28
  documentProperties: 'Page',
28
29
  backgroundColor: 'Background Color: ',
@@ -42,6 +42,11 @@ export const makeColorInput = (editor, onColorChange) => {
42
42
  open: true,
43
43
  });
44
44
  pipetteController.cancel();
45
+ // Focus the Coloris color picker, if it exists.
46
+ // Don't focus the text input within the color picker, however,
47
+ // as this displays a keyboard on mobile devices.
48
+ const colorPickerElem = document.querySelector('#clr-picker #clr-hue-slider');
49
+ colorPickerElem === null || colorPickerElem === void 0 ? void 0 : colorPickerElem.focus();
45
50
  });
46
51
  colorInput.addEventListener('close', () => {
47
52
  editor.notifier.dispatch(EditorEventType.ColorPickerToggled, {
@@ -49,6 +54,8 @@ export const makeColorInput = (editor, onColorChange) => {
49
54
  open: false,
50
55
  });
51
56
  onInputEnd();
57
+ // Restore focus to the input that opened the color picker
58
+ colorInput.focus();
52
59
  });
53
60
  const setColorInputValue = (color) => {
54
61
  if (typeof color === 'object') {
@@ -94,6 +101,7 @@ const addPipetteTool = (editor, container, onColorChange) => {
94
101
  // If already picking, cancel it.
95
102
  if (pipetteButton.classList.contains('active')) {
96
103
  endColorSelectMode();
104
+ editor.announceForAccessibility(editor.localization.colorSelectionCanceledAnnouncement);
97
105
  return;
98
106
  }
99
107
  pipetteTool === null || pipetteTool === void 0 ? void 0 : pipetteTool.setColorListener(pipetteColorPreview, pipetteColorSelect);
@@ -46,6 +46,7 @@ export default abstract class BaseWidget {
46
46
  protected updateIcon(): void;
47
47
  setDisabled(disabled: boolean): void;
48
48
  setSelected(selected: boolean): void;
49
+ private hideDropdownTimeout;
49
50
  protected setDropdownVisible(visible: boolean): void;
50
51
  canBeInOverflowMenu(): boolean;
51
52
  getButtonWidth(): number;
@@ -23,6 +23,7 @@ export default class BaseWidget {
23
23
  this.subWidgets = {};
24
24
  this.toplevel = true;
25
25
  this.toolbarWidgetToggleListener = null;
26
+ this.hideDropdownTimeout = null;
26
27
  this.localizationTable = localizationTable !== null && localizationTable !== void 0 ? localizationTable : editor.localization;
27
28
  this.icon = null;
28
29
  this.container = document.createElement('div');
@@ -211,13 +212,18 @@ export default class BaseWidget {
211
212
  if (currentlySelected === selected) {
212
213
  return;
213
214
  }
215
+ // Ensure that accessibility tools check and read the value of
216
+ // aria-checked.
217
+ // TODO: Ensure that 'role' is set to 'switch' by default for selectable
218
+ // buttons.
219
+ this.button.setAttribute('role', 'switch');
214
220
  if (selected) {
215
221
  this.container.classList.add('selected');
216
- this.button.ariaSelected = 'true';
222
+ this.button.setAttribute('aria-checked', 'true');
217
223
  }
218
224
  else {
219
225
  this.container.classList.remove('selected');
220
- this.button.ariaSelected = 'false';
226
+ this.button.setAttribute('aria-checked', 'false');
221
227
  }
222
228
  }
223
229
  setDropdownVisible(visible) {
@@ -225,6 +231,13 @@ export default class BaseWidget {
225
231
  if (currentlyVisible === visible) {
226
232
  return;
227
233
  }
234
+ // If waiting to hide the dropdown, cancel it.
235
+ if (this.hideDropdownTimeout) {
236
+ clearTimeout(this.hideDropdownTimeout);
237
+ this.hideDropdownTimeout = null;
238
+ this.dropdownContainer.classList.remove('hiding');
239
+ }
240
+ const animationDuration = 150; // ms
228
241
  if (visible) {
229
242
  this.dropdownContainer.classList.remove('hidden');
230
243
  this.container.classList.add('dropdownVisible');
@@ -235,10 +248,20 @@ export default class BaseWidget {
235
248
  });
236
249
  }
237
250
  else {
238
- this.dropdownContainer.classList.add('hidden');
239
251
  this.container.classList.remove('dropdownVisible');
240
252
  this.editor.announceForAccessibility(this.localizationTable.dropdownHidden(this.getTitle()));
253
+ this.dropdownContainer.classList.add('hiding');
254
+ // Hide the dropdown *slightly* before the animation finishes. This
255
+ // prevents flickering in some browsers.
256
+ const hideDelay = animationDuration * 0.95;
257
+ this.hideDropdownTimeout = setTimeout(() => {
258
+ this.dropdownContainer.classList.add('hidden');
259
+ this.dropdownContainer.classList.remove('hiding');
260
+ }, hideDelay);
241
261
  }
262
+ // Animate
263
+ const animationName = `var(--dropdown-${visible ? 'show' : 'hide'}-animation)`;
264
+ this.dropdownContainer.style.animation = `${animationDuration}ms ease ${animationName}`;
242
265
  this.repositionDropdown();
243
266
  }
244
267
  canBeInOverflowMenu() {
@@ -257,11 +280,11 @@ export default class BaseWidget {
257
280
  const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
258
281
  const screenWidth = document.body.clientWidth;
259
282
  if (dropdownBBox.left > screenWidth / 2) {
260
- this.dropdownContainer.style.marginLeft = this.button.clientWidth + 'px';
261
- this.dropdownContainer.style.transform = 'translate(-100%, 0)';
283
+ // Use .translate so as not to conflict with CSS animating the
284
+ // transform property.
285
+ this.dropdownContainer.style.translate = `calc(${this.button.clientWidth + 'px'} - 100%) 0`;
262
286
  }
263
287
  else {
264
- this.dropdownContainer.style.marginLeft = '';
265
288
  this.dropdownContainer.style.transform = '';
266
289
  }
267
290
  }
@@ -14,9 +14,12 @@ export default class Pen extends BaseTool {
14
14
  private builderFactory;
15
15
  protected builder: ComponentBuilder | null;
16
16
  private lastPoint;
17
+ private startPoint;
17
18
  private ctrlKeyPressed;
19
+ private shiftKeyPressed;
18
20
  constructor(editor: Editor, description: string, style: PenStyle, builderFactory?: ComponentBuilderFactory);
19
21
  private getPressureMultiplier;
22
+ private xyAxesSnap;
20
23
  protected toStrokePoint(pointer: Pointer): StrokeDataPoint;
21
24
  protected previewStroke(): void;
22
25
  protected addPointToStroke(point: StrokeDataPoint): void;
@@ -34,6 +37,7 @@ export default class Pen extends BaseTool {
34
37
  getStrokeFactory(): ComponentBuilderFactory;
35
38
  setEnabled(enabled: boolean): void;
36
39
  private isSnappingToGrid;
40
+ private isAngleLocked;
37
41
  onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean;
38
42
  onKeyUp({ key }: KeyUpEvent): boolean;
39
43
  }
@@ -11,14 +11,26 @@ export default class Pen extends BaseTool {
11
11
  this.builderFactory = builderFactory;
12
12
  this.builder = null;
13
13
  this.lastPoint = null;
14
+ this.startPoint = null;
14
15
  this.ctrlKeyPressed = false;
16
+ this.shiftKeyPressed = false;
15
17
  }
16
18
  getPressureMultiplier() {
17
19
  return 1 / this.editor.viewport.getScaleFactor() * this.style.thickness;
18
20
  }
21
+ // Snap the given pointer to the nearer of the x/y axes.
22
+ xyAxesSnap(pointer) {
23
+ if (!this.startPoint) {
24
+ return pointer;
25
+ }
26
+ return pointer.lockedToXYAxes(this.startPoint.pos, this.editor.viewport);
27
+ }
19
28
  // Converts a `pointer` to a `StrokeDataPoint`.
20
29
  toStrokePoint(pointer) {
21
30
  var _a;
31
+ if (this.isAngleLocked() && this.lastPoint) {
32
+ pointer = this.xyAxesSnap(pointer);
33
+ }
22
34
  if (this.isSnappingToGrid()) {
23
35
  pointer = pointer.snappedToGrid(this.editor.viewport);
24
36
  }
@@ -64,7 +76,8 @@ export default class Pen extends BaseTool {
64
76
  }
65
77
  }
66
78
  if ((allPointers.length === 1 && !isEraser) || anyDeviceIsStylus) {
67
- this.builder = this.builderFactory(this.toStrokePoint(current), this.editor.viewport);
79
+ this.startPoint = this.toStrokePoint(current);
80
+ this.builder = this.builderFactory(this.startPoint, this.editor.viewport);
68
81
  return true;
69
82
  }
70
83
  return false;
@@ -104,6 +117,7 @@ export default class Pen extends BaseTool {
104
117
  }
105
118
  }
106
119
  this.builder = null;
120
+ this.lastPoint = null;
107
121
  this.editor.clearWetInk();
108
122
  }
109
123
  noteUpdated() {
@@ -138,6 +152,7 @@ export default class Pen extends BaseTool {
138
152
  this.ctrlKeyPressed = false;
139
153
  }
140
154
  isSnappingToGrid() { return this.ctrlKeyPressed; }
155
+ isAngleLocked() { return this.shiftKeyPressed; }
141
156
  onKeyPress({ key, ctrlKey }) {
142
157
  key = key.toLowerCase();
143
158
  let newThickness;
@@ -156,6 +171,10 @@ export default class Pen extends BaseTool {
156
171
  this.ctrlKeyPressed = true;
157
172
  return true;
158
173
  }
174
+ if (key === 'shift') {
175
+ this.shiftKeyPressed = true;
176
+ return true;
177
+ }
159
178
  // Ctrl+Z: End the stroke so that it can be undone/redone.
160
179
  if (key === 'z' && ctrlKey && this.builder) {
161
180
  this.finalizeStroke();
@@ -168,6 +187,10 @@ export default class Pen extends BaseTool {
168
187
  this.ctrlKeyPressed = false;
169
188
  return true;
170
189
  }
190
+ if (key === 'shift') {
191
+ this.shiftKeyPressed = false;
192
+ return true;
193
+ }
171
194
  return false;
172
195
  }
173
196
  }
@@ -10,6 +10,7 @@ export default class SelectionTool extends BaseTool {
10
10
  private prevSelectionBox;
11
11
  private selectionBox;
12
12
  private lastEvtTarget;
13
+ private startPoint;
13
14
  private expandingSelectionBox;
14
15
  private shiftKeyPressed;
15
16
  private ctrlKeyPressed;