js-draw 1.6.0 → 1.7.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 (65) hide show
  1. package/README.md +4 -6
  2. package/dist/Editor.css +30 -4
  3. package/dist/bundle.js +2 -2
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +5 -0
  6. package/dist/cjs/Editor.js +53 -70
  7. package/dist/cjs/components/BackgroundComponent.js +6 -1
  8. package/dist/cjs/components/TextComponent.d.ts +1 -1
  9. package/dist/cjs/components/TextComponent.js +19 -12
  10. package/dist/cjs/image/EditorImage.js +8 -8
  11. package/dist/cjs/localization.d.ts +2 -0
  12. package/dist/cjs/localization.js +2 -0
  13. package/dist/cjs/localizations/comments.js +1 -0
  14. package/dist/cjs/rendering/RenderablePathSpec.js +16 -1
  15. package/dist/cjs/rendering/caching/CacheRecordManager.d.ts +1 -0
  16. package/dist/cjs/rendering/caching/CacheRecordManager.js +18 -0
  17. package/dist/cjs/rendering/caching/RenderingCache.d.ts +1 -0
  18. package/dist/cjs/rendering/caching/RenderingCache.js +3 -0
  19. package/dist/cjs/rendering/renderers/CanvasRenderer.js +3 -2
  20. package/dist/cjs/toolbar/widgets/BaseWidget.js +3 -3
  21. package/dist/cjs/tools/SelectionTool/Selection.d.ts +5 -4
  22. package/dist/cjs/tools/SelectionTool/Selection.js +81 -52
  23. package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
  24. package/dist/cjs/tools/SelectionTool/SelectionHandle.js +8 -3
  25. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
  26. package/dist/cjs/tools/SelectionTool/SelectionTool.js +36 -16
  27. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  28. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +83 -0
  29. package/dist/cjs/tools/SelectionTool/TransformMode.d.ts +10 -3
  30. package/dist/cjs/tools/SelectionTool/TransformMode.js +52 -9
  31. package/dist/cjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
  32. package/dist/cjs/util/listenForKeyboardEventsFrom.js +142 -0
  33. package/dist/cjs/version.js +1 -1
  34. package/dist/mjs/Editor.d.ts +5 -0
  35. package/dist/mjs/Editor.mjs +53 -70
  36. package/dist/mjs/components/BackgroundComponent.mjs +6 -1
  37. package/dist/mjs/components/TextComponent.d.ts +1 -1
  38. package/dist/mjs/components/TextComponent.mjs +19 -12
  39. package/dist/mjs/image/EditorImage.mjs +8 -8
  40. package/dist/mjs/localization.d.ts +2 -0
  41. package/dist/mjs/localization.mjs +2 -0
  42. package/dist/mjs/localizations/comments.mjs +1 -0
  43. package/dist/mjs/rendering/RenderablePathSpec.mjs +16 -1
  44. package/dist/mjs/rendering/caching/CacheRecordManager.d.ts +1 -0
  45. package/dist/mjs/rendering/caching/CacheRecordManager.mjs +18 -0
  46. package/dist/mjs/rendering/caching/RenderingCache.d.ts +1 -0
  47. package/dist/mjs/rendering/caching/RenderingCache.mjs +3 -0
  48. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +3 -2
  49. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +3 -3
  50. package/dist/mjs/tools/SelectionTool/Selection.d.ts +5 -4
  51. package/dist/mjs/tools/SelectionTool/Selection.mjs +81 -52
  52. package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
  53. package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +8 -3
  54. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
  55. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +36 -16
  56. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  57. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +77 -0
  58. package/dist/mjs/tools/SelectionTool/TransformMode.d.ts +10 -3
  59. package/dist/mjs/tools/SelectionTool/TransformMode.mjs +52 -9
  60. package/dist/mjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
  61. package/dist/mjs/util/listenForKeyboardEventsFrom.mjs +140 -0
  62. package/dist/mjs/version.mjs +1 -1
  63. package/docs/img/readme-images/js-draw.png +0 -0
  64. package/package.json +6 -6
  65. package/src/tools/SelectionTool/SelectionTool.scss +62 -9
@@ -6,6 +6,7 @@ import SVGRenderer from '../../rendering/renderers/SVGRenderer.mjs';
6
6
  import Selection from './Selection.mjs';
7
7
  import TextComponent from '../../components/TextComponent.mjs';
8
8
  import { duplicateSelectionShortcut, selectAllKeyboardShortcut, snapToGridKeyboardShortcutId } from '../keybindings.mjs';
9
+ import ToPointerAutoscroller from './ToPointerAutoscroller.mjs';
9
10
  export const cssPrefix = 'selection-tool-';
10
11
  // Allows users to select/transform portions of the `EditorImage`.
11
12
  // With respect to `extend`ing, `SelectionTool` is not stable.
@@ -17,8 +18,19 @@ class SelectionTool extends BaseTool {
17
18
  this.expandingSelectionBox = false;
18
19
  this.shiftKeyPressed = false;
19
20
  this.snapToGrid = false;
21
+ this.lastPointer = null;
20
22
  this.selectionBoxHandlingEvt = false;
21
23
  this.lastSelectedObjects = [];
24
+ this.autoscroller = new ToPointerAutoscroller(editor.viewport, (scrollBy) => {
25
+ editor.dispatch(Viewport.transformBy(Mat33.translation(scrollBy)), false);
26
+ // Update the selection box/content to match the new viewport.
27
+ if (this.lastPointer) {
28
+ // The viewport has changed -- ensure that the screen and canvas positions
29
+ // of the pointer are both correct
30
+ const updatedPointer = this.lastPointer.withScreenPosition(this.lastPointer.screenPos, editor.viewport);
31
+ this.onMainPointerUpdated(updatedPointer);
32
+ }
33
+ });
22
34
  this.handleOverlay = document.createElement('div');
23
35
  editor.createHTMLOverlay(this.handleOverlay);
24
36
  this.handleOverlay.style.display = 'none';
@@ -81,14 +93,22 @@ class SelectionTool extends BaseTool {
81
93
  this.expandingSelectionBox = this.shiftKeyPressed;
82
94
  this.makeSelectionBox(current.canvasPos);
83
95
  }
96
+ else {
97
+ // Only autoscroll if we're transforming an existing selection
98
+ this.autoscroller.start();
99
+ }
84
100
  return true;
85
101
  }
86
102
  return false;
87
103
  }
88
104
  onPointerMove(event) {
105
+ this.onMainPointerUpdated(event.current);
106
+ }
107
+ onMainPointerUpdated(currentPointer) {
108
+ this.lastPointer = currentPointer;
89
109
  if (!this.selectionBox)
90
110
  return;
91
- let currentPointer = event.current;
111
+ this.autoscroller.onPointerMove(currentPointer.screenPos);
92
112
  if (!this.expandingSelectionBox && this.shiftKeyPressed && this.startPoint) {
93
113
  const screenPos = this.editor.viewport.canvasToScreen(this.startPoint);
94
114
  currentPointer = currentPointer.lockedToXYAxesScreen(screenPos, this.editor.viewport);
@@ -103,21 +123,8 @@ class SelectionTool extends BaseTool {
103
123
  this.selectionBox.setToPoint(currentPointer.canvasPos);
104
124
  }
105
125
  }
106
- // Called after a gestureCancel and a pointerUp
107
- onGestureEnd() {
108
- if (!this.selectionBox)
109
- return;
110
- if (!this.selectionBoxHandlingEvt) {
111
- // Expand/shrink the selection rectangle, if applicable
112
- this.selectionBox.resolveToObjects();
113
- this.onSelectionUpdated();
114
- }
115
- else {
116
- this.selectionBox.onDragEnd();
117
- }
118
- this.selectionBoxHandlingEvt = false;
119
- }
120
126
  onPointerUp(event) {
127
+ this.autoscroller.stop();
121
128
  if (!this.selectionBox)
122
129
  return;
123
130
  let currentPointer = event.current;
@@ -136,10 +143,20 @@ class SelectionTool extends BaseTool {
136
143
  ]);
137
144
  }
138
145
  else {
139
- this.onGestureEnd();
146
+ if (!this.selectionBoxHandlingEvt) {
147
+ // Expand/shrink the selection rectangle, if applicable
148
+ this.selectionBox.resolveToObjects();
149
+ this.onSelectionUpdated();
150
+ }
151
+ else {
152
+ this.selectionBox.onDragEnd();
153
+ }
154
+ this.selectionBoxHandlingEvt = false;
155
+ this.lastPointer = null;
140
156
  }
141
157
  }
142
158
  onGestureCancel() {
159
+ this.autoscroller.stop();
143
160
  if (this.selectionBoxHandlingEvt) {
144
161
  this.selectionBox?.onDragCancel();
145
162
  }
@@ -152,6 +169,8 @@ class SelectionTool extends BaseTool {
152
169
  this.prevSelectionBox = null;
153
170
  }
154
171
  this.expandingSelectionBox = false;
172
+ this.lastPointer = null;
173
+ this.selectionBoxHandlingEvt = false;
155
174
  }
156
175
  onSelectionUpdated() {
157
176
  const selectedItemCount = this.selectionBox?.getSelectedItemCount() ?? 0;
@@ -277,6 +296,7 @@ class SelectionTool extends BaseTool {
277
296
  const transform = Mat33.scaling2D(scaleFactor, this.editor.viewport.roundPoint(region.topLeft)).rightMul(Mat33.translation(regionCenter).rightMul(roundedRotationMatrix).rightMul(Mat33.translation(regionCenter.times(-1)))).rightMul(Mat33.translation(this.editor.viewport.roundPoint(Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize))));
278
297
  const oldTransform = this.selectionBox.getTransform();
279
298
  this.selectionBox.setTransform(oldTransform.rightMul(transform));
299
+ this.selectionBox.scrollTo();
280
300
  }
281
301
  if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
282
302
  this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
@@ -0,0 +1,23 @@
1
+ import { Point2, Vec2 } from '@js-draw/math';
2
+ import Viewport from '../../Viewport';
3
+ type ScrollByCallback = (delta: Vec2) => void;
4
+ /**
5
+ * Automatically scrolls the viewport such that the user's pointer is visible.
6
+ */
7
+ export default class ToPointerAutoscroller {
8
+ private viewport;
9
+ private scrollByCanvasDelta;
10
+ private started;
11
+ private updateLoopId;
12
+ private updateLoopRunning;
13
+ private targetPoint;
14
+ private scrollRate;
15
+ constructor(viewport: Viewport, scrollByCanvasDelta: ScrollByCallback);
16
+ private getScrollForPoint;
17
+ start(): void;
18
+ onPointerMove(pointerScreenPosition: Point2): void;
19
+ stop(): void;
20
+ private startUpdateLoop;
21
+ private stopUpdateLoop;
22
+ }
23
+ export {};
@@ -0,0 +1,77 @@
1
+ import { Rect2, Vec2 } from '@js-draw/math';
2
+ import untilNextAnimationFrame from '../../util/untilNextAnimationFrame.mjs';
3
+ /**
4
+ * Automatically scrolls the viewport such that the user's pointer is visible.
5
+ */
6
+ export default class ToPointerAutoscroller {
7
+ constructor(viewport, scrollByCanvasDelta) {
8
+ this.viewport = viewport;
9
+ this.scrollByCanvasDelta = scrollByCanvasDelta;
10
+ this.started = false;
11
+ this.updateLoopId = 0;
12
+ this.updateLoopRunning = false;
13
+ this.targetPoint = null;
14
+ this.scrollRate = 1000; // px/s
15
+ }
16
+ getScrollForPoint(screenPoint) {
17
+ const screenSize = this.viewport.getScreenRectSize();
18
+ const screenRect = new Rect2(0, 0, screenSize.x, screenSize.y);
19
+ // Starts autoscrolling when the cursor is **outside of** this region
20
+ const marginSize = 44;
21
+ const autoscrollBoundary = screenRect.grownBy(-marginSize);
22
+ if (autoscrollBoundary.containsPoint(screenPoint)) {
23
+ return Vec2.zero;
24
+ }
25
+ const closestEdgePoint = autoscrollBoundary.getClosestPointOnBoundaryTo(screenPoint);
26
+ const distToEdge = closestEdgePoint.minus(screenPoint).magnitude();
27
+ const toEdge = closestEdgePoint.minus(screenPoint);
28
+ // Go faster for points further away from the boundary.
29
+ const maximumScaleFactor = 1.25;
30
+ const scaleFactor = Math.min(distToEdge / marginSize, maximumScaleFactor);
31
+ return toEdge.normalizedOrZero().times(scaleFactor);
32
+ }
33
+ start() {
34
+ this.started = true;
35
+ }
36
+ onPointerMove(pointerScreenPosition) {
37
+ if (!this.started) {
38
+ return;
39
+ }
40
+ if (this.getScrollForPoint(pointerScreenPosition) === Vec2.zero) {
41
+ this.stopUpdateLoop();
42
+ }
43
+ else {
44
+ this.targetPoint = pointerScreenPosition;
45
+ this.startUpdateLoop();
46
+ }
47
+ }
48
+ stop() {
49
+ this.targetPoint = null;
50
+ this.started = false;
51
+ this.stopUpdateLoop();
52
+ }
53
+ startUpdateLoop() {
54
+ if (this.updateLoopRunning) {
55
+ return;
56
+ }
57
+ (async () => {
58
+ this.updateLoopId++;
59
+ const currentUpdateLoopId = this.updateLoopId;
60
+ let lastUpdateTime = performance.now();
61
+ while (this.updateLoopId === currentUpdateLoopId && this.targetPoint) {
62
+ this.updateLoopRunning = true;
63
+ const currentTime = performance.now();
64
+ const deltaTimeMs = currentTime - lastUpdateTime;
65
+ const scrollDirection = this.getScrollForPoint(this.targetPoint);
66
+ const screenScrollAmount = scrollDirection.times(this.scrollRate * deltaTimeMs / 1000);
67
+ this.scrollByCanvasDelta(this.viewport.screenToCanvasTransform.transformVec3(screenScrollAmount));
68
+ lastUpdateTime = currentTime;
69
+ await untilNextAnimationFrame();
70
+ }
71
+ this.updateLoopRunning = false;
72
+ })();
73
+ }
74
+ stopUpdateLoop() {
75
+ this.updateLoopId++;
76
+ }
77
+ }
@@ -9,26 +9,33 @@ export declare class DragTransformer {
9
9
  constructor(editor: Editor, selection: Selection);
10
10
  onDragStart(startPoint: Vec3): void;
11
11
  onDragUpdate(canvasPos: Vec3): void;
12
- onDragEnd(): void;
12
+ onDragEnd(): void | Promise<void>;
13
13
  }
14
14
  export declare class ResizeTransformer {
15
15
  private readonly editor;
16
16
  private selection;
17
17
  private mode;
18
18
  private dragStartPoint;
19
+ private transformOrigin;
20
+ private scaleRate;
19
21
  constructor(editor: Editor, selection: Selection);
20
22
  onDragStart(startPoint: Vec3, mode: ResizeMode): void;
23
+ private computeOriginAndScaleRate;
21
24
  onDragUpdate(canvasPos: Vec3): void;
22
- onDragEnd(): void;
25
+ onDragEnd(): void | Promise<void>;
23
26
  }
24
27
  export declare class RotateTransformer {
25
28
  private readonly editor;
26
29
  private selection;
27
30
  private startAngle;
31
+ private targetRotation;
32
+ private maximumDistFromStart;
33
+ private startPoint;
28
34
  constructor(editor: Editor, selection: Selection);
29
35
  private getAngle;
30
36
  private roundAngle;
31
37
  onDragStart(startPoint: Vec3): void;
38
+ private setRotationTo;
32
39
  onDragUpdate(canvasPos: Vec3): void;
33
- onDragEnd(): void;
40
+ onDragEnd(): void | Promise<void>;
34
41
  }
@@ -15,7 +15,7 @@ export class DragTransformer {
15
15
  this.selection.setTransform(Mat33.translation(delta));
16
16
  }
17
17
  onDragEnd() {
18
- this.selection.finalizeTransform();
18
+ return this.selection.finalizeTransform();
19
19
  }
20
20
  }
21
21
  export class ResizeTransformer {
@@ -28,6 +28,32 @@ export class ResizeTransformer {
28
28
  this.selection.setTransform(Mat33.identity);
29
29
  this.mode = mode;
30
30
  this.dragStartPoint = startPoint;
31
+ this.computeOriginAndScaleRate();
32
+ }
33
+ computeOriginAndScaleRate() {
34
+ // Store the index of the furthest corner from startPoint. We'll use that
35
+ // to determine where the transform considers (0, 0) (where we scale from).
36
+ const selectionRect = this.selection.preTransformRegion;
37
+ const selectionBoxCorners = selectionRect.corners;
38
+ let largestDistSquared = 0;
39
+ for (let i = 0; i < selectionBoxCorners.length; i++) {
40
+ const currentCorner = selectionBoxCorners[i];
41
+ const distSquaredToCurrent = this.dragStartPoint.minus(currentCorner).magnitudeSquared();
42
+ if (distSquaredToCurrent > largestDistSquared) {
43
+ largestDistSquared = distSquaredToCurrent;
44
+ this.transformOrigin = currentCorner;
45
+ }
46
+ }
47
+ // Determine whether moving the mouse to the right increases or decreases the width.
48
+ let widthScaleRate = 1;
49
+ let heightScaleRate = 1;
50
+ if (this.transformOrigin.x > selectionRect.center.x) {
51
+ widthScaleRate = -1;
52
+ }
53
+ if (this.transformOrigin.y > selectionRect.center.y) {
54
+ heightScaleRate = -1;
55
+ }
56
+ this.scaleRate = Vec2.of(widthScaleRate, heightScaleRate);
31
57
  }
32
58
  onDragUpdate(canvasPos) {
33
59
  const canvasDelta = canvasPos.minus(this.dragStartPoint);
@@ -35,11 +61,11 @@ export class ResizeTransformer {
35
61
  const origHeight = this.selection.preTransformRegion.height;
36
62
  let scale = Vec2.of(1, 1);
37
63
  if (this.mode === ResizeMode.HorizontalOnly) {
38
- const newWidth = origWidth + canvasDelta.x;
64
+ const newWidth = origWidth + canvasDelta.x * this.scaleRate.x;
39
65
  scale = Vec2.of(newWidth / origWidth, scale.y);
40
66
  }
41
67
  if (this.mode === ResizeMode.VerticalOnly) {
42
- const newHeight = origHeight + canvasDelta.y;
68
+ const newHeight = origHeight + canvasDelta.y * this.scaleRate.y;
43
69
  scale = Vec2.of(scale.x, newHeight / origHeight);
44
70
  }
45
71
  if (this.mode === ResizeMode.Both) {
@@ -51,12 +77,12 @@ export class ResizeTransformer {
51
77
  // long decimal representations => large file sizes.
52
78
  scale = scale.map(component => Viewport.roundScaleRatio(component, 2));
53
79
  if (scale.x !== 0 && scale.y !== 0) {
54
- const origin = this.editor.viewport.roundPoint(this.selection.preTransformRegion.topLeft);
80
+ const origin = this.editor.viewport.roundPoint(this.transformOrigin);
55
81
  this.selection.setTransform(Mat33.scaling2D(scale, origin));
56
82
  }
57
83
  }
58
84
  onDragEnd() {
59
- this.selection.finalizeTransform();
85
+ return this.selection.finalizeTransform();
60
86
  }
61
87
  }
62
88
  export class RotateTransformer {
@@ -64,6 +90,8 @@ export class RotateTransformer {
64
90
  this.editor = editor;
65
91
  this.selection = selection;
66
92
  this.startAngle = 0;
93
+ this.targetRotation = 0;
94
+ this.maximumDistFromStart = 0;
67
95
  }
68
96
  getAngle(canvasPoint) {
69
97
  const selectionCenter = this.selection.preTransformRegion.center;
@@ -76,14 +104,16 @@ export class RotateTransformer {
76
104
  return Math.round(angle * roundingFactor) / roundingFactor;
77
105
  }
78
106
  onDragStart(startPoint) {
107
+ this.startPoint = startPoint;
79
108
  this.selection.setTransform(Mat33.identity);
80
109
  this.startAngle = this.getAngle(startPoint);
110
+ this.maximumDistFromStart = 0;
111
+ this.targetRotation = 0;
81
112
  }
82
- onDragUpdate(canvasPos) {
83
- const targetRotation = this.roundAngle(this.getAngle(canvasPos) - this.startAngle);
113
+ setRotationTo(angle) {
84
114
  // Transform in canvas space
85
115
  const canvasSelCenter = this.editor.viewport.roundPoint(this.selection.preTransformRegion.center);
86
- const unrounded = Mat33.zRotation(targetRotation);
116
+ const unrounded = Mat33.zRotation(angle);
87
117
  const roundedRotationTransform = unrounded.mapEntries(entry => Viewport.roundScaleRatio(entry));
88
118
  const fullRoundedTransform = Mat33
89
119
  .translation(canvasSelCenter)
@@ -91,7 +121,20 @@ export class RotateTransformer {
91
121
  .rightMul(Mat33.translation(canvasSelCenter.times(-1)));
92
122
  this.selection.setTransform(fullRoundedTransform);
93
123
  }
124
+ onDragUpdate(canvasPos) {
125
+ this.targetRotation = this.roundAngle(this.getAngle(canvasPos) - this.startAngle);
126
+ this.setRotationTo(this.targetRotation);
127
+ const distFromStart = canvasPos.minus(this.startPoint).magnitude();
128
+ if (distFromStart > this.maximumDistFromStart) {
129
+ this.maximumDistFromStart = distFromStart;
130
+ }
131
+ }
94
132
  onDragEnd() {
95
- this.selection.finalizeTransform();
133
+ // Anything less than this is considered a click
134
+ const clickThreshold = 15;
135
+ if (this.maximumDistFromStart < clickThreshold && this.targetRotation === 0) {
136
+ this.setRotationTo(-Math.PI / 2);
137
+ }
138
+ return this.selection.finalizeTransform();
96
139
  }
97
140
  }
@@ -0,0 +1,16 @@
1
+ interface Callbacks {
2
+ filter(event: KeyboardEvent): boolean;
3
+ handleKeyDown(event: KeyboardEvent): void;
4
+ handleKeyUp(event: KeyboardEvent): void;
5
+ }
6
+ /**
7
+ * Calls `callbacks` when different keys are known to be pressed.
8
+ *
9
+ * `filter` can be used to ignore events.
10
+ *
11
+ * This includes keys that didn't trigger a keydown or keyup event, but did cause
12
+ * shiftKey/altKey/metaKey/etc. properties to change on other events (e.g. mousemove
13
+ * events). Artifical events are created for these changes and sent to `callbacks`.
14
+ */
15
+ declare const listenForKeyboardEventsFrom: (elem: HTMLElement, callbacks: Callbacks) => void;
16
+ export default listenForKeyboardEventsFrom;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Calls `callbacks` when different keys are known to be pressed.
3
+ *
4
+ * `filter` can be used to ignore events.
5
+ *
6
+ * This includes keys that didn't trigger a keydown or keyup event, but did cause
7
+ * shiftKey/altKey/metaKey/etc. properties to change on other events (e.g. mousemove
8
+ * events). Artifical events are created for these changes and sent to `callbacks`.
9
+ */
10
+ const listenForKeyboardEventsFrom = (elem, callbacks) => {
11
+ // Track which keys are down so we can release them when the element
12
+ // loses focus. This is particularly important for keys like Control
13
+ // that can trigger shortcuts that cause the editor to lose focus before
14
+ // the keyup event is triggered.
15
+ let keysDown = [];
16
+ // Return whether two objects that are similar to keyboard events represent the
17
+ // same key.
18
+ const keyEventsMatch = (a, b) => {
19
+ return a.key === b.key && a.code === b.code;
20
+ };
21
+ const isKeyDown = (keyEvent) => {
22
+ return keysDown.some(other => keyEventsMatch(other, keyEvent));
23
+ };
24
+ const keyEventToRecord = (event) => {
25
+ return {
26
+ code: event.code,
27
+ key: event.key,
28
+ ctrlKey: event.ctrlKey,
29
+ altKey: event.altKey,
30
+ shiftKey: event.shiftKey,
31
+ metaKey: event.metaKey,
32
+ };
33
+ };
34
+ const handleKeyEvent = (htmlEvent) => {
35
+ if (htmlEvent.type === 'keydown') {
36
+ // Add event to the list of keys that are down (so long as it
37
+ // isn't a duplicate).
38
+ if (!isKeyDown(htmlEvent)) {
39
+ // Destructructring, then pushing seems to cause
40
+ // data loss. Copy properties individually:
41
+ keysDown.push(keyEventToRecord(htmlEvent));
42
+ }
43
+ if (!callbacks.filter(htmlEvent)) {
44
+ return;
45
+ }
46
+ callbacks.handleKeyDown(htmlEvent);
47
+ }
48
+ else { // keyup
49
+ console.assert(htmlEvent.type === 'keyup');
50
+ // Remove the key from keysDown -- it's no longer down.
51
+ keysDown = keysDown.filter(event => {
52
+ const matches = keyEventsMatch(event, htmlEvent);
53
+ return !matches;
54
+ });
55
+ if (!callbacks.filter(htmlEvent)) {
56
+ return;
57
+ }
58
+ callbacks.handleKeyUp(htmlEvent);
59
+ }
60
+ };
61
+ elem.addEventListener('keydown', htmlEvent => {
62
+ handleKeyEvent(htmlEvent);
63
+ });
64
+ elem.addEventListener('keyup', htmlEvent => {
65
+ handleKeyEvent(htmlEvent);
66
+ });
67
+ elem.addEventListener('focusout', (focusEvent) => {
68
+ const stillHasFocus = focusEvent.relatedTarget && elem.contains(focusEvent.relatedTarget);
69
+ if (!stillHasFocus) {
70
+ for (const event of keysDown) {
71
+ callbacks.handleKeyUp(new KeyboardEvent('keyup', {
72
+ ...event,
73
+ }));
74
+ }
75
+ keysDown = [];
76
+ }
77
+ });
78
+ const fireArtificalEventsBasedOn = (htmlEvent) => {
79
+ let wasShiftDown = false;
80
+ let wasCtrlDown = false;
81
+ let wasAltDown = false;
82
+ let wasMetaDown = false;
83
+ for (const otherEvent of keysDown) {
84
+ const code = otherEvent.code;
85
+ wasShiftDown ||= !!code.match(/^Shift(Left|Right)$/);
86
+ wasCtrlDown ||= !!code.match(/^Control(Left|Right)$/);
87
+ wasAltDown ||= !!code.match(/^Alt(Left|Right)$/);
88
+ wasMetaDown ||= !!code.match(/^Meta(Left|Right)$/);
89
+ }
90
+ const eventName = (isDown) => {
91
+ if (isDown) {
92
+ return 'keydown';
93
+ }
94
+ else {
95
+ return 'keyup';
96
+ }
97
+ };
98
+ const eventInitDefaults = {
99
+ shiftKey: htmlEvent.shiftKey,
100
+ altKey: htmlEvent.altKey,
101
+ metaKey: htmlEvent.metaKey,
102
+ ctrlKey: htmlEvent.ctrlKey,
103
+ };
104
+ if (htmlEvent.shiftKey !== wasShiftDown) {
105
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.shiftKey), {
106
+ ...eventInitDefaults,
107
+ key: 'Shift',
108
+ code: 'ShiftLeft',
109
+ }));
110
+ }
111
+ if (htmlEvent.altKey !== wasAltDown) {
112
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.altKey), {
113
+ ...eventInitDefaults,
114
+ key: 'Alt',
115
+ code: 'AltLeft',
116
+ }));
117
+ }
118
+ if (htmlEvent.ctrlKey !== wasCtrlDown) {
119
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.ctrlKey), {
120
+ ...eventInitDefaults,
121
+ key: 'Control',
122
+ code: 'ControlLeft',
123
+ }));
124
+ }
125
+ if (htmlEvent.metaKey !== wasMetaDown) {
126
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.metaKey), {
127
+ ...eventInitDefaults,
128
+ key: 'Meta',
129
+ code: 'MetaLeft',
130
+ }));
131
+ }
132
+ };
133
+ elem.addEventListener('mousedown', (htmlEvent) => {
134
+ fireArtificalEventsBasedOn(htmlEvent);
135
+ });
136
+ elem.addEventListener('mousemove', (htmlEvent) => {
137
+ fireArtificalEventsBasedOn(htmlEvent);
138
+ });
139
+ };
140
+ export default listenForKeyboardEventsFrom;
@@ -1,3 +1,3 @@
1
1
  export default {
2
- number: '1.6.0',
2
+ number: '1.7.0',
3
3
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -64,13 +64,13 @@
64
64
  "postpack": "ts-node tools/copyREADME.ts revert"
65
65
  },
66
66
  "dependencies": {
67
- "@js-draw/math": "^1.6.0",
67
+ "@js-draw/math": "^1.7.0",
68
68
  "@melloware/coloris": "0.21.0"
69
69
  },
70
70
  "devDependencies": {
71
- "@js-draw/build-tool": "^1.4.0",
72
- "@types/jest": "29.5.3",
73
- "@types/jsdom": "21.1.1"
71
+ "@js-draw/build-tool": "^1.7.0",
72
+ "@types/jest": "29.5.5",
73
+ "@types/jsdom": "21.1.3"
74
74
  },
75
75
  "bugs": {
76
76
  "url": "https://github.com/personalizedrefrigerator/js-draw/issues"
@@ -86,5 +86,5 @@
86
86
  "freehand",
87
87
  "svg"
88
88
  ],
89
- "gitHead": "c2278b6819ca0e464e4e7d2c3c2045e3394b1fe8"
89
+ "gitHead": "e7d8a68e6a5ae3063de9c1f033ed8f08c567b393"
90
90
  }
@@ -3,27 +3,51 @@
3
3
  background-color: var(--selection-background-color);
4
4
  opacity: 0.5;
5
5
  overflow: visible;
6
- position: absolute;
7
6
  }
8
7
 
9
8
  .selection-tool-handle {
10
- border: 1px solid var(--foreground-color-1);
11
- background: var(--background-color-1);
12
9
  position: absolute;
13
-
14
10
  box-sizing: border-box;
15
- padding: 3px;
16
11
 
17
- & .icon {
12
+ // Center content
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+
17
+ // Maximum size of the visible region (make the handle slightly larger
18
+ // so that the resize cursor is visible everywhere in the actual selection
19
+ // box).
20
+ --max-size: 17px;
21
+
22
+ .selection-tool-content {
23
+ border: 1px solid var(--foreground-color-1);
24
+ background: var(--background-color-1);
25
+ box-sizing: border-box;
26
+
27
+ max-width: var(--max-size);
28
+ max-height: var(--max-size);
18
29
  width: 100%;
19
30
  height: 100%;
31
+
32
+ display: flex;
33
+ justify-content: center;
34
+ align-items: center;
35
+
36
+ padding: 3px;
37
+ .icon {
38
+ width: 100%;
39
+ height: 100%;
40
+ }
20
41
  }
21
42
 
22
- &.selection-tool-circle {
43
+ &.selection-tool-circle .selection-tool-content {
23
44
  border-radius: 100%;
24
45
  }
25
46
 
26
47
  &.selection-tool-rotate {
48
+ // Shrink less if a rotation handle
49
+ --max-size: 28px;
50
+
27
51
  cursor: grab;
28
52
  }
29
53
  }
@@ -57,8 +81,37 @@
57
81
  }
58
82
 
59
83
  .overlay.handleOverlay {
60
- height: 0;
61
- overflow: visible;
84
+ touch-action: none;
85
+
86
+ // When expanding a selection with shift+click&drag, multiple selection boxes
87
+ // can be present in the same handleOverlay. As such, so that other overlayed
88
+ // selection boxes are in the correct place, the outer container needs to have
89
+ // zero height.
90
+ //
91
+ // This is in addition to the overlay container, which needs zero height to prevent
92
+ // other overlay containers from being affected by its size.
93
+ &, .selection-tool-selection-outer-container {
94
+ height: 0;
95
+ overflow: visible;
96
+ }
97
+
98
+ .selection-tool-selection-inner-container {
99
+ width: var(--editor-current-display-width-px);
100
+ height: var(--editor-current-display-height-px);
101
+ overflow: hidden;
102
+
103
+ // Disable pointer events: If the parent (or the container) has
104
+ // captured pointers and the container is removed, this prevents
105
+ // us from receiving the following events (e.g. in Firefox).
106
+ pointer-events: none;
107
+
108
+ & > * {
109
+ // We *do* want pointer events for handles and the background. This
110
+ // allows the mouse cursor to change shape when hovering over resize
111
+ // handles.
112
+ pointer-events: all;
113
+ }
114
+ }
62
115
  }
63
116
 
64
117
  @keyframes selection-duplicated-animation {