js-draw 1.25.0 → 1.26.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 (35) hide show
  1. package/LICENSE +1 -1
  2. package/dist/Editor.css +1 -1935
  3. package/dist/bundle.js +473 -4
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.js +1 -1
  6. package/dist/cjs/bundle/bundled.js +2 -1
  7. package/dist/cjs/image/EditorImage.d.ts +2 -1
  8. package/dist/cjs/image/EditorImage.js +21 -6
  9. package/dist/cjs/toolbar/AbstractToolbar.js +9 -2
  10. package/dist/cjs/toolbar/widgets/BaseWidget.js +6 -1
  11. package/dist/cjs/toolbar/widgets/HandToolWidget.js +3 -3
  12. package/dist/cjs/tools/SelectionTool/Selection.js +11 -6
  13. package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +3 -1
  14. package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.js +13 -4
  15. package/dist/cjs/tools/TextTool.js +5 -2
  16. package/dist/cjs/util/assertions.d.ts +7 -6
  17. package/dist/cjs/util/assertions.js +35 -29
  18. package/dist/cjs/version.js +1 -1
  19. package/dist/mjs/Editor.mjs +1 -1
  20. package/dist/mjs/bundle/bundled.mjs +2 -1
  21. package/dist/mjs/image/EditorImage.d.ts +2 -1
  22. package/dist/mjs/image/EditorImage.mjs +21 -6
  23. package/dist/mjs/toolbar/AbstractToolbar.mjs +9 -2
  24. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +6 -1
  25. package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +3 -3
  26. package/dist/mjs/tools/SelectionTool/Selection.mjs +11 -6
  27. package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +3 -1
  28. package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.mjs +13 -4
  29. package/dist/mjs/tools/TextTool.mjs +5 -2
  30. package/dist/mjs/util/assertions.d.ts +7 -6
  31. package/dist/mjs/util/assertions.mjs +28 -24
  32. package/dist/mjs/version.mjs +1 -1
  33. package/package.json +4 -4
  34. package/src/tools/SelectionTool/SelectionTool.scss +11 -1
  35. package/src/tools/util/createMenuOverlay.scss +2 -1
@@ -1,15 +1,22 @@
1
1
  "use strict";
2
+ // Note: Arrow functions cannot be used for type assertions. See
3
+ // https://github.com/microsoft/TypeScript/issues/34523
2
4
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.assertIsBoolean = exports.assertIsNumberArray = exports.assertIsNumber = exports.assertUnreachable = void 0;
5
+ exports.assertUnreachable = assertUnreachable;
6
+ exports.assertIsNumber = assertIsNumber;
7
+ exports.assertIsArray = assertIsArray;
8
+ exports.assertIsNumberArray = assertIsNumberArray;
9
+ exports.assertIsBoolean = assertIsBoolean;
10
+ exports.assertTruthy = assertTruthy;
11
+ exports.assertIsObject = assertIsObject;
4
12
  /**
5
13
  * Compile-time assertion that a branch of code is unreachable.
6
14
  * @internal
7
15
  */
8
- const assertUnreachable = (key) => {
16
+ function assertUnreachable(key) {
9
17
  // See https://stackoverflow.com/a/39419171/17055750
10
18
  throw new Error(`Should be unreachable. Key: ${key}.`);
11
- };
12
- exports.assertUnreachable = assertUnreachable;
19
+ }
13
20
  /**
14
21
  * Throws an exception if the typeof given value is not a number or `value` is NaN.
15
22
  *
@@ -20,43 +27,42 @@ exports.assertUnreachable = assertUnreachable;
20
27
  *
21
28
  * assertIsNumber('hello, world'); // throws an Error.
22
29
  * ```
23
- *
24
- *
25
30
  */
26
- const assertIsNumber = (value, allowNaN = false) => {
31
+ function assertIsNumber(value, allowNaN = false) {
27
32
  if (typeof value !== 'number' || (!allowNaN && isNaN(value))) {
28
33
  throw new Error('Given value is not a number');
29
- // return false;
30
34
  }
31
- return true;
32
- };
33
- exports.assertIsNumber = assertIsNumber;
35
+ }
36
+ function assertIsArray(values) {
37
+ if (!Array.isArray(values)) {
38
+ throw new Error('Asserting isArray: Given entity is not an array');
39
+ }
40
+ }
34
41
  /**
35
42
  * Throws if any of `values` is not of type number.
36
43
  */
37
- const assertIsNumberArray = (values, allowNaN = false) => {
38
- if (typeof values !== 'object') {
39
- throw new Error('Asserting isNumberArray: Given entity is not an array');
40
- }
41
- if (!(0, exports.assertIsNumber)(values['length'])) {
42
- return false;
43
- }
44
+ function assertIsNumberArray(values, allowNaN = false) {
45
+ assertIsArray(values);
46
+ assertIsNumber(values.length);
44
47
  for (const value of values) {
45
- if (!(0, exports.assertIsNumber)(value, allowNaN)) {
46
- return false;
47
- }
48
+ assertIsNumber(value, allowNaN);
48
49
  }
49
- return true;
50
- };
51
- exports.assertIsNumberArray = assertIsNumberArray;
50
+ }
52
51
  /**
53
52
  * Throws an exception if `typeof value` is not a boolean.
54
53
  */
55
- const assertIsBoolean = (value) => {
54
+ function assertIsBoolean(value) {
56
55
  if (typeof value !== 'boolean') {
57
56
  throw new Error('Given value is not a boolean');
58
- // return false;
59
57
  }
60
- return true;
61
- };
62
- exports.assertIsBoolean = assertIsBoolean;
58
+ }
59
+ function assertTruthy(value) {
60
+ if (!value) {
61
+ throw new Error(`${JSON.stringify(value)} is not truthy`);
62
+ }
63
+ }
64
+ function assertIsObject(value) {
65
+ if (typeof value !== 'object') {
66
+ throw new Error(`AssertIsObject: Given entity is not an object (type = ${typeof value})`);
67
+ }
68
+ }
@@ -6,5 +6,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  * @internal
7
7
  */
8
8
  exports.default = {
9
- number: '1.25.0',
9
+ number: '1.26.0',
10
10
  };
@@ -1320,7 +1320,7 @@ export class Editor {
1320
1320
  '',
1321
1321
  '',
1322
1322
  '== js-draw ==',
1323
- mitLicenseAttribution('2023-2024 Henry Heino'),
1323
+ mitLicenseAttribution('2023-2025 Henry Heino'),
1324
1324
  '',
1325
1325
  ].join('\n'),
1326
1326
  minimized: true,
@@ -1,4 +1,5 @@
1
- // Main entrypoint for Webpack when building a bundle for release.
1
+ // Main entrypoint for the bundler (ESBuild/Webpack/etc.) when creating the bundled
2
+ // portion of a release.
2
3
  import '../styles';
3
4
  import Editor from '../Editor.mjs';
4
5
  export * from '../lib.mjs';
@@ -225,7 +225,7 @@ export declare class ImageNode {
225
225
  getContent(): AbstractComponent | null;
226
226
  getParent(): ImageNode | null;
227
227
  protected getChildrenIntersectingRegion(region: Rect2, isTooSmallFilter?: TooSmallToRenderCheck): ImageNode[];
228
- getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[];
228
+ getChildrenOrSelfIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[];
229
229
  /**
230
230
  * Returns a list of `ImageNode`s with content (and thus no children).
231
231
  * Override getChildrenIntersectingRegion to customize how this method
@@ -258,6 +258,7 @@ export declare class RootImageNode extends ImageNode {
258
258
  private fullscreenChildren;
259
259
  private dataComponents;
260
260
  protected getChildrenIntersectingRegion(region: Rect2, _isTooSmall?: TooSmallToRenderCheck): ImageNode[];
261
+ getChildrenOrSelfIntersectingRegion(region: Rect2, _isTooSmall?: TooSmallToRenderCheck): ImageNode[];
261
262
  getLeaves(): ImageNode[];
262
263
  removeChild(child: ImageNode): void;
263
264
  getChildWithContent(target: AbstractComponent): ImageNode | null;
@@ -538,11 +538,11 @@ export class ImageNode {
538
538
  return !isTooSmallFilter?.(bbox) && bbox.intersects(region);
539
539
  });
540
540
  }
541
- getChildrenOrSelfIntersectingRegion(region) {
542
- if (this.content) {
541
+ getChildrenOrSelfIntersectingRegion(region, isTooSmall) {
542
+ if (this.content && this.bbox.intersects(region) && !isTooSmall?.(this.bbox)) {
543
543
  return [this];
544
544
  }
545
- return this.getChildrenIntersectingRegion(region);
545
+ return this.getChildrenIntersectingRegion(region, isTooSmall);
546
546
  }
547
547
  /**
548
548
  * Returns a list of `ImageNode`s with content (and thus no children).
@@ -560,10 +560,17 @@ export class ImageNode {
560
560
  workList.push(this);
561
561
  while (workList.length > 0) {
562
562
  const current = workList.pop();
563
- if (current.content !== null) {
564
- result.push(current);
563
+ // Split the children into leaves and non-leaves
564
+ const processed = current.getChildrenOrSelfIntersectingRegion(region, isTooSmall);
565
+ for (const item of processed) {
566
+ if (item.content) {
567
+ result.push(item);
568
+ }
569
+ else {
570
+ // Non-leaves need to be processed
571
+ workList.push(item);
572
+ }
565
573
  }
566
- workList.push(...current.getChildrenIntersectingRegion(region, isTooSmall));
567
574
  }
568
575
  return result;
569
576
  }
@@ -917,6 +924,14 @@ export class RootImageNode extends ImageNode {
917
924
  }
918
925
  return result;
919
926
  }
927
+ getChildrenOrSelfIntersectingRegion(region, _isTooSmall) {
928
+ const content = this.getContent();
929
+ // Fullscreen components always intersect/contain
930
+ if (content && content.getSizingMode() === ComponentSizingMode.FillScreen) {
931
+ return [this];
932
+ }
933
+ return super.getChildrenOrSelfIntersectingRegion(region, _isTooSmall);
934
+ }
920
935
  getLeaves() {
921
936
  const leaves = super.getLeaves();
922
937
  // Add fullscreen/data components — this method should
@@ -31,6 +31,7 @@ import { Color4 } from '@js-draw/math';
31
31
  import { toolbarCSSPrefix } from './constants.mjs';
32
32
  import SaveActionWidget from './widgets/SaveActionWidget.mjs';
33
33
  import ExitActionWidget from './widgets/ExitActionWidget.mjs';
34
+ import { assertIsObject, assertTruthy } from '../util/assertions.mjs';
34
35
  /**
35
36
  * Abstract base class for js-draw editor toolbars.
36
37
  *
@@ -205,8 +206,12 @@ class AbstractToolbar {
205
206
  */
206
207
  deserializeState(state) {
207
208
  const data = JSON.parse(state);
209
+ assertIsObject(data);
210
+ assertTruthy(data);
208
211
  const rootId = AbstractToolbar.rootToolbarId;
209
- this.deserializeInternal(data[rootId]);
212
+ if (rootId in data && typeof data[rootId] !== 'undefined') {
213
+ this.deserializeInternal(data[rootId]);
214
+ }
210
215
  for (const widgetId in data) {
211
216
  if (widgetId === rootId) {
212
217
  continue;
@@ -215,7 +220,9 @@ class AbstractToolbar {
215
220
  console.warn(`Unable to deserialize widget ${widgetId} ­— no such widget.`);
216
221
  continue;
217
222
  }
218
- __classPrivateFieldGet(this, _AbstractToolbar_widgetsById, "f")[widgetId].deserializeFrom(data[widgetId]);
223
+ if (typeof data[widgetId] === 'object' && data[widgetId]) {
224
+ __classPrivateFieldGet(this, _AbstractToolbar_widgetsById, "f")[widgetId].deserializeFrom(data[widgetId]);
225
+ }
219
226
  }
220
227
  }
221
228
  /**
@@ -16,6 +16,7 @@ import { toolbarCSSPrefix } from '../constants.mjs';
16
16
  import DropdownLayoutManager from './layout/DropdownLayoutManager.mjs';
17
17
  import addLongPressOrHoverCssClasses from '../../util/addLongPressOrHoverCssClasses.mjs';
18
18
  import HelpDisplay from '../utils/HelpDisplay.mjs';
19
+ import { assertIsObject } from '../../util/assertions.mjs';
19
20
  /**
20
21
  * A set of labels that allow toolbar themes to treat buttons differently.
21
22
  */
@@ -446,10 +447,14 @@ class BaseWidget {
446
447
  */
447
448
  deserializeFrom(state) {
448
449
  if (state.subwidgetState) {
450
+ assertIsObject(state.subwidgetState);
449
451
  // Deserialize all subwidgets.
450
452
  for (const subwidgetId in state.subwidgetState) {
451
453
  if (subwidgetId in this.subWidgets) {
452
- this.subWidgets[subwidgetId].deserializeFrom(state.subwidgetState[subwidgetId]);
454
+ const serializedSubwidgetState = state.subwidgetState[subwidgetId];
455
+ if (serializedSubwidgetState) {
456
+ this.subWidgets[subwidgetId].deserializeFrom(serializedSubwidgetState);
457
+ }
453
458
  }
454
459
  }
455
460
  }
@@ -28,7 +28,7 @@ const makeZoomControl = (localizationTable, editor, helpDisplay) => {
28
28
  zoomLevel = Math.round(zoomLevel * 1000) / 1000;
29
29
  }
30
30
  if (zoomLevel !== lastZoom) {
31
- zoomLevelDisplay.innerText = localizationTable.zoomLevel(zoomLevel);
31
+ zoomLevelDisplay.textContent = localizationTable.zoomLevel(zoomLevel);
32
32
  lastZoom = zoomLevel;
33
33
  }
34
34
  };
@@ -189,10 +189,10 @@ export default class HandToolWidget extends BaseToolWidget {
189
189
  }
190
190
  deserializeFrom(state) {
191
191
  if (state.touchPanning !== undefined) {
192
- this.overridePanZoomTool.setModeEnabled(PanZoomMode.OneFingerTouchGestures, state.touchPanning);
192
+ this.overridePanZoomTool.setModeEnabled(PanZoomMode.OneFingerTouchGestures, !!state.touchPanning);
193
193
  }
194
194
  if (state.rotationLocked !== undefined) {
195
- this.overridePanZoomTool.setModeEnabled(PanZoomMode.RotationLocked, state.rotationLocked);
195
+ this.overridePanZoomTool.setModeEnabled(PanZoomMode.RotationLocked, !!state.rotationLocked);
196
196
  }
197
197
  super.deserializeFrom(state);
198
198
  }
@@ -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,12 +158,15 @@ 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);
@@ -203,7 +206,7 @@ export default class TextTool extends BaseTool {
203
206
  if (allPointers.length === 1) {
204
207
  // Are we clicking on a text node?
205
208
  const canvasPos = current.canvasPos;
206
- 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());
207
210
  const testRegion = Rect2.fromCorners(canvasPos.minus(halfTestRegionSize), canvasPos.plus(halfTestRegionSize));
208
211
  const targetNodes = this.editor.image.getElementsIntersectingRegion(testRegion);
209
212
  let targetTextNodes = targetNodes.filter((node) => node instanceof TextComponent);
@@ -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.25.0',
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.25.0",
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.25.0",
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.25.0",
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": "8ecc7be6d8b1b00c25fe7d3ed6c5fee239451dfa"
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