js-draw 1.24.2 → 1.26.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +15 -15
  3. package/dist/Editor.css +1 -1935
  4. package/dist/bundle.js +473 -4
  5. package/dist/bundledStyles.js +1 -1
  6. package/dist/cjs/Editor.d.ts +12 -0
  7. package/dist/cjs/Editor.js +2 -1
  8. package/dist/cjs/bundle/bundled.js +2 -1
  9. package/dist/cjs/commands/invertCommand.test.d.ts +1 -0
  10. package/dist/cjs/image/EditorImage.d.ts +2 -1
  11. package/dist/cjs/image/EditorImage.js +21 -6
  12. package/dist/cjs/testing/fillHtmlInput.d.ts +6 -0
  13. package/dist/cjs/testing/fillHtmlInput.js +22 -0
  14. package/dist/cjs/testing/sendKeyPressRelease.d.ts +2 -2
  15. package/dist/cjs/testing/sendKeyPressRelease.js +15 -3
  16. package/dist/cjs/toolbar/AbstractToolbar.js +9 -2
  17. package/dist/cjs/toolbar/widgets/BaseWidget.js +6 -1
  18. package/dist/cjs/toolbar/widgets/HandToolWidget.js +3 -3
  19. package/dist/cjs/tools/PasteHandler.d.ts +1 -1
  20. package/dist/cjs/tools/PasteHandler.js +12 -4
  21. package/dist/cjs/tools/PasteHandler.test.d.ts +1 -0
  22. package/dist/cjs/tools/SelectionTool/Selection.js +11 -6
  23. package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +3 -1
  24. package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.js +13 -4
  25. package/dist/cjs/tools/TextTool.js +9 -2
  26. package/dist/cjs/util/ClipboardHandler.js +23 -1
  27. package/dist/cjs/util/assertions.d.ts +7 -6
  28. package/dist/cjs/util/assertions.js +35 -29
  29. package/dist/cjs/version.js +1 -1
  30. package/dist/mjs/Editor.d.ts +12 -0
  31. package/dist/mjs/Editor.mjs +2 -1
  32. package/dist/mjs/bundle/bundled.mjs +2 -1
  33. package/dist/mjs/commands/invertCommand.test.d.ts +1 -0
  34. package/dist/mjs/image/EditorImage.d.ts +2 -1
  35. package/dist/mjs/image/EditorImage.mjs +21 -6
  36. package/dist/mjs/testing/fillHtmlInput.d.ts +6 -0
  37. package/dist/mjs/testing/fillHtmlInput.mjs +17 -0
  38. package/dist/mjs/testing/sendKeyPressRelease.d.ts +2 -2
  39. package/dist/mjs/testing/sendKeyPressRelease.mjs +12 -3
  40. package/dist/mjs/toolbar/AbstractToolbar.mjs +9 -2
  41. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +6 -1
  42. package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +3 -3
  43. package/dist/mjs/tools/PasteHandler.d.ts +1 -1
  44. package/dist/mjs/tools/PasteHandler.mjs +12 -4
  45. package/dist/mjs/tools/PasteHandler.test.d.ts +1 -0
  46. package/dist/mjs/tools/SelectionTool/Selection.mjs +11 -6
  47. package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +3 -1
  48. package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.mjs +13 -4
  49. package/dist/mjs/tools/TextTool.mjs +9 -2
  50. package/dist/mjs/util/ClipboardHandler.mjs +23 -1
  51. package/dist/mjs/util/assertions.d.ts +7 -6
  52. package/dist/mjs/util/assertions.mjs +28 -24
  53. package/dist/mjs/version.mjs +1 -1
  54. package/package.json +4 -4
  55. package/src/tools/SelectionTool/SelectionTool.scss +11 -1
  56. package/src/tools/util/createMenuOverlay.scss +2 -1
@@ -72,13 +72,13 @@ class Selection {
72
72
  side: Vec2.of(0.5, 0),
73
73
  icon: this.editor.icons.makeRotateIcon(),
74
74
  }, this, this.editor.viewport, (startPoint) => this.transformers.rotate.onDragStart(startPoint), (currentPoint) => this.transformers.rotate.onDragUpdate(currentPoint), () => this.transformers.rotate.onDragEnd());
75
- const menuToggleButton = new SelectionMenuShortcut(this, this.editor.viewport, showContextMenu, this.editor.localization);
75
+ const menuToggleButton = new SelectionMenuShortcut(this, this.editor.viewport, this.editor.icons.makeOverflowIcon(), showContextMenu, this.editor.localization);
76
76
  this.childwidgets = [
77
+ menuToggleButton,
77
78
  resizeBothHandle,
78
79
  ...resizeHorizontalHandles,
79
80
  resizeVerticalHandle,
80
81
  rotationHandle,
81
- menuToggleButton,
82
82
  ];
83
83
  for (const widget of this.childwidgets) {
84
84
  widget.addTo(this.backgroundElem);
@@ -454,6 +454,7 @@ class Selection {
454
454
  if (!wasTransforming) {
455
455
  this.runSelectionDuplicatedAnimation();
456
456
  }
457
+ let command;
457
458
  if (wasTransforming) {
458
459
  // Don't update the selection's focus when redoing/undoing
459
460
  const selectionToUpdate = null;
@@ -463,16 +464,20 @@ class Selection {
463
464
  await tmpApplyCommand.apply(this.editor);
464
465
  // Show items again
465
466
  this.addRemoveSelectionFromImage(true);
466
- }
467
- const duplicateCommand = new Duplicate(this.selectedElems);
468
- if (wasTransforming) {
467
+ // With the transformation applied, create the duplicates
468
+ command = uniteCommands(this.selectedElems.map((elem) => {
469
+ return EditorImage.addElement(elem.clone());
470
+ }));
469
471
  // Move the selected objects back to the correct location.
470
472
  await tmpApplyCommand?.unapply(this.editor);
471
473
  this.addRemoveSelectionFromImage(false);
472
474
  this.previewTransformCmds();
473
475
  this.updateUI();
474
476
  }
475
- return duplicateCommand;
477
+ else {
478
+ command = new Duplicate(this.selectedElems);
479
+ }
480
+ return command;
476
481
  }
477
482
  setHandlesVisible(showHandles) {
478
483
  if (!showHandles) {
@@ -11,10 +11,12 @@ type OnShowContextMenu = (anchor: Point2) => void;
11
11
  export default class SelectionMenuShortcut implements SelectionBoxChild {
12
12
  private readonly parent;
13
13
  private readonly viewport;
14
+ private readonly icon;
14
15
  private localization;
15
16
  private element;
17
+ private button;
16
18
  private onClick;
17
- constructor(parent: Selection, viewport: Viewport, showContextMenu: OnShowContextMenu, localization: ToolLocalization);
19
+ constructor(parent: Selection, viewport: Viewport, icon: Element, showContextMenu: OnShowContextMenu, localization: ToolLocalization);
18
20
  private initUI;
19
21
  addTo(container: HTMLElement): void;
20
22
  remove(): void;
@@ -2,15 +2,17 @@ import { Rect2, Vec2 } from '@js-draw/math';
2
2
  import { cssPrefix } from './SelectionTool.mjs';
3
3
  const verticalOffset = 40;
4
4
  export default class SelectionMenuShortcut {
5
- constructor(parent, viewport, showContextMenu, localization) {
5
+ constructor(parent, viewport, icon, showContextMenu, localization) {
6
6
  this.parent = parent;
7
7
  this.viewport = viewport;
8
+ this.icon = icon;
8
9
  this.localization = localization;
9
10
  this.lastDragPointer = null;
10
11
  this.element = document.createElement('div');
11
12
  this.element.classList.add(`${cssPrefix}handle`, `${cssPrefix}selection-menu`);
12
13
  this.element.style.setProperty('--vertical-offset', `${verticalOffset}px`);
13
14
  this.onClick = () => {
15
+ this.button?.focus({ preventScroll: true });
14
16
  const anchor = this.getBBoxCanvasCoords().center;
15
17
  showContextMenu(anchor);
16
18
  };
@@ -19,16 +21,22 @@ export default class SelectionMenuShortcut {
19
21
  }
20
22
  initUI() {
21
23
  const button = document.createElement('button');
22
- button.textContent = '...';
24
+ this.icon.classList.add('icon');
25
+ button.replaceChildren(this.icon);
23
26
  button.ariaLabel = this.localization.selectionMenu__show;
24
27
  button.title = button.ariaLabel;
28
+ this.button = button;
25
29
  // To prevent editor event handlers from conflicting with those for the button,
26
30
  // don't register a [click] handler. An onclick handler can be fired incorrectly
27
31
  // in this case (in Chrome) after onClick is fired in onDragEnd, leading to a double
28
32
  // on-click action.
29
33
  button.onkeydown = (event) => {
30
- if (event.key === 'Enter')
34
+ if (event.key === 'Enter') {
35
+ // .preventDefault prevents [Enter] from activating the first item in the
36
+ // selection menu.
37
+ event.preventDefault();
31
38
  this.onClick();
39
+ }
32
40
  };
33
41
  this.element.appendChild(button);
34
42
  // Update the bounding box of this in response to the new button.
@@ -58,7 +66,8 @@ export default class SelectionMenuShortcut {
58
66
  const contentCanvasSize = this.getElementScreenSize().times(toCanvasScale);
59
67
  const handleSizeCanvas = verticalOffset / this.viewport.getScaleFactor();
60
68
  const topLeft = Vec2.of(parentCanvasRect.x, parentCanvasRect.y - handleSizeCanvas);
61
- return new Rect2(topLeft.x, topLeft.y, contentCanvasSize.x, contentCanvasSize.y);
69
+ const minSize = Vec2.of(48, 48).times(toCanvasScale);
70
+ return new Rect2(topLeft.x, topLeft.y, contentCanvasSize.x, contentCanvasSize.y).grownToSize(minSize);
62
71
  }
63
72
  updatePosition() {
64
73
  const bbox = this.getBBoxParentCoords();
@@ -158,17 +158,24 @@ export default class TextTool extends BaseTool {
158
158
  }
159
159
  };
160
160
  this.textInputElem.onblur = () => {
161
+ const input = this.textInputElem;
161
162
  // Delay removing the input -- flushInput may be called within a blur()
162
163
  // event handler
163
164
  const removeInput = false;
164
- const input = this.textInputElem;
165
165
  this.flushInput(removeInput);
166
166
  this.textInputElem = null;
167
+ if (input) {
168
+ input.classList.add('-hiding');
169
+ }
167
170
  setTimeout(() => {
168
171
  input?.remove();
169
172
  }, 0);
170
173
  };
171
174
  this.textInputElem.onkeyup = (evt) => {
175
+ // In certain input modes, the <enter> key is used to select characters.
176
+ // When in this mode, prevent <enter> from submitting:
177
+ if (evt.isComposing)
178
+ return;
172
179
  if (evt.key === 'Enter' && !evt.shiftKey) {
173
180
  this.flushInput();
174
181
  this.editor.focus();
@@ -199,7 +206,7 @@ export default class TextTool extends BaseTool {
199
206
  if (allPointers.length === 1) {
200
207
  // Are we clicking on a text node?
201
208
  const canvasPos = current.canvasPos;
202
- const halfTestRegionSize = Vec2.of(2.5, 2.5).times(this.editor.viewport.getSizeOfPixelOnCanvas());
209
+ const halfTestRegionSize = Vec2.of(4, 4).times(this.editor.viewport.getSizeOfPixelOnCanvas());
203
210
  const testRegion = Rect2.fromCorners(canvasPos.minus(halfTestRegionSize), canvasPos.plus(halfTestRegionSize));
204
211
  const targetNodes = this.editor.image.getElementsIntersectingRegion(testRegion);
205
212
  let targetTextNodes = targetNodes.filter((node) => node instanceof TextComponent);
@@ -70,6 +70,7 @@ class ClipboardHandler {
70
70
  const supportedMIMEs = ['image/svg+xml', 'text/html', 'image/png', 'image/jpeg', 'text/plain'];
71
71
  let files = [];
72
72
  const textData = new Map();
73
+ const editorSettings = editor.getCurrentSettings();
73
74
  if (hasEvent) {
74
75
  // NOTE: On some browsers, .getData and .files must be used before any async operations.
75
76
  files = [...clipboardData.files];
@@ -80,6 +81,21 @@ class ClipboardHandler {
80
81
  }
81
82
  }
82
83
  }
84
+ else if (editorSettings.clipboardApi) {
85
+ const clipboardData = await editorSettings.clipboardApi.read();
86
+ for (const [type, data] of clipboardData.entries()) {
87
+ if (typeof data === 'string') {
88
+ textData.set(type, data);
89
+ }
90
+ else {
91
+ let blob = data;
92
+ if (blob.type !== type) {
93
+ blob = new Blob([blob], { type });
94
+ }
95
+ files.push(blob);
96
+ }
97
+ }
98
+ }
83
99
  else {
84
100
  const clipboardData = await navigator.clipboard.read();
85
101
  for (const item of clipboardData) {
@@ -233,7 +249,13 @@ class ClipboardHandler {
233
249
  return navigator.clipboard.write([new ClipboardItem(browserMimeToData)]);
234
250
  };
235
251
  const supportsClipboardApi = typeof ClipboardItem !== 'undefined' && typeof navigator?.clipboard?.write !== 'undefined';
236
- if (!__classPrivateFieldGet(this, _ClipboardHandler_preferClipboardEvents, "f") && supportsClipboardApi && (hasNonTextMimeTypes || !event)) {
252
+ const prefersClipboardApi = !__classPrivateFieldGet(this, _ClipboardHandler_preferClipboardEvents, "f") && supportsClipboardApi && (hasNonTextMimeTypes || !event);
253
+ const editorSettings = this.editor.getCurrentSettings();
254
+ if (prefersClipboardApi && editorSettings.clipboardApi) {
255
+ const writeResult = editorSettings.clipboardApi.write(mimeToData);
256
+ return writeResult ?? Promise.resolve();
257
+ }
258
+ else if (prefersClipboardApi) {
237
259
  let clipboardApiPromise = null;
238
260
  const fallBackToCopyEvent = (reason) => {
239
261
  console.warn('Unable to copy to the clipboard API. Future calls to .copy will use ClipboardEvents if possible.', reason);
@@ -2,7 +2,7 @@
2
2
  * Compile-time assertion that a branch of code is unreachable.
3
3
  * @internal
4
4
  */
5
- export declare const assertUnreachable: (key: never) => never;
5
+ export declare function assertUnreachable(key: never): never;
6
6
  /**
7
7
  * Throws an exception if the typeof given value is not a number or `value` is NaN.
8
8
  *
@@ -13,15 +13,16 @@ export declare const assertUnreachable: (key: never) => never;
13
13
  *
14
14
  * assertIsNumber('hello, world'); // throws an Error.
15
15
  * ```
16
- *
17
- *
18
16
  */
19
- export declare const assertIsNumber: (value: any, allowNaN?: boolean) => value is number;
17
+ export declare function assertIsNumber(value: unknown, allowNaN?: boolean): asserts value is number;
18
+ export declare function assertIsArray(values: unknown): asserts values is unknown[];
20
19
  /**
21
20
  * Throws if any of `values` is not of type number.
22
21
  */
23
- export declare const assertIsNumberArray: (values: any[], allowNaN?: boolean) => values is number[];
22
+ export declare function assertIsNumberArray(values: unknown, allowNaN?: boolean): asserts values is number[];
24
23
  /**
25
24
  * Throws an exception if `typeof value` is not a boolean.
26
25
  */
27
- export declare const assertIsBoolean: (value: any) => value is boolean;
26
+ export declare function assertIsBoolean(value: unknown): asserts value is boolean;
27
+ export declare function assertTruthy<T>(value: T | null | undefined | false | 0): asserts value is T;
28
+ export declare function assertIsObject(value: unknown): asserts value is Record<string, unknown>;
@@ -1,11 +1,13 @@
1
+ // Note: Arrow functions cannot be used for type assertions. See
2
+ // https://github.com/microsoft/TypeScript/issues/34523
1
3
  /**
2
4
  * Compile-time assertion that a branch of code is unreachable.
3
5
  * @internal
4
6
  */
5
- export const assertUnreachable = (key) => {
7
+ export function assertUnreachable(key) {
6
8
  // See https://stackoverflow.com/a/39419171/17055750
7
9
  throw new Error(`Should be unreachable. Key: ${key}.`);
8
- };
10
+ }
9
11
  /**
10
12
  * Throws an exception if the typeof given value is not a number or `value` is NaN.
11
13
  *
@@ -16,40 +18,42 @@ export const assertUnreachable = (key) => {
16
18
  *
17
19
  * assertIsNumber('hello, world'); // throws an Error.
18
20
  * ```
19
- *
20
- *
21
21
  */
22
- export const assertIsNumber = (value, allowNaN = false) => {
22
+ export function assertIsNumber(value, allowNaN = false) {
23
23
  if (typeof value !== 'number' || (!allowNaN && isNaN(value))) {
24
24
  throw new Error('Given value is not a number');
25
- // return false;
26
25
  }
27
- return true;
28
- };
26
+ }
27
+ export function assertIsArray(values) {
28
+ if (!Array.isArray(values)) {
29
+ throw new Error('Asserting isArray: Given entity is not an array');
30
+ }
31
+ }
29
32
  /**
30
33
  * Throws if any of `values` is not of type number.
31
34
  */
32
- export const assertIsNumberArray = (values, allowNaN = false) => {
33
- if (typeof values !== 'object') {
34
- throw new Error('Asserting isNumberArray: Given entity is not an array');
35
- }
36
- if (!assertIsNumber(values['length'])) {
37
- return false;
38
- }
35
+ export function assertIsNumberArray(values, allowNaN = false) {
36
+ assertIsArray(values);
37
+ assertIsNumber(values.length);
39
38
  for (const value of values) {
40
- if (!assertIsNumber(value, allowNaN)) {
41
- return false;
42
- }
39
+ assertIsNumber(value, allowNaN);
43
40
  }
44
- return true;
45
- };
41
+ }
46
42
  /**
47
43
  * Throws an exception if `typeof value` is not a boolean.
48
44
  */
49
- export const assertIsBoolean = (value) => {
45
+ export function assertIsBoolean(value) {
50
46
  if (typeof value !== 'boolean') {
51
47
  throw new Error('Given value is not a boolean');
52
- // return false;
53
48
  }
54
- return true;
55
- };
49
+ }
50
+ export function assertTruthy(value) {
51
+ if (!value) {
52
+ throw new Error(`${JSON.stringify(value)} is not truthy`);
53
+ }
54
+ }
55
+ export function assertIsObject(value) {
56
+ if (typeof value !== 'object') {
57
+ throw new Error(`AssertIsObject: Given entity is not an object (type = ${typeof value})`);
58
+ }
59
+ }
@@ -4,5 +4,5 @@
4
4
  * @internal
5
5
  */
6
6
  export default {
7
- number: '1.24.2',
7
+ number: '1.26.0',
8
8
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "1.24.2",
3
+ "version": "1.26.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,11 +64,11 @@
64
64
  "postpack": "ts-node tools/copyREADME.ts revert"
65
65
  },
66
66
  "dependencies": {
67
- "@js-draw/math": "^1.24.2",
67
+ "@js-draw/math": "^1.26.0",
68
68
  "@melloware/coloris": "0.22.0"
69
69
  },
70
70
  "devDependencies": {
71
- "@js-draw/build-tool": "^1.24.2",
71
+ "@js-draw/build-tool": "^1.26.0",
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": "32c8db56fc8996c8d485118d1ee37077428344a3"
89
+ "gitHead": "6529cad584ca93a992f2576a43f25af48da3d707"
90
90
  }
@@ -85,12 +85,17 @@
85
85
  > button {
86
86
  max-height: var(--vertical-offset);
87
87
  background-color: var(--background-color-1);
88
+
89
+ width: 24px;
90
+ height: 24px;
91
+ padding: 6px;
88
92
  font-size: 14px;
93
+ user-select: none;
94
+ -webkit-user-select: none;
89
95
 
90
96
  color: var(--foreground-color-1);
91
97
  border: 0.5px solid var(--foreground-color-1);
92
98
  border-radius: 3px;
93
- padding: 3px;
94
99
  opacity: 0.8;
95
100
 
96
101
  &:hover,
@@ -102,6 +107,11 @@
102
107
  }
103
108
 
104
109
  transition: 0.2s ease opacity;
110
+
111
+ > .icon {
112
+ width: 100%;
113
+ height: 100%;
114
+ }
105
115
  }
106
116
  }
107
117
 
@@ -32,6 +32,7 @@
32
32
  overflow: clip;
33
33
  border-radius: 6px;
34
34
  box-shadow: 0px 1px 2px var(--shadow-color);
35
+ background-color: var(--background-color-1);
35
36
  }
36
37
 
37
38
  &::backdrop {
@@ -47,7 +48,7 @@
47
48
  padding-top: 6px;
48
49
  padding-bottom: 6px;
49
50
 
50
- background-color: var(--background-color-1);
51
+ background-color: transparent;
51
52
  color: var(--foreground-color-1);
52
53
  --icon-color: currentColor;
53
54