js-draw 1.24.2 → 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.
- package/LICENSE +1 -1
- package/README.md +15 -15
- package/dist/Editor.css +1 -1935
- package/dist/bundle.js +473 -4
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +12 -0
- package/dist/cjs/Editor.js +2 -1
- package/dist/cjs/bundle/bundled.js +2 -1
- package/dist/cjs/commands/invertCommand.test.d.ts +1 -0
- package/dist/cjs/image/EditorImage.d.ts +2 -1
- package/dist/cjs/image/EditorImage.js +21 -6
- package/dist/cjs/testing/fillHtmlInput.d.ts +6 -0
- package/dist/cjs/testing/fillHtmlInput.js +22 -0
- package/dist/cjs/testing/sendKeyPressRelease.d.ts +2 -2
- package/dist/cjs/testing/sendKeyPressRelease.js +15 -3
- package/dist/cjs/toolbar/AbstractToolbar.js +9 -2
- package/dist/cjs/toolbar/widgets/BaseWidget.js +6 -1
- package/dist/cjs/toolbar/widgets/HandToolWidget.js +3 -3
- package/dist/cjs/tools/PasteHandler.d.ts +1 -1
- package/dist/cjs/tools/PasteHandler.js +12 -4
- package/dist/cjs/tools/PasteHandler.test.d.ts +1 -0
- package/dist/cjs/tools/SelectionTool/Selection.js +11 -6
- package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +3 -1
- package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.js +13 -4
- package/dist/cjs/tools/TextTool.js +9 -2
- package/dist/cjs/util/ClipboardHandler.js +23 -1
- package/dist/cjs/util/assertions.d.ts +7 -6
- package/dist/cjs/util/assertions.js +35 -29
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +12 -0
- package/dist/mjs/Editor.mjs +2 -1
- package/dist/mjs/bundle/bundled.mjs +2 -1
- package/dist/mjs/commands/invertCommand.test.d.ts +1 -0
- package/dist/mjs/image/EditorImage.d.ts +2 -1
- package/dist/mjs/image/EditorImage.mjs +21 -6
- package/dist/mjs/testing/fillHtmlInput.d.ts +6 -0
- package/dist/mjs/testing/fillHtmlInput.mjs +17 -0
- package/dist/mjs/testing/sendKeyPressRelease.d.ts +2 -2
- package/dist/mjs/testing/sendKeyPressRelease.mjs +12 -3
- package/dist/mjs/toolbar/AbstractToolbar.mjs +9 -2
- package/dist/mjs/toolbar/widgets/BaseWidget.mjs +6 -1
- package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +3 -3
- package/dist/mjs/tools/PasteHandler.d.ts +1 -1
- package/dist/mjs/tools/PasteHandler.mjs +12 -4
- package/dist/mjs/tools/PasteHandler.test.d.ts +1 -0
- package/dist/mjs/tools/SelectionTool/Selection.mjs +11 -6
- package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +3 -1
- package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.mjs +13 -4
- package/dist/mjs/tools/TextTool.mjs +9 -2
- package/dist/mjs/util/ClipboardHandler.mjs +23 -1
- package/dist/mjs/util/assertions.d.ts +7 -6
- package/dist/mjs/util/assertions.mjs +28 -24
- package/dist/mjs/version.mjs +1 -1
- package/package.json +4 -4
- package/src/tools/SelectionTool/SelectionTool.scss +11 -1
- 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
|
-
|
506
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
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
|
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
|
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
|
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
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
46
|
-
return false;
|
47
|
-
}
|
48
|
+
assertIsNumber(value, allowNaN);
|
48
49
|
}
|
49
|
-
|
50
|
-
};
|
51
|
-
exports.assertIsNumberArray = assertIsNumberArray;
|
50
|
+
}
|
52
51
|
/**
|
53
52
|
* Throws an exception if `typeof value` is not a boolean.
|
54
53
|
*/
|
55
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
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
|
+
}
|
package/dist/cjs/version.js
CHANGED
package/dist/mjs/Editor.d.ts
CHANGED
@@ -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.
|
package/dist/mjs/Editor.mjs
CHANGED
@@ -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-
|
1323
|
+
mitLicenseAttribution('2023-2025 Henry Heino'),
|
1323
1324
|
'',
|
1324
1325
|
].join('\n'),
|
1325
1326
|
minimized: true,
|
@@ -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
|
-
|
564
|
-
|
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,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
|
2
|
-
declare const sendKeyPressRelease: (
|
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
|
-
|
3
|
-
|
4
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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 {};
|