js-draw 0.1.7 → 0.1.10

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 (68) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +15 -3
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +1 -0
  5. package/dist/src/Editor.js +32 -15
  6. package/dist/src/UndoRedoHistory.js +3 -0
  7. package/dist/src/bundle/bundled.d.ts +2 -1
  8. package/dist/src/bundle/bundled.js +2 -1
  9. package/dist/src/components/builders/RectangleBuilder.d.ts +3 -1
  10. package/dist/src/components/builders/RectangleBuilder.js +17 -8
  11. package/dist/src/geometry/LineSegment2.d.ts +1 -0
  12. package/dist/src/geometry/LineSegment2.js +16 -0
  13. package/dist/src/geometry/Rect2.d.ts +1 -0
  14. package/dist/src/geometry/Rect2.js +16 -0
  15. package/dist/src/localizations/en.d.ts +3 -0
  16. package/dist/src/localizations/en.js +4 -0
  17. package/dist/src/localizations/es.d.ts +3 -0
  18. package/dist/src/localizations/es.js +18 -0
  19. package/dist/src/localizations/getLocalizationTable.d.ts +3 -0
  20. package/dist/src/localizations/getLocalizationTable.js +43 -0
  21. package/dist/src/rendering/caching/RenderingCacheNode.js +5 -1
  22. package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
  23. package/dist/src/toolbar/HTMLToolbar.js +10 -8
  24. package/dist/src/toolbar/icons.js +13 -13
  25. package/dist/src/toolbar/localization.d.ts +1 -0
  26. package/dist/src/toolbar/localization.js +1 -0
  27. package/dist/src/toolbar/makeColorInput.js +22 -8
  28. package/dist/src/toolbar/widgets/BaseWidget.js +29 -0
  29. package/dist/src/tools/BaseTool.d.ts +2 -1
  30. package/dist/src/tools/BaseTool.js +3 -0
  31. package/dist/src/tools/PanZoom.d.ts +2 -1
  32. package/dist/src/tools/PanZoom.js +4 -0
  33. package/dist/src/tools/SelectionTool.d.ts +9 -2
  34. package/dist/src/tools/SelectionTool.js +131 -19
  35. package/dist/src/tools/ToolController.js +6 -2
  36. package/dist/src/tools/localization.d.ts +1 -0
  37. package/dist/src/tools/localization.js +1 -0
  38. package/dist/src/types.d.ts +8 -2
  39. package/dist/src/types.js +1 -0
  40. package/package.json +9 -1
  41. package/src/Editor.ts +36 -14
  42. package/src/EditorImage.test.ts +1 -1
  43. package/src/UndoRedoHistory.ts +4 -0
  44. package/src/bundle/bundled.ts +2 -1
  45. package/src/components/builders/RectangleBuilder.ts +23 -8
  46. package/src/geometry/LineSegment2.test.ts +15 -0
  47. package/src/geometry/LineSegment2.ts +20 -0
  48. package/src/geometry/Rect2.test.ts +20 -7
  49. package/src/geometry/Rect2.ts +19 -1
  50. package/src/localizations/en.ts +8 -0
  51. package/src/localizations/es.ts +60 -0
  52. package/src/localizations/getLocalizationTable.test.ts +27 -0
  53. package/src/localizations/getLocalizationTable.ts +53 -0
  54. package/src/rendering/caching/RenderingCacheNode.ts +6 -1
  55. package/src/toolbar/HTMLToolbar.ts +11 -9
  56. package/src/toolbar/icons.ts +13 -13
  57. package/src/toolbar/localization.ts +2 -0
  58. package/src/toolbar/makeColorInput.ts +25 -10
  59. package/src/toolbar/toolbar.css +5 -0
  60. package/src/toolbar/widgets/BaseWidget.ts +34 -0
  61. package/src/tools/BaseTool.ts +5 -1
  62. package/src/tools/PanZoom.ts +6 -0
  63. package/src/tools/SelectionTool.test.ts +24 -1
  64. package/src/tools/SelectionTool.ts +158 -23
  65. package/src/tools/ToolController.ts +6 -2
  66. package/src/tools/localization.ts +2 -0
  67. package/src/types.ts +9 -1
  68. package/dist-test/test-dist-bundle.html +0 -42
@@ -147,14 +147,27 @@ describe('Rect2', () => {
147
147
  it('division of empty square', () => {
148
148
  expect(Rect2.empty.divideIntoGrid(1000, 10000).length).toBe(1);
149
149
  });
150
+
151
+ it('division of rectangle', () => {
152
+ expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject(
153
+ [
154
+ new Rect2(0, 0, 1, 0.5), new Rect2(1, 0, 1, 0.5),
155
+ new Rect2(0, 0.5, 1, 0.5), new Rect2(1, 0.5, 1, 0.5),
156
+ ]
157
+ );
158
+ });
150
159
  });
151
160
 
152
- it('division of rectangle', () => {
153
- expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject(
154
- [
155
- new Rect2(0, 0, 1, 0.5), new Rect2(1, 0, 1, 0.5),
156
- new Rect2(0, 0.5, 1, 0.5), new Rect2(1, 0.5, 1, 0.5),
157
- ]
158
- );
161
+ describe('should correctly return the closest point on the edge of a rectangle', () => {
162
+ it('with the unit square', () => {
163
+ const rect = Rect2.unitSquare;
164
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.zero)).objEq(Vec2.zero);
165
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(-1, -1))).objEq(Vec2.zero);
166
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(-1, 0.5))).objEq(Vec2.of(0, 0.5));
167
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(1, 0.5))).objEq(Vec2.of(1, 0.5));
168
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(0.6, 0.6))).objEq(Vec2.of(1, 0.6));
169
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(2, 0.5))).objEq(Vec2.of(1, 0.5));
170
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(0.6, 0.6))).objEq(Vec2.of(1, 0.6));
171
+ });
159
172
  });
160
173
  });
@@ -51,6 +51,7 @@ export default class Rect2 {
51
51
  return new Rect2(vec.x + this.x, vec.y + this.y, this.w, this.h);
52
52
  }
53
53
 
54
+ // Returns a copy of this with the given size (but same top-left).
54
55
  public resizedTo(size: Vec2): Rect2 {
55
56
  return new Rect2(this.x, this.y, size.x, size.y);
56
57
  }
@@ -163,6 +164,23 @@ export default class Rect2 {
163
164
  );
164
165
  }
165
166
 
167
+ public getClosestPointOnBoundaryTo(target: Point2) {
168
+ const closestEdgePoints = this.getEdges().map(edge => {
169
+ return edge.closestPointTo(target);
170
+ });
171
+
172
+ let closest: Point2|null = null;
173
+ let closestDist: number|null = null;
174
+ for (const point of closestEdgePoints) {
175
+ const dist = point.minus(target).length();
176
+ if (closestDist === null || dist < closestDist) {
177
+ closest = point;
178
+ closestDist = dist;
179
+ }
180
+ }
181
+ return closest!;
182
+ }
183
+
166
184
  public get corners(): Point2[] {
167
185
  return [
168
186
  this.bottomRight,
@@ -172,7 +190,7 @@ export default class Rect2 {
172
190
  ];
173
191
  }
174
192
 
175
- public get maxDimension(): number {
193
+ public get maxDimension() {
176
194
  return Math.max(this.w, this.h);
177
195
  }
178
196
 
@@ -0,0 +1,8 @@
1
+ import { defaultEditorLocalization, EditorLocalization } from '../localization';
2
+
3
+ // Default localizations are already in English.
4
+ const localization: EditorLocalization = {
5
+ ...defaultEditorLocalization,
6
+ };
7
+
8
+ export default localization;
@@ -0,0 +1,60 @@
1
+ import { defaultEditorLocalization, EditorLocalization } from '../localization';
2
+
3
+ // A partial Spanish localization.
4
+ const localization: EditorLocalization = {
5
+ ...defaultEditorLocalization,
6
+
7
+ // Strings for the main editor interface
8
+ // (see src/localization.ts)
9
+ loading: (percentage: number) => `Cargando: ${percentage}%...`,
10
+ imageEditor: 'Editor de dibujos',
11
+
12
+ undoAnnouncement: (commandDescription: string) => `${commandDescription} fue deshecho`,
13
+ redoAnnouncement: (commandDescription: string) => `${commandDescription} fue rehecho`,
14
+ undo: 'Deshace',
15
+ redo: 'Rehace',
16
+
17
+ // Strings for the toolbar
18
+ // (see src/toolbar/localization.ts)
19
+ pen: 'Lapiz',
20
+ eraser: 'Borrador',
21
+ select: 'Selecciona',
22
+ thicknessLabel: 'Tamaño: ',
23
+ colorLabel: 'Color: ',
24
+ doneLoading: 'El cargado terminó',
25
+ fontLabel: 'Fuente: ',
26
+ anyDevicePanning: 'Mover la pantalla con todo dispotivo',
27
+ touchPanning: 'Mover la pantalla con un dedo',
28
+ touchPanTool: 'Instrumento de mover la pantalla con un dedo',
29
+ outlinedRectanglePen: 'Rectángulo con nada más que un borde',
30
+ filledRectanglePen: 'Rectángulo sin borde',
31
+ linePen: 'Línea',
32
+ arrowPen: 'Flecha',
33
+ freehandPen: 'Dibuja sin restricción de forma',
34
+ selectObjectType: 'Forma de dibuja:',
35
+ handTool: 'Mover',
36
+ resizeImageToSelection: 'Redimensionar la imagen a lo que está seleccionado',
37
+ deleteSelection: 'Borra la selección',
38
+ duplicateSelection: 'Duplica la selección',
39
+ pickColorFronScreen: 'Selecciona un color de la pantalla',
40
+ dropdownShown(toolName: string): string {
41
+ return `Menú por ${toolName} es visible`;
42
+ },
43
+ dropdownHidden: function (toolName: string): string {
44
+ return `Menú por ${toolName} fue ocultado`;
45
+ },
46
+ colorChangedAnnouncement: function (color: string): string {
47
+ return `Color fue cambiado a ${color}`;
48
+ },
49
+ keyboardPanZoom: 'Mover la pantalla con el teclado',
50
+ penTool: function (penId: number): string {
51
+ return `Lapiz ${penId}`;
52
+ },
53
+ selectionTool: 'Selecciona',
54
+ eraserTool: 'Borrador',
55
+ textTool: 'Texto',
56
+ enterTextToInsert: 'Entra texto',
57
+ rerenderAsText: 'Redibuja la pantalla al texto',
58
+ };
59
+
60
+ export default localization;
@@ -0,0 +1,27 @@
1
+
2
+ import { defaultEditorLocalization } from '../localization';
3
+ import en from './en';
4
+ import es from './es';
5
+ import getLocalizationTable from './getLocalizationTable';
6
+
7
+ describe('getLocalizationTable', () => {
8
+ it('should return the en localization for es_TEST', () => {
9
+ expect(getLocalizationTable([ 'es_TEST' ]) === es).toBe(true);
10
+ });
11
+
12
+ it('should return the default localization for unsupported language', () => {
13
+ expect(getLocalizationTable([ 'test' ]) === defaultEditorLocalization).toBe(true);
14
+ });
15
+
16
+ it('should return the first localization matching a language in the list of user locales', () => {
17
+ expect(getLocalizationTable([ 'test_TEST1', 'test_TEST2', 'test_TEST3', 'en_TEST', 'notalanguage']) === en).toBe(true);
18
+ });
19
+
20
+ it('should return the default localization for unsupported language', () => {
21
+ expect(getLocalizationTable([ 'test' ]) === defaultEditorLocalization).toBe(true);
22
+ });
23
+
24
+ it('should return first of user\'s supported languages', () => {
25
+ expect(getLocalizationTable([ 'es_MX', 'es_ES', 'en_US' ]) === es).toBe(true);
26
+ });
27
+ });
@@ -0,0 +1,53 @@
1
+
2
+ import { defaultEditorLocalization, EditorLocalization } from '../localization';
3
+ import en from './en';
4
+ import es from './es';
5
+
6
+ const allLocales: Record<string, EditorLocalization> = {
7
+ en,
8
+ es,
9
+ };
10
+
11
+ // [locale]: A string in the format languageCode_Region or just languageCode. For example, en_US.
12
+ const languageFromLocale = (locale: string) => {
13
+ const matches = /^(\w+)[_-](\w+)$/.exec(locale);
14
+ if (!matches) {
15
+ // If not in languageCode_region format, the locale should be the
16
+ // languageCode. Return that.
17
+ return locale;
18
+ }
19
+
20
+ return matches[1];
21
+ };
22
+
23
+ const getLocalizationTable = (userLocales?: readonly string[]): EditorLocalization => {
24
+ userLocales ??= navigator.languages;
25
+
26
+ let prevLanguage: string|undefined;
27
+ for (const locale of userLocales) {
28
+ const language = languageFromLocale(locale);
29
+
30
+ // If the specific localization of the language is not available, but
31
+ // a localization for the language is,
32
+ if (prevLanguage && language !== prevLanguage) {
33
+ if (prevLanguage in allLocales) {
34
+ return allLocales[prevLanguage];
35
+ }
36
+ }
37
+
38
+ // If the full locale (e.g. en_US) is available,
39
+ if (locale in allLocales) {
40
+ return allLocales[locale];
41
+ }
42
+
43
+ prevLanguage = language;
44
+ }
45
+
46
+ if (prevLanguage && prevLanguage in allLocales) {
47
+ return allLocales[prevLanguage];
48
+ } else {
49
+ return defaultEditorLocalization;
50
+ }
51
+ };
52
+
53
+ export default getLocalizationTable;
@@ -65,6 +65,11 @@ export default class RenderingCacheNode {
65
65
  if (this.instantiatedChildren.length === 0) {
66
66
  const childRects = this.region.divideIntoGrid(cacheDivisionSize, cacheDivisionSize);
67
67
 
68
+ if (this.region.size.x === 0 || this.region.size.y === 0) {
69
+ console.warn('Cache element has zero size! Not generating children.');
70
+ return;
71
+ }
72
+
68
73
  for (const rect of childRects) {
69
74
  const child = new RenderingCacheNode(rect, this.cacheState);
70
75
  child.parent = this;
@@ -357,7 +362,7 @@ export default class RenderingCacheNode {
357
362
 
358
363
  private checkRep() {
359
364
  if (this.instantiatedChildren.length !== cacheDivisionSize * cacheDivisionSize && this.instantiatedChildren.length !== 0) {
360
- throw new Error('Repcheck: Wrong number of children');
365
+ throw new Error(`Repcheck: Wrong number of children. Got ${this.instantiatedChildren.length}`);
361
366
  }
362
367
 
363
368
  if (this.renderedIds[1] !== undefined && this.renderedIds[0] >= this.renderedIds[1]) {
@@ -25,6 +25,8 @@ export const toolbarCSSPrefix = 'toolbar-';
25
25
  export default class HTMLToolbar {
26
26
  private container: HTMLElement;
27
27
 
28
+ private static colorisStarted: boolean = false;
29
+
28
30
  public constructor(
29
31
  private editor: Editor, parent: HTMLElement,
30
32
  private localizationTable: ToolbarLocalization = defaultToolbarLocalization,
@@ -34,7 +36,10 @@ export default class HTMLToolbar {
34
36
  this.container.setAttribute('role', 'toolbar');
35
37
  parent.appendChild(this.container);
36
38
 
37
- colorisInit();
39
+ if (!HTMLToolbar.colorisStarted) {
40
+ colorisInit();
41
+ HTMLToolbar.colorisStarted = true;
42
+ }
38
43
  this.setupColorPickers();
39
44
  }
40
45
 
@@ -133,13 +138,13 @@ export default class HTMLToolbar {
133
138
  undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
134
139
 
135
140
  const undoButton = this.addActionButton({
136
- label: 'Undo',
141
+ label: this.localizationTable.undo,
137
142
  icon: makeUndoIcon()
138
143
  }, () => {
139
144
  this.editor.history.undo();
140
145
  }, undoRedoGroup);
141
146
  const redoButton = this.addActionButton({
142
- label: 'Redo',
147
+ label: this.localizationTable.redo,
143
148
  icon: makeRedoIcon(),
144
149
  }, () => {
145
150
  this.editor.history.redo();
@@ -195,12 +200,9 @@ export default class HTMLToolbar {
195
200
  (new TextToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
196
201
  }
197
202
 
198
- for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) {
199
- if (!(tool instanceof PanZoom)) {
200
- throw new Error('All SelectionTools must have kind === ToolType.PanZoom');
201
- }
202
-
203
- (new HandToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
203
+ const panZoomTool = toolController.getMatchingTools(ToolType.PanZoom)[0];
204
+ if (panZoomTool && panZoomTool instanceof PanZoom) {
205
+ (new HandToolWidget(this.editor, panZoomTool, this.localizationTable)).addTo(this.container);
204
206
  }
205
207
 
206
208
  this.setupColorPickers();
@@ -9,11 +9,11 @@ import { StrokeDataPoint } from '../types';
9
9
  import Viewport from '../Viewport';
10
10
 
11
11
  const svgNamespace = 'http://www.w3.org/2000/svg';
12
- const primaryForegroundFill = `
13
- style='fill: var(--primary-foreground-color);'
12
+ const iconColorFill = `
13
+ style='fill: var(--icon-color);'
14
14
  `;
15
- const primaryForegroundStrokeFill = `
16
- style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);'
15
+ const iconColorStrokeFill = `
16
+ style='fill: var(--icon-color); stroke: var(--icon-color);'
17
17
  `;
18
18
  const checkerboardPatternDef = `
19
19
  <pattern
@@ -39,7 +39,7 @@ export const makeRedoIcon = (mirror: boolean = false) => {
39
39
  icon.innerHTML = `
40
40
  <style>
41
41
  .toolbar-svg-undo-redo-icon {
42
- stroke: var(--primary-foreground-color);
42
+ stroke: var(--icon-color);
43
43
  stroke-width: 12;
44
44
  stroke-linejoin: round;
45
45
  stroke-linecap: round;
@@ -63,7 +63,7 @@ export const makeDropdownIcon = () => {
63
63
  <g>
64
64
  <path
65
65
  d='M5,10 L50,90 L95,10 Z'
66
- ${primaryForegroundFill}
66
+ ${iconColorFill}
67
67
  />
68
68
  </g>
69
69
  `;
@@ -80,7 +80,7 @@ export const makeEraserIcon = () => {
80
80
  <rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
81
81
  <rect
82
82
  x=10 y=10 width=80 height=50
83
- ${primaryForegroundFill}
83
+ ${iconColorFill}
84
84
  />
85
85
  </g>
86
86
  `;
@@ -132,7 +132,7 @@ export const makeHandToolIcon = () => {
132
132
 
133
133
  fill='none'
134
134
  style='
135
- stroke: var(--primary-foreground-color);
135
+ stroke: var(--icon-color);
136
136
  stroke-width: 2;
137
137
  '
138
138
  />
@@ -175,7 +175,7 @@ export const makeTouchPanningIcon = () => {
175
175
  '
176
176
  fill='none'
177
177
  style='
178
- stroke: var(--primary-foreground-color);
178
+ stroke: var(--icon-color);
179
179
  stroke-width: 2;
180
180
  '
181
181
  />
@@ -241,7 +241,7 @@ export const makeAllDevicePanningIcon = () => {
241
241
  '
242
242
  fill='none'
243
243
  style='
244
- stroke: var(--primary-foreground-color);
244
+ stroke: var(--icon-color);
245
245
  stroke-width: 2;
246
246
  '
247
247
  />
@@ -263,7 +263,7 @@ export const makeZoomIcon = () => {
263
263
  textNode.style.textAlign = 'center';
264
264
  textNode.style.textAnchor = 'middle';
265
265
  textNode.style.fontSize = '55px';
266
- textNode.style.fill = 'var(--primary-foreground-color)';
266
+ textNode.style.fill = 'var(--icon-color)';
267
267
  textNode.style.fontFamily = 'monospace';
268
268
 
269
269
  icon.appendChild(textNode);
@@ -315,7 +315,7 @@ export const makePenIcon = (tipThickness: number, color: string) => {
315
315
  <!-- Pen grip -->
316
316
  <path
317
317
  d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
318
- ${primaryForegroundStrokeFill}
318
+ ${iconColorStrokeFill}
319
319
  />
320
320
  </g>
321
321
  <g>
@@ -389,7 +389,7 @@ export const makePipetteIcon = (color?: Color4) => {
389
389
  65,15 65,5 47,6
390
390
  Z
391
391
  `);
392
- pipette.style.fill = 'var(--primary-foreground-color)';
392
+ pipette.style.fill = 'var(--icon-color)';
393
393
 
394
394
  if (color) {
395
395
  const defs = document.createElementNS(svgNamespace, 'defs');
@@ -23,6 +23,7 @@ export interface ToolbarLocalization {
23
23
  undo: string;
24
24
  redo: string;
25
25
  zoom: string;
26
+ selectionToolKeyboardShortcuts: string;
26
27
 
27
28
  dropdownShown: (toolName: string)=> string;
28
29
  dropdownHidden: (toolName: string)=> string;
@@ -46,6 +47,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
46
47
  redo: 'Redo',
47
48
  selectObjectType: 'Object type: ',
48
49
  pickColorFronScreen: 'Pick color from screen',
50
+ selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
49
51
 
50
52
  touchPanning: 'Touchscreen panning',
51
53
  anyDevicePanning: 'Any device panning',
@@ -20,7 +20,7 @@ export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListe
20
20
  colorInputContainer.appendChild(colorInput);
21
21
  addPipetteTool(editor, colorInputContainer, (color: Color4) => {
22
22
  colorInput.value = color.toHexString();
23
- handleColorInput();
23
+ onInputEnd();
24
24
 
25
25
  // Update the color preview, if it exists (may be managed by Coloris).
26
26
  const parentElem = colorInput.parentElement;
@@ -32,15 +32,20 @@ export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListe
32
32
  let currentColor: Color4|undefined;
33
33
  const handleColorInput = () => {
34
34
  currentColor = Color4.fromHex(colorInput.value);
35
- editor.announceForAccessibility(
36
- editor.localization.colorChangedAnnouncement(currentColor.toHexString())
37
- );
38
- onColorChange(currentColor);
35
+ };
36
+ const onInputEnd = () => {
37
+ handleColorInput();
39
38
 
40
- editor.notifier.dispatch(EditorEventType.ColorPickerColorSelected, {
41
- kind: EditorEventType.ColorPickerColorSelected,
42
- color: currentColor,
43
- });
39
+ if (currentColor) {
40
+ editor.announceForAccessibility(
41
+ editor.localization.colorChangedAnnouncement(currentColor.toHexString())
42
+ );
43
+ onColorChange(currentColor);
44
+ editor.notifier.dispatch(EditorEventType.ColorPickerColorSelected, {
45
+ kind: EditorEventType.ColorPickerColorSelected,
46
+ color: currentColor,
47
+ });
48
+ }
44
49
  };
45
50
 
46
51
  colorInput.oninput = handleColorInput;
@@ -55,6 +60,7 @@ export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListe
55
60
  kind: EditorEventType.ColorPickerToggled,
56
61
  open: false,
57
62
  });
63
+ onInputEnd();
58
64
  });
59
65
 
60
66
  return [ colorInput, colorInputContainer ];
@@ -72,10 +78,13 @@ const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: O
72
78
  updatePipetteIcon();
73
79
 
74
80
  const pipetteTool: PipetteTool|undefined = editor.toolController.getMatchingTools(ToolType.Pipette)[0] as PipetteTool|undefined;
75
- const pipetteColorSelect = (color: Color4|null) => {
81
+ const endColorSelectMode = () => {
76
82
  pipetteTool?.clearColorListener();
77
83
  updatePipetteIcon();
78
84
  pipetteButton.classList.remove('active');
85
+ };
86
+ const pipetteColorSelect = (color: Color4|null) => {
87
+ endColorSelectMode();
79
88
 
80
89
  if (color) {
81
90
  onColorChange(color);
@@ -90,6 +99,12 @@ const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: O
90
99
  };
91
100
 
92
101
  pipetteButton.onclick = () => {
102
+ // If already picking, cancel it.
103
+ if (pipetteButton.classList.contains('active')) {
104
+ endColorSelectMode();
105
+ return;
106
+ }
107
+
93
108
  pipetteTool?.setColorListener(
94
109
  pipetteColorPreview,
95
110
  pipetteColorSelect,
@@ -1,5 +1,7 @@
1
1
  .toolbar-root {
2
2
  background-color: var(--primary-background-color);
3
+ --icon-color: var(--primary-foreground-color);
4
+
3
5
 
4
6
  border: 1px solid var(--secondary-background-color);
5
7
  border-radius: 2px;
@@ -38,6 +40,7 @@
38
40
  text-align: center;
39
41
  border-radius: 6px;
40
42
 
43
+ --icon-color: var(--primary-foreground-color);
41
44
  background-color: var(--primary-background-color);
42
45
  color: var(--primary-foreground-color);
43
46
  border: none;
@@ -83,6 +86,7 @@
83
86
  .toolbar-toolContainer.selected .toolbar-button {
84
87
  background-color: var(--secondary-background-color);
85
88
  color: var(--secondary-foreground-color);
89
+ --icon-color: var(--secondary-foreground-color);
86
90
  }
87
91
 
88
92
  .toolbar-toolContainer:not(.selected):not(.dropdownShowable) > .toolbar-button > .toolbar-showHideDropdownIcon {
@@ -173,4 +177,5 @@
173
177
 
174
178
  .color-input-container .pipetteButton.active {
175
179
  background-color: var(--secondary-background-color);
180
+ --icon-color: var(--secondary-foreground-color);
176
181
  }
@@ -1,4 +1,5 @@
1
1
  import Editor from '../../Editor';
2
+ import { InputEvtType } from '../../types';
2
3
  import { toolbarCSSPrefix } from '../HTMLToolbar';
3
4
  import { makeDropdownIcon } from '../icons';
4
5
  import { ToolbarLocalization } from '../localization';
@@ -50,6 +51,39 @@ export default abstract class BaseWidget {
50
51
  }
51
52
 
52
53
  protected setupActionBtnClickListener(button: HTMLElement) {
54
+ const clickTriggers = { enter: true, ' ': true, };
55
+ button.onkeydown = (evt) => {
56
+ let handled = false;
57
+
58
+ if (evt.key in clickTriggers) {
59
+ if (!this.disabled) {
60
+ this.handleClick();
61
+ handled = true;
62
+ }
63
+ }
64
+
65
+ // If we didn't do anything with the event, send it to the editor.
66
+ if (!handled) {
67
+ this.editor.toolController.dispatchInputEvent({
68
+ kind: InputEvtType.KeyPressEvent,
69
+ key: evt.key,
70
+ ctrlKey: evt.ctrlKey,
71
+ });
72
+ }
73
+ };
74
+
75
+ button.onkeyup = evt => {
76
+ if (evt.key in clickTriggers) {
77
+ return;
78
+ }
79
+
80
+ this.editor.toolController.dispatchInputEvent({
81
+ kind: InputEvtType.KeyUpEvent,
82
+ key: evt.key,
83
+ ctrlKey: evt.ctrlKey,
84
+ });
85
+ };
86
+
53
87
  button.onclick = () => {
54
88
  if (!this.disabled) {
55
89
  this.handleClick();
@@ -1,4 +1,4 @@
1
- import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent } from '../types';
1
+ import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent, KeyUpEvent } from '../types';
2
2
  import { ToolType } from './ToolController';
3
3
  import ToolEnabledGroup from './ToolEnabledGroup';
4
4
 
@@ -24,6 +24,10 @@ export default abstract class BaseTool implements PointerEvtListener {
24
24
  return false;
25
25
  }
26
26
 
27
+ public onKeyUp(_event: KeyUpEvent): boolean {
28
+ return false;
29
+ }
30
+
27
31
  public setEnabled(enabled: boolean) {
28
32
  this.enabled = enabled;
29
33
 
@@ -16,11 +16,13 @@ interface PinchData {
16
16
  dist: number;
17
17
  }
18
18
 
19
+
19
20
  export enum PanZoomMode {
20
21
  OneFingerTouchGestures = 0x1,
21
22
  TwoFingerTouchGestures = 0x1 << 1,
22
23
  RightClickDrags = 0x1 << 2,
23
24
  SinglePointerGestures = 0x1 << 3,
25
+ Keyboard = 0x1 << 4,
24
26
  }
25
27
 
26
28
  export default class PanZoom extends BaseTool {
@@ -180,6 +182,10 @@ export default class PanZoom extends BaseTool {
180
182
  }
181
183
 
182
184
  public onKeyPress({ key }: KeyPressEvent): boolean {
185
+ if (!(this.mode & PanZoomMode.Keyboard)) {
186
+ return false;
187
+ }
188
+
183
189
  let translation = Vec2.zero;
184
190
  let scale = 1;
185
191
  let rotation = 0;
@@ -66,7 +66,7 @@ describe('SelectionTool', () => {
66
66
 
67
67
  // Drag the object
68
68
  selection!.handleBackgroundDrag(Vec2.of(5, 5));
69
- selection!.finishDragging();
69
+ selection!.finalizeTransform();
70
70
 
71
71
  expect(testStroke.getBBox().topLeft).toMatchObject({
72
72
  x: 5,
@@ -80,4 +80,27 @@ describe('SelectionTool', () => {
80
80
  y: 0,
81
81
  });
82
82
  });
83
+
84
+ it('moving the selection with a keyboard should move the view to keep the selection in view', () => {
85
+ const { addTestStrokeCommand } = createSquareStroke();
86
+ const editor = createEditor();
87
+ editor.dispatch(addTestStrokeCommand);
88
+
89
+ // Select the stroke
90
+ const selectionTool = getSelectionTool(editor);
91
+ selectionTool.setEnabled(true);
92
+ editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
93
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10));
94
+ editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(100, 100));
95
+
96
+ const selection = selectionTool.getSelection();
97
+ if (selection === null) {
98
+ // Throw to allow TypeScript's non-null checker to understand that selection
99
+ // must be non-null after this.
100
+ throw new Error('Selection should be non-null.');
101
+ }
102
+
103
+ selection.handleBackgroundDrag(Vec2.of(0, -1000));
104
+ expect(editor.viewport.visibleRect.containsPoint(selection.region.center)).toBe(true);
105
+ });
83
106
  });