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
@@ -110,13 +110,13 @@ class Selection {
110
110
  side: math_1.Vec2.of(0.5, 0),
111
111
  icon: this.editor.icons.makeRotateIcon(),
112
112
  }, this, this.editor.viewport, (startPoint) => this.transformers.rotate.onDragStart(startPoint), (currentPoint) => this.transformers.rotate.onDragUpdate(currentPoint), () => this.transformers.rotate.onDragEnd());
113
- const menuToggleButton = new SelectionMenuShortcut_1.default(this, this.editor.viewport, showContextMenu, this.editor.localization);
113
+ const menuToggleButton = new SelectionMenuShortcut_1.default(this, this.editor.viewport, this.editor.icons.makeOverflowIcon(), showContextMenu, this.editor.localization);
114
114
  this.childwidgets = [
115
+ menuToggleButton,
115
116
  resizeBothHandle,
116
117
  ...resizeHorizontalHandles,
117
118
  resizeVerticalHandle,
118
119
  rotationHandle,
119
- menuToggleButton,
120
120
  ];
121
121
  for (const widget of this.childwidgets) {
122
122
  widget.addTo(this.backgroundElem);
@@ -492,6 +492,7 @@ class Selection {
492
492
  if (!wasTransforming) {
493
493
  this.runSelectionDuplicatedAnimation();
494
494
  }
495
+ let command;
495
496
  if (wasTransforming) {
496
497
  // Don't update the selection's focus when redoing/undoing
497
498
  const selectionToUpdate = null;
@@ -501,16 +502,20 @@ class Selection {
501
502
  await tmpApplyCommand.apply(this.editor);
502
503
  // Show items again
503
504
  this.addRemoveSelectionFromImage(true);
504
- }
505
- const duplicateCommand = new Duplicate_1.default(this.selectedElems);
506
- if (wasTransforming) {
505
+ // With the transformation applied, create the duplicates
506
+ command = (0, uniteCommands_1.default)(this.selectedElems.map((elem) => {
507
+ return EditorImage_1.default.addElement(elem.clone());
508
+ }));
507
509
  // Move the selected objects back to the correct location.
508
510
  await tmpApplyCommand?.unapply(this.editor);
509
511
  this.addRemoveSelectionFromImage(false);
510
512
  this.previewTransformCmds();
511
513
  this.updateUI();
512
514
  }
513
- return duplicateCommand;
515
+ else {
516
+ command = new Duplicate_1.default(this.selectedElems);
517
+ }
518
+ return command;
514
519
  }
515
520
  setHandlesVisible(showHandles) {
516
521
  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;
@@ -4,15 +4,17 @@ const math_1 = require("@js-draw/math");
4
4
  const SelectionTool_1 = require("./SelectionTool");
5
5
  const verticalOffset = 40;
6
6
  class SelectionMenuShortcut {
7
- constructor(parent, viewport, showContextMenu, localization) {
7
+ constructor(parent, viewport, icon, showContextMenu, localization) {
8
8
  this.parent = parent;
9
9
  this.viewport = viewport;
10
+ this.icon = icon;
10
11
  this.localization = localization;
11
12
  this.lastDragPointer = null;
12
13
  this.element = document.createElement('div');
13
14
  this.element.classList.add(`${SelectionTool_1.cssPrefix}handle`, `${SelectionTool_1.cssPrefix}selection-menu`);
14
15
  this.element.style.setProperty('--vertical-offset', `${verticalOffset}px`);
15
16
  this.onClick = () => {
17
+ this.button?.focus({ preventScroll: true });
16
18
  const anchor = this.getBBoxCanvasCoords().center;
17
19
  showContextMenu(anchor);
18
20
  };
@@ -21,16 +23,22 @@ class SelectionMenuShortcut {
21
23
  }
22
24
  initUI() {
23
25
  const button = document.createElement('button');
24
- button.textContent = '...';
26
+ this.icon.classList.add('icon');
27
+ button.replaceChildren(this.icon);
25
28
  button.ariaLabel = this.localization.selectionMenu__show;
26
29
  button.title = button.ariaLabel;
30
+ this.button = button;
27
31
  // To prevent editor event handlers from conflicting with those for the button,
28
32
  // don't register a [click] handler. An onclick handler can be fired incorrectly
29
33
  // in this case (in Chrome) after onClick is fired in onDragEnd, leading to a double
30
34
  // on-click action.
31
35
  button.onkeydown = (event) => {
32
- if (event.key === 'Enter')
36
+ if (event.key === 'Enter') {
37
+ // .preventDefault prevents [Enter] from activating the first item in the
38
+ // selection menu.
39
+ event.preventDefault();
33
40
  this.onClick();
41
+ }
34
42
  };
35
43
  this.element.appendChild(button);
36
44
  // Update the bounding box of this in response to the new button.
@@ -60,7 +68,8 @@ class SelectionMenuShortcut {
60
68
  const contentCanvasSize = this.getElementScreenSize().times(toCanvasScale);
61
69
  const handleSizeCanvas = verticalOffset / this.viewport.getScaleFactor();
62
70
  const topLeft = math_1.Vec2.of(parentCanvasRect.x, parentCanvasRect.y - handleSizeCanvas);
63
- return new math_1.Rect2(topLeft.x, topLeft.y, contentCanvasSize.x, contentCanvasSize.y);
71
+ const minSize = math_1.Vec2.of(48, 48).times(toCanvasScale);
72
+ return new math_1.Rect2(topLeft.x, topLeft.y, contentCanvasSize.x, contentCanvasSize.y).grownToSize(minSize);
64
73
  }
65
74
  updatePosition() {
66
75
  const bbox = this.getBBoxParentCoords();
@@ -163,17 +163,24 @@ class TextTool extends BaseTool_1.default {
163
163
  }
164
164
  };
165
165
  this.textInputElem.onblur = () => {
166
+ const input = this.textInputElem;
166
167
  // Delay removing the input -- flushInput may be called within a blur()
167
168
  // event handler
168
169
  const removeInput = false;
169
- const input = this.textInputElem;
170
170
  this.flushInput(removeInput);
171
171
  this.textInputElem = null;
172
+ if (input) {
173
+ input.classList.add('-hiding');
174
+ }
172
175
  setTimeout(() => {
173
176
  input?.remove();
174
177
  }, 0);
175
178
  };
176
179
  this.textInputElem.onkeyup = (evt) => {
180
+ // In certain input modes, the <enter> key is used to select characters.
181
+ // When in this mode, prevent <enter> from submitting:
182
+ if (evt.isComposing)
183
+ return;
177
184
  if (evt.key === 'Enter' && !evt.shiftKey) {
178
185
  this.flushInput();
179
186
  this.editor.focus();
@@ -204,7 +211,7 @@ class TextTool extends BaseTool_1.default {
204
211
  if (allPointers.length === 1) {
205
212
  // Are we clicking on a text node?
206
213
  const canvasPos = current.canvasPos;
207
- const halfTestRegionSize = math_1.Vec2.of(2.5, 2.5).times(this.editor.viewport.getSizeOfPixelOnCanvas());
214
+ const halfTestRegionSize = math_1.Vec2.of(4, 4).times(this.editor.viewport.getSizeOfPixelOnCanvas());
208
215
  const testRegion = math_1.Rect2.fromCorners(canvasPos.minus(halfTestRegionSize), canvasPos.plus(halfTestRegionSize));
209
216
  const targetNodes = this.editor.image.getElementsIntersectingRegion(testRegion);
210
217
  let targetTextNodes = targetNodes.filter((node) => node instanceof TextComponent_1.default);
@@ -75,6 +75,7 @@ class ClipboardHandler {
75
75
  const supportedMIMEs = ['image/svg+xml', 'text/html', 'image/png', 'image/jpeg', 'text/plain'];
76
76
  let files = [];
77
77
  const textData = new Map();
78
+ const editorSettings = editor.getCurrentSettings();
78
79
  if (hasEvent) {
79
80
  // NOTE: On some browsers, .getData and .files must be used before any async operations.
80
81
  files = [...clipboardData.files];
@@ -85,6 +86,21 @@ class ClipboardHandler {
85
86
  }
86
87
  }
87
88
  }
89
+ else if (editorSettings.clipboardApi) {
90
+ const clipboardData = await editorSettings.clipboardApi.read();
91
+ for (const [type, data] of clipboardData.entries()) {
92
+ if (typeof data === 'string') {
93
+ textData.set(type, data);
94
+ }
95
+ else {
96
+ let blob = data;
97
+ if (blob.type !== type) {
98
+ blob = new Blob([blob], { type });
99
+ }
100
+ files.push(blob);
101
+ }
102
+ }
103
+ }
88
104
  else {
89
105
  const clipboardData = await navigator.clipboard.read();
90
106
  for (const item of clipboardData) {
@@ -238,7 +254,13 @@ class ClipboardHandler {
238
254
  return navigator.clipboard.write([new ClipboardItem(browserMimeToData)]);
239
255
  };
240
256
  const supportsClipboardApi = typeof ClipboardItem !== 'undefined' && typeof navigator?.clipboard?.write !== 'undefined';
241
- if (!__classPrivateFieldGet(this, _ClipboardHandler_preferClipboardEvents, "f") && supportsClipboardApi && (hasNonTextMimeTypes || !event)) {
257
+ const prefersClipboardApi = !__classPrivateFieldGet(this, _ClipboardHandler_preferClipboardEvents, "f") && supportsClipboardApi && (hasNonTextMimeTypes || !event);
258
+ const editorSettings = this.editor.getCurrentSettings();
259
+ if (prefersClipboardApi && editorSettings.clipboardApi) {
260
+ const writeResult = editorSettings.clipboardApi.write(mimeToData);
261
+ return writeResult ?? Promise.resolve();
262
+ }
263
+ else if (prefersClipboardApi) {
242
264
  let clipboardApiPromise = null;
243
265
  const fallBackToCopyEvent = (reason) => {
244
266
  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,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.24.2',
9
+ number: '1.26.0',
10
10
  };
@@ -121,6 +121,18 @@ export interface EditorSettings {
121
121
  */
122
122
  showImagePicker?: ShowCustomFilePickerCallback;
123
123
  } | null;
124
+ /**
125
+ * Allows changing how js-draw interacts with the clipboard.
126
+ *
127
+ * **Note**: Even when a custom `clipboardApi` is specified, if a `ClipboardEvent` is available
128
+ * (e.g. from when a user pastes with ctrl+v), the `ClipboardEvent` will be preferred.
129
+ */
130
+ clipboardApi: {
131
+ /** Called to read data to the clipboard. Keys in the result are MIME types. Values are the data associated with that type. */
132
+ read(): Promise<Map<string, Blob | string>>;
133
+ /** Called to write data to the clipboard. Keys in `data` are MIME types. Values are the data associated with that type. */
134
+ write(data: Map<string, Blob | Promise<Blob> | string>): void | Promise<void>;
135
+ } | null;
124
136
  }
125
137
  /**
126
138
  * The main entrypoint for the full editor.
@@ -120,6 +120,7 @@ export class Editor {
120
120
  image: {
121
121
  showImagePicker: settings.image?.showImagePicker ?? undefined,
122
122
  },
123
+ clipboardApi: settings.clipboardApi ?? null,
123
124
  };
124
125
  // Validate settings
125
126
  if (this.settings.minZoom > this.settings.maxZoom) {
@@ -1319,7 +1320,7 @@ export class Editor {
1319
1320
  '',
1320
1321
  '',
1321
1322
  '== js-draw ==',
1322
- mitLicenseAttribution('2023-2024 Henry Heino'),
1323
+ mitLicenseAttribution('2023-2025 Henry Heino'),
1323
1324
  '',
1324
1325
  ].join('\n'),
1325
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';
@@ -0,0 +1 @@
1
+ export {};
@@ -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
@@ -0,0 +1,6 @@
1
+ interface Options {
2
+ clear?: boolean;
3
+ }
4
+ /** Sets the content of the given `input` or textarea to be `text`. */
5
+ declare const fillInput: (input: HTMLInputElement | HTMLTextAreaElement, text: string, { clear }?: Options) => void;
6
+ export default fillInput;
@@ -0,0 +1,17 @@
1
+ import sendKeyPressRelease from './sendKeyPressRelease.mjs';
2
+ /** Sets the content of the given `input` or textarea to be `text`. */
3
+ const fillInput = (input, text, { clear = false } = {}) => {
4
+ const dispatchUpdate = () => {
5
+ input.dispatchEvent(new InputEvent('input'));
6
+ };
7
+ if (clear) {
8
+ input.value = '';
9
+ dispatchUpdate();
10
+ }
11
+ for (const character of text.split('')) {
12
+ input.value += character;
13
+ sendKeyPressRelease(input, character);
14
+ dispatchUpdate();
15
+ }
16
+ };
17
+ export default fillInput;
@@ -1,3 +1,3 @@
1
- import type Editor from '../Editor';
2
- declare const sendKeyPressRelease: (editor: Editor, key: string) => void;
1
+ import Editor from '../Editor';
2
+ declare const sendKeyPressRelease: (target: Editor | HTMLElement, key: string) => void;
3
3
  export default sendKeyPressRelease;
@@ -1,6 +1,15 @@
1
+ import Editor from '../Editor.mjs';
1
2
  import { InputEvtType } from '../inputEvents.mjs';
2
- const sendKeyPressRelease = (editor, key) => {
3
- editor.sendKeyboardEvent(InputEvtType.KeyPressEvent, key);
4
- editor.sendKeyboardEvent(InputEvtType.KeyUpEvent, key);
3
+ import guessKeyCodeFromKey from '../util/guessKeyCodeFromKey.mjs';
4
+ const sendKeyPressRelease = (target, key) => {
5
+ if (target instanceof Editor) {
6
+ target.sendKeyboardEvent(InputEvtType.KeyPressEvent, key);
7
+ target.sendKeyboardEvent(InputEvtType.KeyUpEvent, key);
8
+ }
9
+ else {
10
+ const code = guessKeyCodeFromKey(key);
11
+ target.dispatchEvent(new KeyboardEvent('keydown', { key, code }));
12
+ target.dispatchEvent(new KeyboardEvent('keyup', { key, code }));
13
+ }
5
14
  };
6
15
  export default sendKeyPressRelease;
@@ -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
  }
@@ -15,7 +15,7 @@ import BaseTool from './BaseTool';
15
15
  export default class PasteHandler extends BaseTool {
16
16
  private editor;
17
17
  constructor(editor: Editor);
18
- onPaste(event: PasteEvent): boolean;
18
+ onPaste(event: PasteEvent, onComplete?: () => void): boolean;
19
19
  private addComponentsFromPaste;
20
20
  private doSVGPaste;
21
21
  private doTextPaste;
@@ -21,12 +21,20 @@ export default class PasteHandler extends BaseTool {
21
21
  this.editor = editor;
22
22
  }
23
23
  // @internal
24
- onPaste(event) {
24
+ onPaste(event, onComplete) {
25
25
  const mime = event.mime.toLowerCase();
26
26
  const svgData = (() => {
27
27
  if (mime === 'image/svg+xml') {
28
28
  return event.data;
29
29
  }
30
+ // In some environments, it isn't possible to write non-text data to the
31
+ // clipboard. To support these cases, auto-detect text/plain SVG data.
32
+ if (mime === 'text/plain') {
33
+ const trimmedData = event.data.trim();
34
+ if (trimmedData.startsWith('<svg') && trimmedData.endsWith('</svg>')) {
35
+ return trimmedData;
36
+ }
37
+ }
30
38
  if (mime !== 'text/html') {
31
39
  return false;
32
40
  }
@@ -44,15 +52,15 @@ export default class PasteHandler extends BaseTool {
44
52
  return event.data.substring(event.data.search(/<svg/i), svgEnd);
45
53
  })();
46
54
  if (svgData) {
47
- void this.doSVGPaste(svgData);
55
+ void this.doSVGPaste(svgData).then(onComplete);
48
56
  return true;
49
57
  }
50
58
  else if (mime === 'text/plain') {
51
- void this.doTextPaste(event.data);
59
+ void this.doTextPaste(event.data).then(onComplete);
52
60
  return true;
53
61
  }
54
62
  else if (mime === 'image/png' || mime === 'image/jpeg') {
55
- void this.doImagePaste(event.data);
63
+ void this.doImagePaste(event.data).then(onComplete);
56
64
  return true;
57
65
  }
58
66
  return false;
@@ -0,0 +1 @@
1
+ export {};