js-draw 0.4.1 → 0.5.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.
@@ -193,7 +193,7 @@ export declare class Editor {
193
193
  remove: () => void;
194
194
  };
195
195
  addStyleSheet(content: string): HTMLStyleElement;
196
- sendKeyboardEvent(eventType: InputEvtType.KeyPressEvent | InputEvtType.KeyUpEvent, key: string, ctrlKey?: boolean): void;
196
+ sendKeyboardEvent(eventType: InputEvtType.KeyPressEvent | InputEvtType.KeyUpEvent, key: string, ctrlKey?: boolean, altKey?: boolean): void;
197
197
  sendPenEvent(eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, point: Point2, allPointers?: Pointer[]): void;
198
198
  toSVG(): SVGElement;
199
199
  loadFrom(loader: ImageLoader): Promise<void>;
@@ -441,6 +441,7 @@ export class Editor {
441
441
  kind: InputEvtType.KeyPressEvent,
442
442
  key: evt.key,
443
443
  ctrlKey: evt.ctrlKey,
444
+ altKey: evt.altKey,
444
445
  })) {
445
446
  evt.preventDefault();
446
447
  }
@@ -453,6 +454,7 @@ export class Editor {
453
454
  kind: InputEvtType.KeyUpEvent,
454
455
  key: evt.key,
455
456
  ctrlKey: evt.ctrlKey,
457
+ altKey: evt.altKey,
456
458
  })) {
457
459
  evt.preventDefault();
458
460
  }
@@ -598,11 +600,12 @@ export class Editor {
598
600
  }
599
601
  // Dispatch a keyboard event to the currently selected tool.
600
602
  // Intended for unit testing
601
- sendKeyboardEvent(eventType, key, ctrlKey = false) {
603
+ sendKeyboardEvent(eventType, key, ctrlKey = false, altKey = false) {
602
604
  this.toolController.dispatchInputEvent({
603
605
  kind: eventType,
604
606
  key,
605
- ctrlKey
607
+ ctrlKey,
608
+ altKey,
606
609
  });
607
610
  }
608
611
  // Dispatch a pen event to the currently selected tool.
@@ -320,7 +320,7 @@ export default class FreehandLineBuilder {
320
320
  }
321
321
  let exitingVec = this.computeExitingVec();
322
322
  // Find the intersection between the entering vector and the exiting vector
323
- const maxRelativeLength = 3;
323
+ const maxRelativeLength = 2;
324
324
  const segmentStart = this.buffer[0];
325
325
  const segmentEnd = newPoint.pos;
326
326
  const startEndDist = segmentEnd.minus(segmentStart).magnitude();
@@ -72,7 +72,7 @@ export default class SVGRenderer extends AbstractRenderer {
72
72
  drawPath(pathSpec) {
73
73
  var _a;
74
74
  const style = pathSpec.style;
75
- const path = Path.fromRenderable(pathSpec);
75
+ const path = Path.fromRenderable(pathSpec).transformedBy(this.getCanvasToScreenTransform());
76
76
  // Try to extend the previous path, if possible
77
77
  if (!style.fill.eq((_a = this.lastPathStyle) === null || _a === void 0 ? void 0 : _a.fill) || this.lastPathString.length === 0) {
78
78
  this.addPathToSVG();
@@ -1,4 +1,5 @@
1
1
  import Editor from '../../Editor';
2
+ import { KeyPressEvent } from '../../types';
2
3
  import { ToolbarLocalization } from '../localization';
3
4
  export default abstract class BaseWidget {
4
5
  #private;
@@ -18,6 +19,7 @@ export default abstract class BaseWidget {
18
19
  protected abstract createIcon(): Element;
19
20
  protected fillDropdown(dropdown: HTMLElement): boolean;
20
21
  protected setupActionBtnClickListener(button: HTMLElement): void;
22
+ protected onKeyPress(_event: KeyPressEvent): boolean;
21
23
  protected abstract handleClick(): void;
22
24
  protected get hasDropdown(): boolean;
23
25
  protected addSubWidget(widget: BaseWidget): void;
@@ -10,6 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
12
  var _BaseWidget_hasDropdown;
13
+ import ToolbarShortcutHandler from '../../tools/ToolbarShortcutHandler';
13
14
  import { EditorEventType, InputEvtType } from '../../types';
14
15
  import { toolbarCSSPrefix } from '../HTMLToolbar';
15
16
  import { makeDropdownIcon } from '../icons';
@@ -33,6 +34,12 @@ export default class BaseWidget {
33
34
  this.label = document.createElement('label');
34
35
  this.button.setAttribute('role', 'button');
35
36
  this.button.tabIndex = 0;
37
+ const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler);
38
+ // If the onKeyPress function has been extended and the editor is configured to send keypress events to
39
+ // toolbar widgets,
40
+ if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== BaseWidget.prototype.onKeyPress) {
41
+ toolbarShortcutHandlers[0].registerListener(event => this.onKeyPress(event));
42
+ }
36
43
  }
37
44
  // Add content to the widget's associated dropdown menu.
38
45
  // Returns true if such a menu should be created, false otherwise.
@@ -62,6 +69,7 @@ export default class BaseWidget {
62
69
  kind: InputEvtType.KeyPressEvent,
63
70
  key: evt.key,
64
71
  ctrlKey: evt.ctrlKey,
72
+ altKey: evt.altKey,
65
73
  });
66
74
  }
67
75
  };
@@ -73,6 +81,7 @@ export default class BaseWidget {
73
81
  kind: InputEvtType.KeyUpEvent,
74
82
  key: evt.key,
75
83
  ctrlKey: evt.ctrlKey,
84
+ altKey: evt.altKey,
76
85
  });
77
86
  };
78
87
  button.onclick = () => {
@@ -81,6 +90,12 @@ export default class BaseWidget {
81
90
  }
82
91
  };
83
92
  }
93
+ // Add a listener that is triggered when a key is pressed.
94
+ // Listeners will fire regardless of whether this widget is selected and require that
95
+ // {@link lib!Editor.toolController} to have an enabled {@link lib!ToolbarShortcutHandler} tool.
96
+ onKeyPress(_event) {
97
+ return false;
98
+ }
84
99
  get hasDropdown() {
85
100
  return __classPrivateFieldGet(this, _BaseWidget_hasDropdown, "f");
86
101
  }
@@ -1,6 +1,7 @@
1
1
  import { ComponentBuilderFactory } from '../../components/builders/types';
2
2
  import Editor from '../../Editor';
3
3
  import Pen from '../../tools/Pen';
4
+ import { KeyPressEvent } from '../../types';
4
5
  import { ToolbarLocalization } from '../localization';
5
6
  import BaseToolWidget from './BaseToolWidget';
6
7
  export interface PenTypeRecord {
@@ -16,4 +17,5 @@ export default class PenToolWidget extends BaseToolWidget {
16
17
  protected createIcon(): Element;
17
18
  private static idCounter;
18
19
  protected fillDropdown(dropdown: HTMLElement): boolean;
20
+ protected onKeyPress(event: KeyPressEvent): boolean;
19
21
  }
@@ -127,5 +127,19 @@ export default class PenToolWidget extends BaseToolWidget {
127
127
  dropdown.replaceChildren(container);
128
128
  return true;
129
129
  }
130
+ onKeyPress(event) {
131
+ if (!this.isSelected()) {
132
+ return false;
133
+ }
134
+ // Map alt+0-9 to different pen types.
135
+ if (/^[0-9]$/.exec(event.key) && event.ctrlKey) {
136
+ const penTypeIdx = parseInt(event.key) - 1;
137
+ if (penTypeIdx >= 0 && penTypeIdx < this.penTypes.length) {
138
+ this.tool.setStrokeFactory(this.penTypes[penTypeIdx].factory);
139
+ return true;
140
+ }
141
+ }
142
+ return false;
143
+ }
130
144
  }
131
145
  PenToolWidget.idCounter = 0;
@@ -10,6 +10,7 @@ import TextTool from './TextTool';
10
10
  import PipetteTool from './PipetteTool';
11
11
  import ToolSwitcherShortcut from './ToolSwitcherShortcut';
12
12
  import PasteHandler from './PasteHandler';
13
+ import ToolbarShortcutHandler from './ToolbarShortcutHandler';
13
14
  export default class ToolController {
14
15
  /** @internal */
15
16
  constructor(editor, localization) {
@@ -35,6 +36,7 @@ export default class ToolController {
35
36
  ...primaryTools,
36
37
  keyboardPanZoomTool,
37
38
  new UndoRedoShortcut(editor),
39
+ new ToolbarShortcutHandler(editor),
38
40
  new ToolSwitcherShortcut(editor),
39
41
  new PasteHandler(editor),
40
42
  ];
@@ -0,0 +1,12 @@
1
+ import Editor from '../Editor';
2
+ import { KeyPressEvent } from '../types';
3
+ import BaseTool from './BaseTool';
4
+ declare type KeyPressListener = (event: KeyPressEvent) => boolean;
5
+ export default class ToolbarShortcutHandler extends BaseTool {
6
+ private listeners;
7
+ constructor(editor: Editor);
8
+ registerListener(listener: KeyPressListener): void;
9
+ removeListener(listener: KeyPressListener): void;
10
+ onKeyPress(event: KeyPressEvent): boolean;
11
+ }
12
+ export {};
@@ -0,0 +1,23 @@
1
+ // Allows the toolbar to register keyboard events.
2
+ // @packageDocumentation
3
+ import BaseTool from './BaseTool';
4
+ export default class ToolbarShortcutHandler extends BaseTool {
5
+ constructor(editor) {
6
+ super(editor.notifier, editor.localization.changeTool);
7
+ this.listeners = new Set([]);
8
+ }
9
+ registerListener(listener) {
10
+ this.listeners.add(listener);
11
+ }
12
+ removeListener(listener) {
13
+ this.listeners.delete(listener);
14
+ }
15
+ onKeyPress(event) {
16
+ for (const listener of this.listeners) {
17
+ if (listener(event)) {
18
+ return true;
19
+ }
20
+ }
21
+ return false;
22
+ }
23
+ }
@@ -12,3 +12,4 @@ export { default as TextTool } from './TextTool';
12
12
  export { default as SelectionTool } from './SelectionTool/SelectionTool';
13
13
  export { default as EraserTool } from './Eraser';
14
14
  export { default as PasteHandler } from './PasteHandler';
15
+ export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
@@ -12,3 +12,4 @@ export { default as TextTool } from './TextTool';
12
12
  export { default as SelectionTool } from './SelectionTool/SelectionTool';
13
13
  export { default as EraserTool } from './Eraser';
14
14
  export { default as PasteHandler } from './PasteHandler';
15
+ export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
@@ -34,12 +34,14 @@ export interface WheelEvt {
34
34
  export interface KeyPressEvent {
35
35
  readonly kind: InputEvtType.KeyPressEvent;
36
36
  readonly key: string;
37
- readonly ctrlKey: boolean;
37
+ readonly ctrlKey: boolean | undefined;
38
+ readonly altKey: boolean | undefined;
38
39
  }
39
40
  export interface KeyUpEvent {
40
41
  readonly kind: InputEvtType.KeyUpEvent;
41
42
  readonly key: string;
42
- readonly ctrlKey: boolean;
43
+ readonly ctrlKey: boolean | undefined;
44
+ readonly altKey: boolean | undefined;
43
45
  }
44
46
  export interface CopyEvent {
45
47
  readonly kind: InputEvtType.CopyEvent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
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",
package/src/Editor.ts CHANGED
@@ -586,6 +586,7 @@ export class Editor {
586
586
  kind: InputEvtType.KeyPressEvent,
587
587
  key: evt.key,
588
588
  ctrlKey: evt.ctrlKey,
589
+ altKey: evt.altKey,
589
590
  })) {
590
591
  evt.preventDefault();
591
592
  } else if (evt.key === 'Escape') {
@@ -598,6 +599,7 @@ export class Editor {
598
599
  kind: InputEvtType.KeyUpEvent,
599
600
  key: evt.key,
600
601
  ctrlKey: evt.ctrlKey,
602
+ altKey: evt.altKey,
601
603
  })) {
602
604
  evt.preventDefault();
603
605
  }
@@ -783,12 +785,14 @@ export class Editor {
783
785
  public sendKeyboardEvent(
784
786
  eventType: InputEvtType.KeyPressEvent|InputEvtType.KeyUpEvent,
785
787
  key: string,
786
- ctrlKey: boolean = false
788
+ ctrlKey: boolean = false,
789
+ altKey: boolean = false,
787
790
  ) {
788
791
  this.toolController.dispatchInputEvent({
789
792
  kind: eventType,
790
793
  key,
791
- ctrlKey
794
+ ctrlKey,
795
+ altKey,
792
796
  });
793
797
  }
794
798
 
@@ -426,7 +426,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
426
426
  let exitingVec = this.computeExitingVec();
427
427
 
428
428
  // Find the intersection between the entering vector and the exiting vector
429
- const maxRelativeLength = 3;
429
+ const maxRelativeLength = 2;
430
430
  const segmentStart = this.buffer[0];
431
431
  const segmentEnd = newPoint.pos;
432
432
  const startEndDist = segmentEnd.minus(segmentStart).magnitude();
@@ -88,7 +88,7 @@ export default class SVGRenderer extends AbstractRenderer {
88
88
 
89
89
  public drawPath(pathSpec: RenderablePathSpec) {
90
90
  const style = pathSpec.style;
91
- const path = Path.fromRenderable(pathSpec);
91
+ const path = Path.fromRenderable(pathSpec).transformedBy(this.getCanvasToScreenTransform());
92
92
 
93
93
  // Try to extend the previous path, if possible
94
94
  if (!style.fill.eq(this.lastPathStyle?.fill) || this.lastPathString.length === 0) {
@@ -1,5 +1,6 @@
1
1
  import Editor from '../../Editor';
2
- import { EditorEventType, InputEvtType } from '../../types';
2
+ import ToolbarShortcutHandler from '../../tools/ToolbarShortcutHandler';
3
+ import { EditorEventType, InputEvtType, KeyPressEvent } from '../../types';
3
4
  import { toolbarCSSPrefix } from '../HTMLToolbar';
4
5
  import { makeDropdownIcon } from '../icons';
5
6
  import { ToolbarLocalization } from '../localization';
@@ -33,6 +34,14 @@ export default abstract class BaseWidget {
33
34
  this.label = document.createElement('label');
34
35
  this.button.setAttribute('role', 'button');
35
36
  this.button.tabIndex = 0;
37
+
38
+ const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler);
39
+
40
+ // If the onKeyPress function has been extended and the editor is configured to send keypress events to
41
+ // toolbar widgets,
42
+ if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== BaseWidget.prototype.onKeyPress) {
43
+ toolbarShortcutHandlers[0].registerListener(event => this.onKeyPress(event));
44
+ }
36
45
  }
37
46
 
38
47
  protected abstract getTitle(): string;
@@ -70,6 +79,7 @@ export default abstract class BaseWidget {
70
79
  kind: InputEvtType.KeyPressEvent,
71
80
  key: evt.key,
72
81
  ctrlKey: evt.ctrlKey,
82
+ altKey: evt.altKey,
73
83
  });
74
84
  }
75
85
  };
@@ -83,6 +93,7 @@ export default abstract class BaseWidget {
83
93
  kind: InputEvtType.KeyUpEvent,
84
94
  key: evt.key,
85
95
  ctrlKey: evt.ctrlKey,
96
+ altKey: evt.altKey,
86
97
  });
87
98
  };
88
99
 
@@ -93,6 +104,13 @@ export default abstract class BaseWidget {
93
104
  };
94
105
  }
95
106
 
107
+ // Add a listener that is triggered when a key is pressed.
108
+ // Listeners will fire regardless of whether this widget is selected and require that
109
+ // {@link lib!Editor.toolController} to have an enabled {@link lib!ToolbarShortcutHandler} tool.
110
+ protected onKeyPress(_event: KeyPressEvent): boolean {
111
+ return false;
112
+ }
113
+
96
114
  protected abstract handleClick(): void;
97
115
 
98
116
  protected get hasDropdown() {
@@ -5,7 +5,7 @@ import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../../
5
5
  import { ComponentBuilderFactory } from '../../components/builders/types';
6
6
  import Editor from '../../Editor';
7
7
  import Pen from '../../tools/Pen';
8
- import { EditorEventType } from '../../types';
8
+ import { EditorEventType, KeyPressEvent } from '../../types';
9
9
  import { toolbarCSSPrefix } from '../HTMLToolbar';
10
10
  import { makeIconFromFactory, makePenIcon } from '../icons';
11
11
  import { ToolbarLocalization } from '../localization';
@@ -165,4 +165,21 @@ export default class PenToolWidget extends BaseToolWidget {
165
165
  dropdown.replaceChildren(container);
166
166
  return true;
167
167
  }
168
+
169
+ protected onKeyPress(event: KeyPressEvent): boolean {
170
+ if (!this.isSelected()) {
171
+ return false;
172
+ }
173
+
174
+ // Map alt+0-9 to different pen types.
175
+ if (/^[0-9]$/.exec(event.key) && event.ctrlKey) {
176
+ const penTypeIdx = parseInt(event.key) - 1;
177
+ if (penTypeIdx >= 0 && penTypeIdx < this.penTypes.length) {
178
+ this.tool.setStrokeFactory(this.penTypes[penTypeIdx].factory);
179
+ return true;
180
+ }
181
+ }
182
+
183
+ return false;
184
+ }
168
185
  }
@@ -1,7 +1,7 @@
1
1
 
2
2
  .selection-tool-selection-background {
3
3
  background-color: var(--secondary-background-color);
4
- opacity: 0.8;
4
+ opacity: 0.5;
5
5
  overflow: visible;
6
6
  }
7
7
 
@@ -13,6 +13,7 @@ import TextTool from './TextTool';
13
13
  import PipetteTool from './PipetteTool';
14
14
  import ToolSwitcherShortcut from './ToolSwitcherShortcut';
15
15
  import PasteHandler from './PasteHandler';
16
+ import ToolbarShortcutHandler from './ToolbarShortcutHandler';
16
17
 
17
18
  export default class ToolController {
18
19
  private tools: BaseTool[];
@@ -45,6 +46,7 @@ export default class ToolController {
45
46
  ...primaryTools,
46
47
  keyboardPanZoomTool,
47
48
  new UndoRedoShortcut(editor),
49
+ new ToolbarShortcutHandler(editor),
48
50
  new ToolSwitcherShortcut(editor),
49
51
  new PasteHandler(editor),
50
52
  ];
@@ -0,0 +1,34 @@
1
+ // Allows the toolbar to register keyboard events.
2
+ // @packageDocumentation
3
+
4
+ import Editor from '../Editor';
5
+ import { KeyPressEvent } from '../types';
6
+ import BaseTool from './BaseTool';
7
+
8
+ // Returns true if the event was handled, false otherwise.
9
+ type KeyPressListener = (event: KeyPressEvent)=>boolean;
10
+
11
+ export default class ToolbarShortcutHandler extends BaseTool {
12
+ private listeners: Set<KeyPressListener> = new Set([]);
13
+ public constructor(editor: Editor) {
14
+ super(editor.notifier, editor.localization.changeTool);
15
+ }
16
+
17
+ public registerListener(listener: KeyPressListener) {
18
+ this.listeners.add(listener);
19
+ }
20
+
21
+ public removeListener(listener: KeyPressListener) {
22
+ this.listeners.delete(listener);
23
+ }
24
+
25
+ public onKeyPress(event: KeyPressEvent): boolean {
26
+ for (const listener of this.listeners) {
27
+ if (listener(event)) {
28
+ return true;
29
+ }
30
+ }
31
+
32
+ return false;
33
+ }
34
+ }
@@ -19,6 +19,7 @@ describe('UndoRedoShortcut', () => {
19
19
  editor.toolController.dispatchInputEvent({
20
20
  kind: InputEvtType.KeyPressEvent,
21
21
  ctrlKey: true,
22
+ altKey: false,
22
23
  key: 'z',
23
24
  });
24
25
 
@@ -35,6 +36,7 @@ describe('UndoRedoShortcut', () => {
35
36
  editor.toolController.dispatchInputEvent({
36
37
  kind: InputEvtType.KeyPressEvent,
37
38
  ctrlKey: true,
39
+ altKey: false,
38
40
  key: 'z',
39
41
  });
40
42
 
@@ -44,6 +46,7 @@ describe('UndoRedoShortcut', () => {
44
46
  editor.toolController.dispatchInputEvent({
45
47
  kind: InputEvtType.KeyPressEvent,
46
48
  ctrlKey: true,
49
+ altKey: false,
47
50
  key: 'Z',
48
51
  });
49
52
 
package/src/tools/lib.ts CHANGED
@@ -17,3 +17,4 @@ export { default as SelectionTool } from './SelectionTool/SelectionTool';
17
17
  export { default as EraserTool } from './Eraser';
18
18
  export { default as PasteHandler } from './PasteHandler';
19
19
 
20
+ export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
package/src/types.ts CHANGED
@@ -52,14 +52,25 @@ export interface WheelEvt {
52
52
 
53
53
  export interface KeyPressEvent {
54
54
  readonly kind: InputEvtType.KeyPressEvent;
55
+
56
+ // key, as given by an HTML `KeyboardEvent`
55
57
  readonly key: string;
56
- readonly ctrlKey: boolean;
58
+
59
+ // If `ctrlKey` is undefined, that is equivalent to `ctrlKey = false`.
60
+ readonly ctrlKey: boolean|undefined;
61
+
62
+ // If falsey, the `alt` key is not pressed.
63
+ readonly altKey: boolean|undefined;
57
64
  }
58
65
 
59
66
  export interface KeyUpEvent {
60
67
  readonly kind: InputEvtType.KeyUpEvent;
61
68
  readonly key: string;
62
- readonly ctrlKey: boolean;
69
+
70
+ // As in `KeyPressEvent, if `ctrlKey` is undefined, that is equivalent to
71
+ // `ctrlKey = false`.
72
+ readonly ctrlKey: boolean|undefined;
73
+ readonly altKey: boolean|undefined;
63
74
  }
64
75
 
65
76
  export interface CopyEvent {
@@ -101,12 +112,6 @@ export type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt
101
112
  export type EditorNotifier = EventDispatcher<EditorEventType, EditorEventDataType>;
102
113
 
103
114
 
104
-
105
-
106
-
107
-
108
-
109
-
110
115
  export enum EditorEventType {
111
116
  ToolEnabled,
112
117
  ToolDisabled,