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.
- 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 {};
|