js-draw 1.12.0 → 1.13.2
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/dist/Editor.css +66 -118
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +4 -2
- package/dist/cjs/Editor.js +50 -39
- package/dist/cjs/components/AbstractComponent.d.ts +7 -0
- package/dist/cjs/components/AbstractComponent.js +7 -5
- package/dist/cjs/components/util/StrokeSmoother.d.ts +0 -1
- package/dist/cjs/components/util/StrokeSmoother.js +6 -17
- package/dist/cjs/localizations/es.js +11 -22
- package/dist/cjs/rendering/Display.d.ts +2 -0
- package/dist/cjs/rendering/Display.js +4 -0
- package/dist/cjs/toolbar/DropdownToolbar.d.ts +3 -1
- package/dist/cjs/toolbar/DropdownToolbar.js +2 -0
- package/dist/cjs/toolbar/EdgeToolbar.js +6 -5
- package/dist/cjs/toolbar/widgets/BaseWidget.js +2 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget.js +11 -4
- package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +10 -0
- package/dist/cjs/util/addLongPressOrHoverCssClasses.d.ts +10 -0
- package/dist/cjs/util/addLongPressOrHoverCssClasses.js +34 -0
- package/dist/cjs/util/fileToBase64Url.d.ts +9 -0
- package/dist/cjs/util/fileToBase64Url.js +39 -0
- package/dist/cjs/util/fileToBase64Url.test.d.ts +1 -0
- package/dist/cjs/util/listenForLongPressOrHover.d.ts +13 -0
- package/dist/cjs/util/listenForLongPressOrHover.js +80 -0
- package/dist/cjs/util/listenForLongPressOrHover.test.d.ts +1 -0
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +4 -2
- package/dist/mjs/Editor.mjs +50 -39
- package/dist/mjs/components/AbstractComponent.d.ts +7 -0
- package/dist/mjs/components/AbstractComponent.mjs +7 -5
- package/dist/mjs/components/util/StrokeSmoother.d.ts +0 -1
- package/dist/mjs/components/util/StrokeSmoother.mjs +6 -17
- package/dist/mjs/localizations/es.mjs +11 -22
- package/dist/mjs/rendering/Display.d.ts +2 -0
- package/dist/mjs/rendering/Display.mjs +4 -0
- package/dist/mjs/toolbar/DropdownToolbar.d.ts +3 -1
- package/dist/mjs/toolbar/DropdownToolbar.mjs +2 -0
- package/dist/mjs/toolbar/EdgeToolbar.mjs +6 -5
- package/dist/mjs/toolbar/widgets/BaseWidget.mjs +2 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +11 -4
- package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +10 -0
- package/dist/mjs/util/addLongPressOrHoverCssClasses.d.ts +10 -0
- package/dist/mjs/util/addLongPressOrHoverCssClasses.mjs +29 -0
- package/dist/mjs/util/fileToBase64Url.d.ts +9 -0
- package/dist/mjs/util/fileToBase64Url.mjs +37 -0
- package/dist/mjs/util/fileToBase64Url.test.d.ts +1 -0
- package/dist/mjs/util/listenForLongPressOrHover.d.ts +13 -0
- package/dist/mjs/util/listenForLongPressOrHover.mjs +78 -0
- package/dist/mjs/util/listenForLongPressOrHover.test.d.ts +1 -0
- package/dist/mjs/version.mjs +1 -1
- package/package.json +2 -2
- package/src/toolbar/EdgeToolbar.scss +19 -97
- package/src/toolbar/utils/labelVisibleOnHover.scss +114 -0
- package/src/toolbar/widgets/components/makeGridSelector.scss +1 -0
- package/dist/cjs/util/fileToBase64.d.ts +0 -3
- package/dist/cjs/util/fileToBase64.js +0 -15
- package/dist/mjs/util/fileToBase64.d.ts +0 -3
- package/dist/mjs/util/fileToBase64.mjs +0 -13
@@ -143,8 +143,10 @@ class EdgeToolbar extends AbstractToolbar_1.default {
|
|
143
143
|
this.sidebarContainer.style.animation = `${animationProperties} ${constants_1.toolbarCSSPrefix}-edgemenu-transition-in`;
|
144
144
|
this.menuContainer.style.animation = `${animationProperties} ${constants_1.toolbarCSSPrefix}-edgemenu-container-transition-in`;
|
145
145
|
this.menuContainer.style.opacity = '1';
|
146
|
-
// Focus the close button when first shown
|
147
|
-
|
146
|
+
// Focus the close button when first shown, but prevent scroll because the button
|
147
|
+
// is likely at the bottom of the screen (and we want the full sidebar to remain
|
148
|
+
// visible).
|
149
|
+
this.closeButton.focus({ preventScroll: true, });
|
148
150
|
}
|
149
151
|
else {
|
150
152
|
this.closeColorPickers();
|
@@ -272,9 +274,8 @@ class EdgeToolbar extends AbstractToolbar_1.default {
|
|
272
274
|
if (event.target === this.menuContainer) {
|
273
275
|
if (eventName === 'pointerdown') {
|
274
276
|
this.sidebarVisible.set(false);
|
275
|
-
|
276
|
-
|
277
|
-
this.editor.focus();
|
277
|
+
// A delay seems necessary for the editor
|
278
|
+
setTimeout(() => this.editor.focus(), 0);
|
278
279
|
}
|
279
280
|
return true;
|
280
281
|
}
|
@@ -20,6 +20,7 @@ const ToolbarShortcutHandler_1 = __importDefault(require("../../tools/ToolbarSho
|
|
20
20
|
const inputEvents_1 = require("../../inputEvents");
|
21
21
|
const constants_1 = require("../constants");
|
22
22
|
const DropdownLayoutManager_1 = __importDefault(require("./layout/DropdownLayoutManager"));
|
23
|
+
const addLongPressOrHoverCssClasses_1 = __importDefault(require("../../util/addLongPressOrHoverCssClasses"));
|
23
24
|
/**
|
24
25
|
* A set of labels that allow toolbar themes to treat buttons differently.
|
25
26
|
*/
|
@@ -66,6 +67,7 @@ class BaseWidget {
|
|
66
67
|
this.button.oncontextmenu = event => {
|
67
68
|
event.preventDefault();
|
68
69
|
};
|
70
|
+
(0, addLongPressOrHoverCssClasses_1.default)(this.button);
|
69
71
|
}
|
70
72
|
/**
|
71
73
|
* Should return a constant true or false value. If true (the default),
|
@@ -9,7 +9,7 @@ const EditorImage_1 = __importDefault(require("../../image/EditorImage"));
|
|
9
9
|
const uniteCommands_1 = __importDefault(require("../../commands/uniteCommands"));
|
10
10
|
const SelectionTool_1 = __importDefault(require("../../tools/SelectionTool/SelectionTool"));
|
11
11
|
const math_1 = require("@js-draw/math");
|
12
|
-
const
|
12
|
+
const fileToBase64Url_1 = __importDefault(require("../../util/fileToBase64Url"));
|
13
13
|
const BaseWidget_1 = __importDefault(require("./BaseWidget"));
|
14
14
|
const types_1 = require("../../types");
|
15
15
|
const constants_1 = require("../constants");
|
@@ -111,11 +111,13 @@ class InsertImageWidget extends BaseWidget_1.default {
|
|
111
111
|
this.imagePreview.style.display = 'block';
|
112
112
|
const image = files[0];
|
113
113
|
let data = null;
|
114
|
+
let errorMessage = null;
|
114
115
|
try {
|
115
|
-
data = await (0,
|
116
|
+
data = await (0, fileToBase64Url_1.default)(image);
|
116
117
|
}
|
117
|
-
catch (
|
118
|
-
|
118
|
+
catch (error) {
|
119
|
+
console.error('Image load error', error);
|
120
|
+
errorMessage = this.localizationTable.imageLoadError(error);
|
119
121
|
}
|
120
122
|
if (data) {
|
121
123
|
this.image = ImageWrapper.fromSrcAndPreview(data, this.imagePreview, () => this.onImageDataUpdate());
|
@@ -124,6 +126,11 @@ class InsertImageWidget extends BaseWidget_1.default {
|
|
124
126
|
this.image = null;
|
125
127
|
}
|
126
128
|
this.onImageDataUpdate();
|
129
|
+
// Show the error after image update callbacks to ensure it is
|
130
|
+
// actually shown.
|
131
|
+
if (errorMessage) {
|
132
|
+
this.statusView.innerText = errorMessage;
|
133
|
+
}
|
127
134
|
});
|
128
135
|
altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
|
129
136
|
actionButtonRow.replaceChildren(this.submitButton);
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
6
|
const ReactiveValue_1 = require("../../../util/ReactiveValue");
|
7
7
|
const stopPropagationOfScrollingWheelEvents_1 = __importDefault(require("../../../util/stopPropagationOfScrollingWheelEvents"));
|
8
|
+
const addLongPressOrHoverCssClasses_1 = __importDefault(require("../../../util/addLongPressOrHoverCssClasses"));
|
8
9
|
const constants_1 = require("../../constants");
|
9
10
|
let idCounter = 0;
|
10
11
|
/**
|
@@ -36,6 +37,9 @@ labelText, defaultId, choices) => {
|
|
36
37
|
const button = document.createElement('input');
|
37
38
|
button.type = 'radio';
|
38
39
|
button.id = `${constants_1.toolbarCSSPrefix}-grid-select-button-${idCounter++}`;
|
40
|
+
// Some toolbars only show the label on hover. Having long press or hover
|
41
|
+
// CSS classes are helpful here.
|
42
|
+
(0, addLongPressOrHoverCssClasses_1.default)(buttonContainer);
|
39
43
|
// Clicking any part of labelContainer triggers the radio button.
|
40
44
|
const labelContainer = document.createElement('label');
|
41
45
|
const rebuildLabel = () => {
|
@@ -81,6 +85,12 @@ labelText, defaultId, choices) => {
|
|
81
85
|
button.onblur = () => {
|
82
86
|
buttonContainer.classList.remove('focus-visible');
|
83
87
|
};
|
88
|
+
// Prevent the right-click menu from being shown on long-press
|
89
|
+
// (important for some toolbars that use long-press gestures to
|
90
|
+
// show grid selector labels).
|
91
|
+
buttonContainer.oncontextmenu = event => {
|
92
|
+
event.preventDefault();
|
93
|
+
};
|
84
94
|
buttonContainer.replaceChildren(button, labelContainer);
|
85
95
|
menuContainer.appendChild(buttonContainer);
|
86
96
|
// Set whether the current button is checked
|
@@ -0,0 +1,10 @@
|
|
1
|
+
/**
|
2
|
+
* When a pointer is inside `element`, after a delay, adds the `has-long-press-or-hover`
|
3
|
+
* CSS class to `element`.
|
4
|
+
*
|
5
|
+
* When no pointers are inside `element`, adds the CSS class `no-long-press-or-hover`.
|
6
|
+
*/
|
7
|
+
declare const addLongPressOrHoverCssClasses: (element: HTMLElement) => {
|
8
|
+
removeEventListeners: () => void;
|
9
|
+
};
|
10
|
+
export default addLongPressOrHoverCssClasses;
|
@@ -0,0 +1,34 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const listenForLongPressOrHover_1 = __importDefault(require("./listenForLongPressOrHover"));
|
7
|
+
/**
|
8
|
+
* When a pointer is inside `element`, after a delay, adds the `has-long-press-or-hover`
|
9
|
+
* CSS class to `element`.
|
10
|
+
*
|
11
|
+
* When no pointers are inside `element`, adds the CSS class `no-long-press-or-hover`.
|
12
|
+
*/
|
13
|
+
const addLongPressOrHoverCssClasses = (element) => {
|
14
|
+
const hasLongPressClass = 'has-long-press-or-hover';
|
15
|
+
const noLongPressClass = 'no-long-press-or-hover';
|
16
|
+
element.classList.add('no-long-press-or-hover');
|
17
|
+
const { removeListeners } = (0, listenForLongPressOrHover_1.default)(element, {
|
18
|
+
onStart() {
|
19
|
+
element.classList.remove(noLongPressClass);
|
20
|
+
element.classList.add(hasLongPressClass);
|
21
|
+
},
|
22
|
+
onEnd() {
|
23
|
+
element.classList.add(noLongPressClass);
|
24
|
+
element.classList.remove(hasLongPressClass);
|
25
|
+
},
|
26
|
+
});
|
27
|
+
return {
|
28
|
+
removeEventListeners: () => {
|
29
|
+
element.classList.remove(noLongPressClass);
|
30
|
+
removeListeners();
|
31
|
+
},
|
32
|
+
};
|
33
|
+
};
|
34
|
+
exports.default = addLongPressOrHoverCssClasses;
|
@@ -0,0 +1,9 @@
|
|
1
|
+
export interface FileToBase64UrlOptions {
|
2
|
+
onprogress?: (evt: ProgressEvent<FileReader>) => void;
|
3
|
+
onWarning?: (message: string, error: any) => void;
|
4
|
+
}
|
5
|
+
/**
|
6
|
+
* Converts `file` to a base64 data URL.
|
7
|
+
*/
|
8
|
+
declare const fileToBase64Url: (file: Blob, options?: FileToBase64UrlOptions) => Promise<string | null>;
|
9
|
+
export default fileToBase64Url;
|
@@ -0,0 +1,39 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
/**
|
4
|
+
* Converts `file` to a base64 data URL.
|
5
|
+
*/
|
6
|
+
const fileToBase64Url = async (file, options = {}) => {
|
7
|
+
try {
|
8
|
+
const reader = new FileReader();
|
9
|
+
return await new Promise((resolve, reject) => {
|
10
|
+
reader.onload = () => resolve(reader.result);
|
11
|
+
reader.onerror = reject;
|
12
|
+
reader.onabort = reject;
|
13
|
+
reader.onprogress = (evt) => {
|
14
|
+
options.onprogress?.(evt);
|
15
|
+
};
|
16
|
+
reader.readAsDataURL(file);
|
17
|
+
});
|
18
|
+
}
|
19
|
+
catch (error) {
|
20
|
+
// Files can fail to load with a FileReader in some cases. For example,
|
21
|
+
// in iOS Lockdown mode, where FileReader is unavailable.
|
22
|
+
(options.onWarning ?? console.warn)('Unable to convert file to base64 with a FileReader: ', error);
|
23
|
+
const arrayBuffer = await file.arrayBuffer();
|
24
|
+
const array = new Uint8Array(arrayBuffer);
|
25
|
+
// step: must be divisible by 3 (3 bytes = 4 base64 numerals)
|
26
|
+
// If too large, this will fail (String.fromCharCode accepts a limited
|
27
|
+
// number of arguments).
|
28
|
+
const step = 30;
|
29
|
+
const result = [];
|
30
|
+
for (let i = 0; i < array.length; i += step) {
|
31
|
+
// btoa accepts only characters with byte value 0-255 (which can be created
|
32
|
+
// with String.fromCharCode)
|
33
|
+
const stringByteArray = String.fromCharCode(...array.slice(i, i + step));
|
34
|
+
result.push(btoa(stringByteArray));
|
35
|
+
}
|
36
|
+
return `data:${file.type ?? 'image/*'};base64,${result.join('')}`;
|
37
|
+
}
|
38
|
+
};
|
39
|
+
exports.default = fileToBase64Url;
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,13 @@
|
|
1
|
+
type Options = {
|
2
|
+
onStart: () => void;
|
3
|
+
onEnd: () => void;
|
4
|
+
longPressTimeout?: number;
|
5
|
+
};
|
6
|
+
/**
|
7
|
+
* Calls `options.onStart` at the start of a long press or hover.
|
8
|
+
* Calls `options.onEnd` when no pointers are within the container.
|
9
|
+
*/
|
10
|
+
declare const listenForLongPressOrHover: (target: HTMLElement, options: Options) => {
|
11
|
+
removeListeners: () => void;
|
12
|
+
};
|
13
|
+
export default listenForLongPressOrHover;
|
@@ -0,0 +1,80 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
/**
|
4
|
+
* Calls `options.onStart` at the start of a long press or hover.
|
5
|
+
* Calls `options.onEnd` when no pointers are within the container.
|
6
|
+
*/
|
7
|
+
const listenForLongPressOrHover = (target, options) => {
|
8
|
+
const pointersInside = new Map();
|
9
|
+
let timeoutId = null;
|
10
|
+
let isLongPressInProgress = false;
|
11
|
+
const updateTimeout = () => {
|
12
|
+
if (pointersInside.size === 0) {
|
13
|
+
if (isLongPressInProgress) {
|
14
|
+
isLongPressInProgress = false;
|
15
|
+
options.onEnd();
|
16
|
+
}
|
17
|
+
else if (timeoutId !== null) {
|
18
|
+
clearTimeout(timeoutId);
|
19
|
+
timeoutId = null;
|
20
|
+
}
|
21
|
+
}
|
22
|
+
else {
|
23
|
+
const nowTime = Date.now();
|
24
|
+
let timeSinceFirstPointer = 0; // ms
|
25
|
+
for (const record of pointersInside.values()) {
|
26
|
+
const timeSince = nowTime - record.timeEnter;
|
27
|
+
timeSinceFirstPointer = Math.max(timeSince, timeSinceFirstPointer);
|
28
|
+
}
|
29
|
+
const longPressTimeout = options.longPressTimeout ?? 700; // ms
|
30
|
+
if (timeoutId !== null) {
|
31
|
+
clearTimeout(timeoutId);
|
32
|
+
timeoutId = null;
|
33
|
+
}
|
34
|
+
const timeLeft = longPressTimeout - timeSinceFirstPointer;
|
35
|
+
if (timeLeft <= 0) {
|
36
|
+
options.onStart();
|
37
|
+
isLongPressInProgress = true;
|
38
|
+
}
|
39
|
+
else {
|
40
|
+
timeoutId = setTimeout(() => {
|
41
|
+
timeoutId = null;
|
42
|
+
updateTimeout();
|
43
|
+
}, timeLeft);
|
44
|
+
}
|
45
|
+
}
|
46
|
+
};
|
47
|
+
// Detects long press
|
48
|
+
const pointerEventListener = (event) => {
|
49
|
+
const eventRecord = {
|
50
|
+
timeEnter: Date.now(),
|
51
|
+
};
|
52
|
+
if (event.type === 'pointerenter') {
|
53
|
+
pointersInside.set(event.pointerId, eventRecord);
|
54
|
+
}
|
55
|
+
else if (event.type === 'pointerleave' || event.type === 'pointercancel') {
|
56
|
+
// In some cases (for example, a click with a stylus on Android/Chrome), moving the pen
|
57
|
+
// over the target, clicking, then moving the pen out of the target produces input
|
58
|
+
// similar to this:
|
59
|
+
// - pointerenter (pointerId: 4)
|
60
|
+
// - pointerleave (pointerId: 4)
|
61
|
+
// - pointerenter (pointerId: 6)
|
62
|
+
// - pointerenter (pointerId: 1)
|
63
|
+
// - pointerleave (pointerId: 6)
|
64
|
+
// Observe that no pointerleave event was fired for the pointer with ID 1.
|
65
|
+
pointersInside.clear();
|
66
|
+
}
|
67
|
+
updateTimeout();
|
68
|
+
};
|
69
|
+
target.addEventListener('pointerenter', pointerEventListener);
|
70
|
+
target.addEventListener('pointerleave', pointerEventListener);
|
71
|
+
target.addEventListener('pointercancel', pointerEventListener);
|
72
|
+
return {
|
73
|
+
removeListeners: () => {
|
74
|
+
target.removeEventListener('pointerenter', pointerEventListener);
|
75
|
+
target.removeEventListener('pointerleave', pointerEventListener);
|
76
|
+
target.removeEventListener('pointercancel', pointerEventListener);
|
77
|
+
},
|
78
|
+
};
|
79
|
+
};
|
80
|
+
exports.default = listenForLongPressOrHover;
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
package/dist/cjs/version.js
CHANGED
package/dist/mjs/Editor.d.ts
CHANGED
@@ -253,6 +253,8 @@ export declare class Editor {
|
|
253
253
|
addToolbar(defaultLayout?: boolean): AbstractToolbar;
|
254
254
|
private registerListeners;
|
255
255
|
private updateEditorSizeVariables;
|
256
|
+
/** @internal */
|
257
|
+
protected handleHTMLWheelEvent(event: WheelEvent): boolean | undefined;
|
256
258
|
private pointers;
|
257
259
|
private getPointerList;
|
258
260
|
/**
|
@@ -312,9 +314,9 @@ export declare class Editor {
|
|
312
314
|
remove: () => void;
|
313
315
|
};
|
314
316
|
/** @internal */
|
315
|
-
protected handleHTMLKeyDownEvent(htmlEvent: KeyboardEvent):
|
317
|
+
protected handleHTMLKeyDownEvent(htmlEvent: KeyboardEvent): boolean;
|
316
318
|
/** @internal */
|
317
|
-
protected handleHTMLKeyUpEvent(htmlEvent: KeyboardEvent):
|
319
|
+
protected handleHTMLKeyUpEvent(htmlEvent: KeyboardEvent): boolean;
|
318
320
|
/**
|
319
321
|
* Adds event listners for keypresses (and drop events) on `elem` and forwards those
|
320
322
|
* events to the editor.
|
package/dist/mjs/Editor.mjs
CHANGED
@@ -13,7 +13,7 @@ import getLocalizationTable from './localizations/getLocalizationTable.mjs';
|
|
13
13
|
import IconProvider from './toolbar/IconProvider.mjs';
|
14
14
|
import CanvasRenderer from './rendering/renderers/CanvasRenderer.mjs';
|
15
15
|
import untilNextAnimationFrame from './util/untilNextAnimationFrame.mjs';
|
16
|
-
import
|
16
|
+
import fileToBase64Url from './util/fileToBase64Url.mjs';
|
17
17
|
import uniteCommands from './commands/uniteCommands.mjs';
|
18
18
|
import SelectionTool from './tools/SelectionTool/SelectionTool.mjs';
|
19
19
|
import Erase from './commands/Erase.mjs';
|
@@ -239,41 +239,7 @@ export class Editor {
|
|
239
239
|
this.handleKeyEventsFrom(this.renderingRegion);
|
240
240
|
this.handlePointerEventsFrom(this.accessibilityAnnounceArea);
|
241
241
|
this.container.addEventListener('wheel', evt => {
|
242
|
-
|
243
|
-
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
244
|
-
// pinch-zooming.
|
245
|
-
if (!evt.ctrlKey && !evt.metaKey) {
|
246
|
-
if (!this.settings.wheelEventsEnabled) {
|
247
|
-
return;
|
248
|
-
}
|
249
|
-
else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
|
250
|
-
const focusedChild = this.container.querySelector(':focus');
|
251
|
-
if (!focusedChild) {
|
252
|
-
return;
|
253
|
-
}
|
254
|
-
}
|
255
|
-
}
|
256
|
-
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
257
|
-
delta = delta.times(15);
|
258
|
-
}
|
259
|
-
else if (evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
260
|
-
delta = delta.times(100);
|
261
|
-
}
|
262
|
-
if (evt.ctrlKey || evt.metaKey) {
|
263
|
-
delta = Vec3.of(0, 0, evt.deltaY);
|
264
|
-
}
|
265
|
-
// Ensure that `pos` is relative to `this.renderingRegion`
|
266
|
-
const bbox = this.renderingRegion.getBoundingClientRect();
|
267
|
-
const pos = Vec2.of(evt.clientX, evt.clientY).minus(Vec2.of(bbox.left, bbox.top));
|
268
|
-
if (this.toolController.dispatchInputEvent({
|
269
|
-
kind: InputEvtType.WheelEvt,
|
270
|
-
delta,
|
271
|
-
screenPos: pos,
|
272
|
-
})) {
|
273
|
-
evt.preventDefault();
|
274
|
-
return true;
|
275
|
-
}
|
276
|
-
return false;
|
242
|
+
this.handleHTMLWheelEvent(evt);
|
277
243
|
});
|
278
244
|
const handleResize = () => {
|
279
245
|
this.viewport.updateScreenSize(Vec2.of(this.display.width, this.display.height));
|
@@ -317,6 +283,44 @@ export class Editor {
|
|
317
283
|
this.container.style.setProperty('--editor-current-display-width-px', `${this.renderingRegion.clientWidth}px`);
|
318
284
|
this.container.style.setProperty('--editor-current-display-height-px', `${this.renderingRegion.clientHeight}px`);
|
319
285
|
}
|
286
|
+
/** @internal */
|
287
|
+
handleHTMLWheelEvent(event) {
|
288
|
+
let delta = Vec3.of(event.deltaX, event.deltaY, event.deltaZ);
|
289
|
+
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
290
|
+
// pinch-zooming.
|
291
|
+
if (!event.ctrlKey && !event.metaKey) {
|
292
|
+
if (!this.settings.wheelEventsEnabled) {
|
293
|
+
return;
|
294
|
+
}
|
295
|
+
else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
|
296
|
+
const focusedChild = this.container.querySelector(':focus');
|
297
|
+
if (!focusedChild) {
|
298
|
+
return;
|
299
|
+
}
|
300
|
+
}
|
301
|
+
}
|
302
|
+
if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
303
|
+
delta = delta.times(15);
|
304
|
+
}
|
305
|
+
else if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
306
|
+
delta = delta.times(100);
|
307
|
+
}
|
308
|
+
if (event.ctrlKey || event.metaKey) {
|
309
|
+
delta = Vec3.of(0, 0, event.deltaY);
|
310
|
+
}
|
311
|
+
// Ensure that `pos` is relative to `this.renderingRegion`
|
312
|
+
const bbox = this.renderingRegion.getBoundingClientRect();
|
313
|
+
const pos = Vec2.of(event.clientX, event.clientY).minus(Vec2.of(bbox.left, bbox.top));
|
314
|
+
if (this.toolController.dispatchInputEvent({
|
315
|
+
kind: InputEvtType.WheelEvt,
|
316
|
+
delta,
|
317
|
+
screenPos: pos,
|
318
|
+
})) {
|
319
|
+
event.preventDefault();
|
320
|
+
return true;
|
321
|
+
}
|
322
|
+
return false;
|
323
|
+
}
|
320
324
|
getPointerList() {
|
321
325
|
const nowTime = performance.now();
|
322
326
|
const res = [];
|
@@ -443,7 +447,7 @@ export class Editor {
|
|
443
447
|
this.showLoadingWarning(evt.loaded / evt.total);
|
444
448
|
};
|
445
449
|
try {
|
446
|
-
const data = await
|
450
|
+
const data = await fileToBase64Url(file, { onprogress });
|
447
451
|
if (data && this.toolController.dispatchInputEvent({
|
448
452
|
kind: InputEvtType.PasteEvent,
|
449
453
|
mime: fileType,
|
@@ -625,14 +629,18 @@ export class Editor {
|
|
625
629
|
const event = keyPressEventFromHTMLEvent(htmlEvent);
|
626
630
|
if (this.toolController.dispatchInputEvent(event)) {
|
627
631
|
htmlEvent.preventDefault();
|
632
|
+
return true;
|
628
633
|
}
|
629
634
|
else if (event.key === 't' || event.key === 'T') {
|
630
635
|
htmlEvent.preventDefault();
|
631
636
|
this.display.rerenderAsText();
|
637
|
+
return true;
|
632
638
|
}
|
633
639
|
else if (event.key === 'Escape') {
|
634
640
|
this.renderingRegion.blur();
|
641
|
+
return true;
|
635
642
|
}
|
643
|
+
return false;
|
636
644
|
}
|
637
645
|
/** @internal */
|
638
646
|
handleHTMLKeyUpEvent(htmlEvent) {
|
@@ -640,7 +648,9 @@ export class Editor {
|
|
640
648
|
const event = keyUpEventFromHTMLEvent(htmlEvent);
|
641
649
|
if (this.toolController.dispatchInputEvent(event)) {
|
642
650
|
htmlEvent.preventDefault();
|
651
|
+
return true;
|
643
652
|
}
|
653
|
+
return false;
|
644
654
|
}
|
645
655
|
/**
|
646
656
|
* Adds event listners for keypresses (and drop events) on `elem` and forwards those
|
@@ -1160,8 +1170,9 @@ export class Editor {
|
|
1160
1170
|
` ${this.viewport.getScaleFactor()}x zoom, ${180 / Math.PI * this.viewport.getRotationAngle()}° rotation`,
|
1161
1171
|
` ${this.image.estimateNumElements()} components`,
|
1162
1172
|
` auto-resize: ${this.image.getAutoresizeEnabled() ? 'enabled' : 'disabled'}`,
|
1163
|
-
` ${this.getImportExportRect().w}x${this.getImportExportRect().h}
|
1164
|
-
` ${screenSize.x}x${screenSize.y}
|
1173
|
+
` image size: ${this.getImportExportRect().w}x${this.getImportExportRect().h}`,
|
1174
|
+
` screen size: ${screenSize.x}x${screenSize.y}`,
|
1175
|
+
` device pixel ratio: ${this.display.getDevicePixelRatio()}`,
|
1165
1176
|
' cache:',
|
1166
1177
|
` ${this.display.getCache().getDebugInfo()
|
1167
1178
|
// Indent
|
@@ -114,6 +114,13 @@ export default abstract class AbstractComponent {
|
|
114
114
|
intersectsRect(rect: Rect2): boolean;
|
115
115
|
protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
|
116
116
|
protected abstract applyTransformation(affineTransfm: Mat33): void;
|
117
|
+
/**
|
118
|
+
* Returns a command that, when applied, transforms this by [affineTransfm] and
|
119
|
+
* updates the editor.
|
120
|
+
*
|
121
|
+
* The transformed component is also moved to the top (use
|
122
|
+
* {@link AbstractComponent.setZIndexAndTransformBy} to avoid this behavior).
|
123
|
+
*/
|
117
124
|
transformBy(affineTransfm: Mat33): SerializableCommand;
|
118
125
|
setZIndex(newZIndex: number): SerializableCommand;
|
119
126
|
/**
|
@@ -136,11 +136,13 @@ class AbstractComponent {
|
|
136
136
|
const testLines = rect.getEdges();
|
137
137
|
return testLines.some(edge => this.intersects(edge));
|
138
138
|
}
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
139
|
+
/**
|
140
|
+
* Returns a command that, when applied, transforms this by [affineTransfm] and
|
141
|
+
* updates the editor.
|
142
|
+
*
|
143
|
+
* The transformed component is also moved to the top (use
|
144
|
+
* {@link AbstractComponent.setZIndexAndTransformBy} to avoid this behavior).
|
145
|
+
*/
|
144
146
|
transformBy(affineTransfm) {
|
145
147
|
return new AbstractComponent.TransformElementCommand(affineTransfm, this.getId(), this);
|
146
148
|
}
|
@@ -12,7 +12,6 @@ export class StrokeSmoother {
|
|
12
12
|
this.maxFitAllowed = maxFitAllowed;
|
13
13
|
this.onCurveAdded = onCurveAdded;
|
14
14
|
this.isFirstSegment = true;
|
15
|
-
this.centerOfMass = null;
|
16
15
|
this.lastExitingVec = null;
|
17
16
|
this.currentCurve = null;
|
18
17
|
this.lastPoint = this.startPoint;
|
@@ -58,7 +57,6 @@ export class StrokeSmoother {
|
|
58
57
|
this.buffer[this.buffer.length - 2], lastPoint,
|
59
58
|
];
|
60
59
|
this.currentCurve = null;
|
61
|
-
this.centerOfMass = null;
|
62
60
|
this.isFirstSegment = false;
|
63
61
|
}
|
64
62
|
// Returns [upper curve, connector, lower curve]
|
@@ -106,20 +104,10 @@ export class StrokeSmoother {
|
|
106
104
|
if (shouldSnapToInitial) {
|
107
105
|
return;
|
108
106
|
}
|
109
|
-
if (!this.centerOfMass) {
|
110
|
-
this.centerOfMass = newPoint.pos;
|
111
|
-
}
|
112
|
-
else {
|
113
|
-
this.centerOfMass = this.centerOfMass
|
114
|
-
.times(this.buffer.length)
|
115
|
-
.plus(newPoint.pos).times(1 / (this.buffer.length + 1));
|
116
|
-
}
|
117
|
-
const toCenterOfMass = this.centerOfMass.minus(newPoint.pos);
|
118
107
|
const deltaTimeSeconds = deltaTime / 1000;
|
119
108
|
const velocity = newPoint.pos.minus(this.lastPoint.pos).times(1 / deltaTimeSeconds);
|
120
109
|
// TODO: Do we need momentum smoothing? (this.momentum.lerp(velocity, 0.9);)
|
121
|
-
|
122
|
-
this.momentum = velocity.plus(toCenterOfMass.times(k));
|
110
|
+
this.momentum = velocity;
|
123
111
|
}
|
124
112
|
const lastPoint = this.lastPoint ?? newPoint;
|
125
113
|
this.lastPoint = newPoint;
|
@@ -156,7 +144,7 @@ export class StrokeSmoother {
|
|
156
144
|
}
|
157
145
|
let exitingVec = this.computeExitingVec();
|
158
146
|
// Find the intersection between the entering vector and the exiting vector
|
159
|
-
const maxRelativeLength = 1.
|
147
|
+
const maxRelativeLength = 1.7;
|
160
148
|
const segmentStart = this.buffer[0];
|
161
149
|
const segmentEnd = newPoint.pos;
|
162
150
|
const startEndDist = segmentEnd.minus(segmentStart).magnitude();
|
@@ -185,9 +173,10 @@ export class StrokeSmoother {
|
|
185
173
|
.lerp(segmentStart.plus(enteringVec.times(startEndDist)), 0.25);
|
186
174
|
}
|
187
175
|
// Equal to an endpoint?
|
188
|
-
if (segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
|
189
|
-
// Position the control point
|
190
|
-
|
176
|
+
else if (segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
|
177
|
+
// Position the control point closer to the first -- the connecting
|
178
|
+
// segment will be roughly a line.
|
179
|
+
controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 5));
|
191
180
|
}
|
192
181
|
console.assert(!segmentStart.eq(controlPoint, 1e-11), 'Start and control points are equal!');
|
193
182
|
console.assert(!controlPoint.eq(segmentEnd, 1e-11), 'Control and end points are equal!');
|