js-draw 1.11.0 → 1.11.2

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 (40) hide show
  1. package/dist/Editor.css +6 -2
  2. package/dist/bundle.js +3 -3
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.d.ts +7 -0
  5. package/dist/cjs/Editor.js +23 -6
  6. package/dist/cjs/components/BackgroundComponent.d.ts +1 -1
  7. package/dist/cjs/components/ImageComponent.d.ts +1 -1
  8. package/dist/cjs/components/SVGGlobalAttributesObject.d.ts +1 -1
  9. package/dist/cjs/components/Stroke.d.ts +1 -1
  10. package/dist/cjs/rendering/Display.js +3 -1
  11. package/dist/cjs/rendering/renderers/DummyRenderer.d.ts +1 -0
  12. package/dist/cjs/rendering/renderers/DummyRenderer.js +3 -0
  13. package/dist/cjs/tools/FindTool.js +1 -1
  14. package/dist/cjs/tools/Pen.js +8 -2
  15. package/dist/cjs/tools/SelectionTool/SelectionTool.js +16 -2
  16. package/dist/cjs/tools/localization.d.ts +2 -0
  17. package/dist/cjs/tools/localization.js +2 -0
  18. package/dist/cjs/util/listenForKeyboardEventsFrom.d.ts +5 -0
  19. package/dist/cjs/util/listenForKeyboardEventsFrom.js +5 -1
  20. package/dist/cjs/version.js +1 -1
  21. package/dist/mjs/Editor.d.ts +7 -0
  22. package/dist/mjs/Editor.mjs +23 -6
  23. package/dist/mjs/components/BackgroundComponent.d.ts +1 -1
  24. package/dist/mjs/components/ImageComponent.d.ts +1 -1
  25. package/dist/mjs/components/SVGGlobalAttributesObject.d.ts +1 -1
  26. package/dist/mjs/components/Stroke.d.ts +1 -1
  27. package/dist/mjs/rendering/Display.mjs +3 -1
  28. package/dist/mjs/rendering/renderers/DummyRenderer.d.ts +1 -0
  29. package/dist/mjs/rendering/renderers/DummyRenderer.mjs +3 -0
  30. package/dist/mjs/tools/FindTool.mjs +1 -1
  31. package/dist/mjs/tools/Pen.mjs +8 -2
  32. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +16 -2
  33. package/dist/mjs/tools/localization.d.ts +2 -0
  34. package/dist/mjs/tools/localization.mjs +2 -0
  35. package/dist/mjs/util/listenForKeyboardEventsFrom.d.ts +5 -0
  36. package/dist/mjs/util/listenForKeyboardEventsFrom.mjs +5 -1
  37. package/dist/mjs/version.mjs +1 -1
  38. package/package.json +5 -5
  39. package/src/toolbar/AbstractToolbar.scss +3 -2
  40. package/src/toolbar/widgets/components/makeColorInput.scss +8 -0
@@ -327,6 +327,17 @@ export class Editor {
327
327
  }
328
328
  return res;
329
329
  }
330
+ /**
331
+ * A protected method that can override setPointerCapture in environments where it may fail
332
+ * (e.g. with synthetic events). @internal
333
+ */
334
+ setPointerCapture(target, pointerId) {
335
+ target.setPointerCapture(pointerId);
336
+ }
337
+ /** Can be overridden in a testing environment to handle synthetic events. @internal */
338
+ releasePointerCapture(target, pointerId) {
339
+ target.releasePointerCapture(pointerId);
340
+ }
330
341
  /**
331
342
  * Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
332
343
  * as the content of the editor.
@@ -337,7 +348,7 @@ export class Editor {
337
348
  if (eventType === 'pointerdown') {
338
349
  const pointer = Pointer.ofEvent(evt, true, this.viewport, eventsRelativeTo);
339
350
  this.pointers[pointer.id] = pointer;
340
- eventTarget.setPointerCapture(pointer.id);
351
+ this.setPointerCapture(eventTarget, pointer.id);
341
352
  const event = {
342
353
  kind: InputEvtType.PointerDownEvt,
343
354
  current: pointer,
@@ -374,7 +385,7 @@ export class Editor {
374
385
  return false;
375
386
  }
376
387
  this.pointers[pointer.id] = pointer;
377
- eventTarget.releasePointerCapture(pointer.id);
388
+ this.releasePointerCapture(eventTarget, pointer.id);
378
389
  if (this.toolController.dispatchInputEvent({
379
390
  kind: InputEvtType.PointerUpEvt,
380
391
  current: pointer,
@@ -551,7 +562,8 @@ export class Editor {
551
562
  return false;
552
563
  }
553
564
  // Position of the current event.
554
- const currentPos = Vec2.of(event.pageX, event.pageY);
565
+ // jsdom doesn't seem to support pageX/pageY -- use clientX/clientY if unavailable
566
+ const currentPos = Vec2.of(event.pageX ?? event.clientX, event.pageY ?? event.clientY);
555
567
  const pointerId = event.pointerId ?? 0;
556
568
  // Whether to send the current event to the editor
557
569
  let sendToEditor = true;
@@ -561,9 +573,10 @@ export class Editor {
561
573
  gestureData[pointerId] = {
562
574
  eventBuffer: [[eventName, event]],
563
575
  startPoint: currentPos,
576
+ hasMovedSignificantly: false,
564
577
  };
565
578
  // Capture the pointer so we receive future events even if the overlay is hidden.
566
- elem.setPointerCapture(event.pointerId);
579
+ this.setPointerCapture(elem, event.pointerId);
567
580
  // Don't send to the editor.
568
581
  sendToEditor = false;
569
582
  }
@@ -573,7 +586,7 @@ export class Editor {
573
586
  // Skip if the pointer hasn't moved enough to not be a "click".
574
587
  const strokeStartThreshold = 10;
575
588
  const isWithinClickThreshold = gestureStartPos && currentPos.minus(gestureStartPos).magnitude() < strokeStartThreshold;
576
- if (isWithinClickThreshold) {
589
+ if (isWithinClickThreshold && !gestureData[pointerId].hasMovedSignificantly) {
577
590
  eventBuffer.push([eventName, event]);
578
591
  sendToEditor = false;
579
592
  }
@@ -583,6 +596,7 @@ export class Editor {
583
596
  this.handleHTMLPointerEvent(eventName, event);
584
597
  }
585
598
  gestureData[pointerId].eventBuffer = [];
599
+ gestureData[pointerId].hasMovedSignificantly = true;
586
600
  sendToEditor = true;
587
601
  }
588
602
  }
@@ -595,7 +609,7 @@ export class Editor {
595
609
  // pointercancel event.
596
610
  else if ((eventName === 'pointerup' || eventName === 'pointercancel')
597
611
  && gestureData[pointerId] && gestureData[pointerId].eventBuffer.length > 0) {
598
- elem.releasePointerCapture(event.pointerId);
612
+ this.releasePointerCapture(elem, event.pointerId);
599
613
  // Don't send to the editor.
600
614
  sendToEditor = false;
601
615
  delete gestureData[pointerId];
@@ -643,6 +657,9 @@ export class Editor {
643
657
  handleKeyUp: (htmlEvent) => {
644
658
  this.handleHTMLKeyUpEvent(htmlEvent);
645
659
  },
660
+ getHandlesKeyEventsFrom: (element) => {
661
+ return this.eventListenerTargets.includes(element);
662
+ },
646
663
  });
647
664
  // Allow drop.
648
665
  elem.ondragover = evt => {
@@ -64,5 +64,5 @@ export default class BackgroundComponent extends AbstractComponent implements Re
64
64
  protected applyTransformation(_affineTransfm: Mat33): void;
65
65
  description(localizationTable: ImageComponentLocalization): string;
66
66
  protected createClone(): AbstractComponent;
67
- static deserializeFromJSON(json: any): BackgroundComponent;
67
+ static deserializeFromJSON(this: void, json: any): BackgroundComponent;
68
68
  }
@@ -37,5 +37,5 @@ export default class ImageComponent extends AbstractComponent {
37
37
  height: number;
38
38
  transform: Mat33Array;
39
39
  };
40
- static deserializeFromJSON(data: any): ImageComponent;
40
+ static deserializeFromJSON(this: void, data: any): ImageComponent;
41
41
  }
@@ -15,6 +15,6 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
15
15
  protected createClone(): SVGGlobalAttributesObject;
16
16
  description(localization: ImageComponentLocalization): string;
17
17
  protected serializeToJSON(): string | null;
18
- static deserializeFromString(_data: string): AbstractComponent;
18
+ static deserializeFromString(this: void, _data: string): AbstractComponent;
19
19
  }
20
20
  export {};
@@ -88,5 +88,5 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
88
88
  path: string;
89
89
  }[];
90
90
  /** @internal */
91
- static deserializeFromJSON(json: any): Stroke;
91
+ static deserializeFromJSON(this: void, json: any): Stroke;
92
92
  }
@@ -70,7 +70,9 @@ export default class Display {
70
70
  },
71
71
  blockResolution: cacheBlockResolution,
72
72
  cacheSize: 600 * 600 * 4 * 90,
73
- maxScale: 1.3,
73
+ // On higher resolution displays, don't scale cache blocks as much to decrease blurriness.
74
+ // TODO: Decrease the minimum cache scale as well.
75
+ maxScale: Math.max(1, 1.3 / window.devicePixelRatio),
74
76
  // Require about 20 strokes with 4 parts each to cache an image in one of the
75
77
  // parts of the cache grid.
76
78
  minProportionalRenderTimePerCache: 20 * 4,
@@ -29,4 +29,5 @@ export default class DummyRenderer extends AbstractRenderer {
29
29
  isTooSmallToRender(_rect: Rect2): boolean;
30
30
  canRenderFromWithoutDataLoss(other: AbstractRenderer): boolean;
31
31
  renderFromOtherOfSameType(transform: Mat33, other: AbstractRenderer): void;
32
+ toString(): string;
32
33
  }
@@ -103,4 +103,7 @@ export default class DummyRenderer extends AbstractRenderer {
103
103
  return transform.transformVec2(point);
104
104
  }));
105
105
  }
106
+ toString() {
107
+ return '[DummyRenderer]';
108
+ }
106
109
  }
@@ -34,7 +34,7 @@ export default class FindTool extends BaseTool {
34
34
  }
35
35
  if (matchIdx < matches.length) {
36
36
  const undoable = false;
37
- this.editor.dispatch(this.editor.viewport.zoomTo(matches[matchIdx], true, true), undoable);
37
+ void this.editor.dispatch(this.editor.viewport.zoomTo(matches[matchIdx], true, true), undoable);
38
38
  this.editor.announceForAccessibility(this.editor.localization.focusedFoundText(matchIdx + 1, matches.length));
39
39
  }
40
40
  }
@@ -97,8 +97,8 @@ export default class Pen extends BaseTool {
97
97
  this.currentDeviceType = current.device;
98
98
  if (this.shapeAutocompletionEnabled) {
99
99
  const stationaryDetectionConfig = {
100
- maxSpeed: 5,
101
- maxRadius: 10,
100
+ maxSpeed: 8.5,
101
+ maxRadius: 11,
102
102
  minTimeSeconds: 0.5, // s
103
103
  };
104
104
  this.stationaryDetector = new StationaryPenDetector(current, stationaryDetectionConfig, pointer => this.autocorrectShape(pointer));
@@ -141,6 +141,7 @@ export default class Pen extends BaseTool {
141
141
  if (this.autocorrectedShape) {
142
142
  this.removedAutocorrectedShapeTime = performance.now();
143
143
  this.autocorrectedShape = null;
144
+ this.editor.announceForAccessibility(this.editor.localization.autocorrectionCanceled);
144
145
  }
145
146
  }
146
147
  }
@@ -192,6 +193,8 @@ export default class Pen extends BaseTool {
192
193
  if (bboxArea === 0 || !isFinite(bboxArea)) {
193
194
  return;
194
195
  }
196
+ const shapeDescription = correctedShape.description(this.editor.localization);
197
+ this.editor.announceForAccessibility(this.editor.localization.autocorrectedTo(shapeDescription));
195
198
  this.autocorrectedShape = correctedShape;
196
199
  this.lastAutocorrectedShape = correctedShape;
197
200
  this.previewStroke();
@@ -206,6 +209,9 @@ export default class Pen extends BaseTool {
206
209
  const stroke = this.autocorrectedShape ?? this.builder.build();
207
210
  this.previewStroke();
208
211
  if (stroke.getBBox().area > 0) {
212
+ if (stroke === this.autocorrectedShape) {
213
+ this.editor.announceForAccessibility(this.editor.localization.autocorrectedTo(stroke.description(this.editor.localization)));
214
+ }
209
215
  const canFlatten = true;
210
216
  const action = EditorImage.addElement(stroke, canFlatten);
211
217
  this.editor.dispatch(action);
@@ -231,9 +231,11 @@ class SelectionTool extends BaseTool {
231
231
  // Pass it to another tool, if apliccable.
232
232
  return false;
233
233
  }
234
- else if (event.key === 'Shift') {
234
+ else if (event.shiftKey || event.key === 'Shift') {
235
235
  this.shiftKeyPressed = true;
236
- return true;
236
+ if (event.key === 'Shift') {
237
+ return true;
238
+ }
237
239
  }
238
240
  let rotationSteps = 0;
239
241
  let xTranslateSteps = 0;
@@ -335,6 +337,14 @@ class SelectionTool extends BaseTool {
335
337
  }
336
338
  return true;
337
339
  }
340
+ // Here, we check if shiftKey === false because, as of this writing,
341
+ // evt.shiftKey is an optional property. Being falsey could just mean
342
+ // that it wasn't set.
343
+ if (evt.shiftKey === false) {
344
+ this.shiftKeyPressed = false;
345
+ // Don't return immediately -- event may be otherwise handled
346
+ }
347
+ // Also check for key === 'Shift' (for the case where shiftKey is undefined)
338
348
  if (evt.key === 'Shift') {
339
349
  this.shiftKeyPressed = false;
340
350
  return true;
@@ -379,7 +389,11 @@ class SelectionTool extends BaseTool {
379
389
  return true;
380
390
  }
381
391
  setEnabled(enabled) {
392
+ const wasEnabled = this.isEnabled();
382
393
  super.setEnabled(enabled);
394
+ if (wasEnabled === enabled) {
395
+ return;
396
+ }
383
397
  // Clear the selection
384
398
  this.selectionBox?.cancelSelection();
385
399
  this.onSelectionUpdated();
@@ -9,6 +9,8 @@ export interface ToolLocalization {
9
9
  undoRedoTool: string;
10
10
  pipetteTool: string;
11
11
  rightClickDragPanTool: string;
12
+ autocorrectedTo: (description: string) => string;
13
+ autocorrectionCanceled: string;
12
14
  textTool: string;
13
15
  enterTextToInsert: string;
14
16
  changeTool: string;
@@ -9,6 +9,8 @@ export const defaultToolLocalization = {
9
9
  rightClickDragPanTool: 'Right-click drag',
10
10
  pipetteTool: 'Pick color from screen',
11
11
  keyboardPanZoom: 'Keyboard pan/zoom shortcuts',
12
+ autocorrectedTo: (strokeDescription) => `Autocorrected to ${strokeDescription}`,
13
+ autocorrectionCanceled: 'Autocorrect cancelled',
12
14
  textTool: 'Text',
13
15
  enterTextToInsert: 'Text to insert',
14
16
  changeTool: 'Change tool',
@@ -2,6 +2,11 @@ interface Callbacks {
2
2
  filter(event: KeyboardEvent): boolean;
3
3
  handleKeyDown(event: KeyboardEvent): void;
4
4
  handleKeyUp(event: KeyboardEvent): void;
5
+ /**
6
+ * Should return `true` iff `source` is also registered as an event listener source.
7
+ * If `false` and focus leaves the original source, keyup events are fired.
8
+ */
9
+ getHandlesKeyEventsFrom(source: Node): boolean;
5
10
  }
6
11
  /**
7
12
  * Calls `callbacks` when different keys are known to be pressed.
@@ -65,7 +65,11 @@ const listenForKeyboardEventsFrom = (elem, callbacks) => {
65
65
  handleKeyEvent(htmlEvent);
66
66
  });
67
67
  elem.addEventListener('focusout', (focusEvent) => {
68
- const stillHasFocus = focusEvent.relatedTarget && elem.contains(focusEvent.relatedTarget);
68
+ let stillHasFocus = false;
69
+ if (focusEvent.relatedTarget) {
70
+ const relatedTarget = focusEvent.relatedTarget;
71
+ stillHasFocus = elem.contains(relatedTarget) || callbacks.getHandlesKeyEventsFrom(relatedTarget);
72
+ }
69
73
  if (!stillHasFocus) {
70
74
  for (const event of keysDown) {
71
75
  callbacks.handleKeyUp(new KeyboardEvent('keyup', {
@@ -1,3 +1,3 @@
1
1
  export default {
2
- number: '1.11.0',
2
+ number: '1.11.2',
3
3
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
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,11 +64,11 @@
64
64
  "postpack": "ts-node tools/copyREADME.ts revert"
65
65
  },
66
66
  "dependencies": {
67
- "@js-draw/math": "^1.10.0",
68
- "@melloware/coloris": "0.21.0"
67
+ "@js-draw/math": "^1.11.1",
68
+ "@melloware/coloris": "0.22.0"
69
69
  },
70
70
  "devDependencies": {
71
- "@js-draw/build-tool": "^1.7.0",
71
+ "@js-draw/build-tool": "^1.11.1",
72
72
  "@types/jest": "29.5.5",
73
73
  "@types/jsdom": "21.1.3"
74
74
  },
@@ -86,5 +86,5 @@
86
86
  "freehand",
87
87
  "svg"
88
88
  ],
89
- "gitHead": "01fc3dc7bdbc9f456705bf08d9c30b4549122d97"
89
+ "gitHead": "e1d593def957ec85633ff89166f9af25ae03f862"
90
90
  }
@@ -45,7 +45,8 @@
45
45
  }
46
46
 
47
47
  .toolbar-button.disabled {
48
- filter: opacity(0.5) sepia(0.2);
48
+ filter: sepia(0.2);
49
+ opacity: 0.45;
49
50
  cursor: unset;
50
51
  }
51
52
 
@@ -103,7 +104,7 @@
103
104
 
104
105
  .toolbar-root button:disabled {
105
106
  cursor: inherit;
106
- filter: opacity(0.5);
107
+ opacity: 0.5;
107
108
  }
108
109
 
109
110
  .toolbar-root .toolbar-icon {
@@ -29,6 +29,14 @@
29
29
  display: inline-flex;
30
30
  flex-direction: row;
31
31
 
32
+ .coloris_input {
33
+ // Ensure that the region that can be clicked to open the input is roughly
34
+ // the full height of the container.
35
+ // Because the color picker is always shown below or above the input, 5px is
36
+ // subtracted to make the picker better align with the input's container.
37
+ height: calc(100% - 6px);
38
+ }
39
+
32
40
  &.picker-open .clr-field {
33
41
  // Work around what seems to be a Coloris bug -- clicking on the input button
34
42
  // keeps the color picker open (while clicking anywhere else closes the picker).