js-draw 1.11.2 → 1.13.1
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 +61 -39
- package/dist/cjs/components/AbstractComponent.d.ts +7 -0
- package/dist/cjs/components/AbstractComponent.js +7 -5
- package/dist/cjs/components/util/StrokeSmoother.js +11 -4
- 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/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/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/util/mitLicenseAttribution.d.ts +2 -0
- package/dist/cjs/util/mitLicenseAttribution.js +28 -0
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +4 -2
- package/dist/mjs/Editor.mjs +61 -39
- package/dist/mjs/components/AbstractComponent.d.ts +7 -0
- package/dist/mjs/components/AbstractComponent.mjs +7 -5
- package/dist/mjs/components/util/StrokeSmoother.mjs +11 -4
- 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/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/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/util/mitLicenseAttribution.d.ts +2 -0
- package/dist/mjs/util/mitLicenseAttribution.mjs +26 -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
@@ -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),
|
@@ -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,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 {};
|
@@ -0,0 +1,28 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
const mitLicenseAttribution = (copyright) => {
|
4
|
+
const removeSingleLineBreaks = (text) => text.replace(/([^\n])[\n]([^\n])/g, '$1 $2');
|
5
|
+
return removeSingleLineBreaks(`
|
6
|
+
MIT License
|
7
|
+
|
8
|
+
Copyright (c) ${copyright}
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
12
|
+
in the Software without restriction, including without limitation the rights
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
15
|
+
furnished to do so, subject to the following conditions:
|
16
|
+
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
18
|
+
copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
+
SOFTWARE.`);
|
27
|
+
};
|
28
|
+
exports.default = mitLicenseAttribution;
|
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
@@ -28,6 +28,7 @@ import version from './version.mjs';
|
|
28
28
|
import { editorImageToSVGSync, editorImageToSVGAsync } from './image/export/editorImageToSVG.mjs';
|
29
29
|
import { MutableReactiveValue } from './util/ReactiveValue.mjs';
|
30
30
|
import listenForKeyboardEventsFrom from './util/listenForKeyboardEventsFrom.mjs';
|
31
|
+
import mitLicenseAttribution from './util/mitLicenseAttribution.mjs';
|
31
32
|
/**
|
32
33
|
* The main entrypoint for the full editor.
|
33
34
|
*
|
@@ -238,41 +239,7 @@ export class Editor {
|
|
238
239
|
this.handleKeyEventsFrom(this.renderingRegion);
|
239
240
|
this.handlePointerEventsFrom(this.accessibilityAnnounceArea);
|
240
241
|
this.container.addEventListener('wheel', evt => {
|
241
|
-
|
242
|
-
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
243
|
-
// pinch-zooming.
|
244
|
-
if (!evt.ctrlKey && !evt.metaKey) {
|
245
|
-
if (!this.settings.wheelEventsEnabled) {
|
246
|
-
return;
|
247
|
-
}
|
248
|
-
else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
|
249
|
-
const focusedChild = this.container.querySelector(':focus');
|
250
|
-
if (!focusedChild) {
|
251
|
-
return;
|
252
|
-
}
|
253
|
-
}
|
254
|
-
}
|
255
|
-
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
256
|
-
delta = delta.times(15);
|
257
|
-
}
|
258
|
-
else if (evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
259
|
-
delta = delta.times(100);
|
260
|
-
}
|
261
|
-
if (evt.ctrlKey || evt.metaKey) {
|
262
|
-
delta = Vec3.of(0, 0, evt.deltaY);
|
263
|
-
}
|
264
|
-
// Ensure that `pos` is relative to `this.renderingRegion`
|
265
|
-
const bbox = this.renderingRegion.getBoundingClientRect();
|
266
|
-
const pos = Vec2.of(evt.clientX, evt.clientY).minus(Vec2.of(bbox.left, bbox.top));
|
267
|
-
if (this.toolController.dispatchInputEvent({
|
268
|
-
kind: InputEvtType.WheelEvt,
|
269
|
-
delta,
|
270
|
-
screenPos: pos,
|
271
|
-
})) {
|
272
|
-
evt.preventDefault();
|
273
|
-
return true;
|
274
|
-
}
|
275
|
-
return false;
|
242
|
+
this.handleHTMLWheelEvent(evt);
|
276
243
|
});
|
277
244
|
const handleResize = () => {
|
278
245
|
this.viewport.updateScreenSize(Vec2.of(this.display.width, this.display.height));
|
@@ -316,6 +283,44 @@ export class Editor {
|
|
316
283
|
this.container.style.setProperty('--editor-current-display-width-px', `${this.renderingRegion.clientWidth}px`);
|
317
284
|
this.container.style.setProperty('--editor-current-display-height-px', `${this.renderingRegion.clientHeight}px`);
|
318
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
|
+
}
|
319
324
|
getPointerList() {
|
320
325
|
const nowTime = performance.now();
|
321
326
|
const res = [];
|
@@ -624,14 +629,18 @@ export class Editor {
|
|
624
629
|
const event = keyPressEventFromHTMLEvent(htmlEvent);
|
625
630
|
if (this.toolController.dispatchInputEvent(event)) {
|
626
631
|
htmlEvent.preventDefault();
|
632
|
+
return true;
|
627
633
|
}
|
628
634
|
else if (event.key === 't' || event.key === 'T') {
|
629
635
|
htmlEvent.preventDefault();
|
630
636
|
this.display.rerenderAsText();
|
637
|
+
return true;
|
631
638
|
}
|
632
639
|
else if (event.key === 'Escape') {
|
633
640
|
this.renderingRegion.blur();
|
641
|
+
return true;
|
634
642
|
}
|
643
|
+
return false;
|
635
644
|
}
|
636
645
|
/** @internal */
|
637
646
|
handleHTMLKeyUpEvent(htmlEvent) {
|
@@ -639,7 +648,9 @@ export class Editor {
|
|
639
648
|
const event = keyUpEventFromHTMLEvent(htmlEvent);
|
640
649
|
if (this.toolController.dispatchInputEvent(event)) {
|
641
650
|
htmlEvent.preventDefault();
|
651
|
+
return true;
|
642
652
|
}
|
653
|
+
return false;
|
643
654
|
}
|
644
655
|
/**
|
645
656
|
* Adds event listners for keypresses (and drop events) on `elem` and forwards those
|
@@ -1159,8 +1170,9 @@ export class Editor {
|
|
1159
1170
|
` ${this.viewport.getScaleFactor()}x zoom, ${180 / Math.PI * this.viewport.getRotationAngle()}° rotation`,
|
1160
1171
|
` ${this.image.estimateNumElements()} components`,
|
1161
1172
|
` auto-resize: ${this.image.getAutoresizeEnabled() ? 'enabled' : 'disabled'}`,
|
1162
|
-
` ${this.getImportExportRect().w}x${this.getImportExportRect().h}
|
1163
|
-
` ${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()}`,
|
1164
1176
|
' cache:',
|
1165
1177
|
` ${this.display.getCache().getDebugInfo()
|
1166
1178
|
// Indent
|
@@ -1173,9 +1185,19 @@ export class Editor {
|
|
1173
1185
|
text: [
|
1174
1186
|
`This image editor is powered by js-draw v${version.number}.`,
|
1175
1187
|
'',
|
1176
|
-
'js-draw uses
|
1188
|
+
'At runtime, js-draw uses',
|
1177
1189
|
' - The Coloris color picker: https://github.com/mdbassit/Coloris',
|
1178
|
-
' - The bezier.js Bézier curve library: https://github.com/Pomax/bezierjs'
|
1190
|
+
' - The bezier.js Bézier curve library: https://github.com/Pomax/bezierjs',
|
1191
|
+
'',
|
1192
|
+
'Both are licensed under the MIT license:',
|
1193
|
+
'',
|
1194
|
+
'',
|
1195
|
+
'== Coloris ==',
|
1196
|
+
mitLicenseAttribution('2021 Mohammed Bassit'),
|
1197
|
+
'',
|
1198
|
+
'',
|
1199
|
+
'== Bezier.js ==',
|
1200
|
+
mitLicenseAttribution('2023 Mike "Pomax" Kamermans'),
|
1179
1201
|
].join('\n'),
|
1180
1202
|
minimized: true,
|
1181
1203
|
});
|
@@ -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
|
}
|
@@ -144,7 +144,7 @@ export class StrokeSmoother {
|
|
144
144
|
}
|
145
145
|
let exitingVec = this.computeExitingVec();
|
146
146
|
// Find the intersection between the entering vector and the exiting vector
|
147
|
-
const maxRelativeLength =
|
147
|
+
const maxRelativeLength = 1.7;
|
148
148
|
const segmentStart = this.buffer[0];
|
149
149
|
const segmentEnd = newPoint.pos;
|
150
150
|
const startEndDist = segmentEnd.minus(segmentStart).magnitude();
|
@@ -165,11 +165,18 @@ export class StrokeSmoother {
|
|
165
165
|
if (intersection) {
|
166
166
|
controlPoint = intersection.point;
|
167
167
|
}
|
168
|
-
// No intersection
|
169
|
-
if (!controlPoint
|
168
|
+
// No intersection?
|
169
|
+
if (!controlPoint) {
|
170
|
+
// Estimate the control point position based on the entering tangent line
|
171
|
+
controlPoint = segmentStart
|
172
|
+
.lerp(segmentEnd, 0.5)
|
173
|
+
.lerp(segmentStart.plus(enteringVec.times(startEndDist)), 0.25);
|
174
|
+
}
|
175
|
+
// Equal to an endpoint?
|
176
|
+
else if (segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
|
170
177
|
// Position the control point closer to the first -- the connecting
|
171
178
|
// segment will be roughly a line.
|
172
|
-
controlPoint = segmentStart.plus(enteringVec.times(startEndDist /
|
179
|
+
controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 5));
|
173
180
|
}
|
174
181
|
console.assert(!segmentStart.eq(controlPoint, 1e-11), 'Start and control points are equal!');
|
175
182
|
console.assert(!controlPoint.eq(segmentEnd, 1e-11), 'Control and end points are equal!');
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { defaultEditorLocalization } from '../localization.mjs';
|
2
|
-
// A partial Spanish localization
|
2
|
+
// A partial Spanish localization
|
3
3
|
const localization = {
|
4
4
|
...defaultEditorLocalization,
|
5
5
|
pen: 'Lapiz',
|
@@ -7,6 +7,8 @@ const localization = {
|
|
7
7
|
select: 'Selecciona',
|
8
8
|
handTool: 'Mover',
|
9
9
|
image: 'Imagen',
|
10
|
+
inputAltText: 'Texto alternativo',
|
11
|
+
resetImage: 'Reiniciar',
|
10
12
|
chooseFile: 'Seleccionar archivo',
|
11
13
|
cancel: 'Cancelar',
|
12
14
|
resetView: 'Reiniciar vista',
|
@@ -28,7 +30,9 @@ const localization = {
|
|
28
30
|
backgroundColor: 'Color de fondo',
|
29
31
|
imageWidthOption: 'Ancho',
|
30
32
|
imageHeightOption: 'Alto',
|
33
|
+
enableAutoresizeOption: 'Redimensionar automático',
|
31
34
|
toggleOverflow: 'Más',
|
35
|
+
about: 'Acerca de',
|
32
36
|
touchPanning: 'Mover la pantalla con un dedo',
|
33
37
|
roundedTipPen: 'Lapiz Redondeado',
|
34
38
|
arrowPen: 'Flecha',
|
@@ -44,10 +48,11 @@ const localization = {
|
|
44
48
|
colorChangedAnnouncement: (color) => { return `Color fue cambiado a ${color}`; },
|
45
49
|
imageSize: (size, units) => `Tamaño del imagen: ${size} ${units}`,
|
46
50
|
imageLoadError: (message) => `Error cargando imagen: ${message}`,
|
47
|
-
penTool: (penId) =>
|
51
|
+
penTool: (penId) => `Lapiz ${penId}`,
|
48
52
|
selectionTool: 'Selecciona',
|
49
53
|
eraserTool: 'Borrador',
|
50
54
|
touchPanTool: 'Instrumento de mover la pantalla con un dedo',
|
55
|
+
undoRedoTool: 'Deshace/rehace',
|
51
56
|
pipetteTool: 'Seleccione un color de la pantalla',
|
52
57
|
keyboardPanZoom: 'Mover la pantalla con el teclado',
|
53
58
|
textTool: 'Texto',
|
@@ -55,29 +60,13 @@ const localization = {
|
|
55
60
|
findLabel: 'Buscar',
|
56
61
|
toNextMatch: 'Próxima',
|
57
62
|
closeDialog: 'Cerrar',
|
58
|
-
focusedFoundText: (matchIdx, totalMatches) => `Viewing match ${matchIdx} of ${totalMatches}`,
|
59
63
|
anyDevicePanning: 'Mover la pantalla con todo dispotivo',
|
60
64
|
copied: (count, description) => `Copied ${count} ${description}`,
|
61
65
|
pasted: (count, description) => `Pasted ${count} ${description}`,
|
62
|
-
toolEnabledAnnouncement: (toolName) => `${toolName}
|
63
|
-
toolDisabledAnnouncement: (toolName) => `${toolName}
|
64
|
-
|
65
|
-
|
66
|
-
addElementAction: (componentDescription) => `Added ${componentDescription}`,
|
67
|
-
eraseAction: (componentDescription, numElems) => `Erased ${numElems} ${componentDescription}`,
|
68
|
-
duplicateAction: (componentDescription, numElems) => `Duplicated ${numElems} ${componentDescription}`,
|
69
|
-
unionOf: (actionDescription, actionCount) => `Union: ${actionCount} ${actionDescription}`,
|
70
|
-
inverseOf: (actionDescription) => `Inverse of ${actionDescription}`,
|
71
|
-
rotatedBy: (degrees) => `Rotated by ${Math.abs(degrees)} degrees ${degrees < 0 ? 'clockwise' : 'counter-clockwise'}`,
|
72
|
-
selectedElements: (count) => `Selected ${count} element${count === 1 ? '' : 's'}`,
|
73
|
-
filledBackgroundWithColor: (color) => `Filled background (${color})`,
|
74
|
-
text: (text) => `Text object: ${text}`,
|
75
|
-
imageNode: (label) => `Image: ${label}`,
|
76
|
-
restyledElement: (elementDescription) => `Restyled ${elementDescription}`,
|
77
|
-
pathNodeCount: (count) => `There are ${count} visible path objects.`,
|
78
|
-
textNodeCount: (count) => `There are ${count} visible text nodes.`,
|
79
|
-
imageNodeCount: (nodeCount) => `There are ${nodeCount} visible image nodes.`,
|
80
|
-
textNode: (content) => `Text: ${content}`,
|
66
|
+
toolEnabledAnnouncement: (toolName) => `${toolName} fue activado`,
|
67
|
+
toolDisabledAnnouncement: (toolName) => `${toolName} fue desactivado`,
|
68
|
+
resizeOutputCommand: (newSize) => `Tamaño de imagen fue cambiado a ${newSize.w}x${newSize.h}`,
|
69
|
+
eraseAction: (componentDescription, numElems) => `Borrado: ${numElems} ${componentDescription}`,
|
81
70
|
rerenderAsText: 'Redibuja la pantalla al texto',
|
82
71
|
loading: (percentage) => `Cargando: ${percentage}%...`,
|
83
72
|
imageEditor: 'Editor de dibujos',
|
@@ -55,6 +55,8 @@ export default class Display {
|
|
55
55
|
* @internal
|
56
56
|
*/
|
57
57
|
setDevicePixelRatio(dpr: number): Promise<void> | undefined;
|
58
|
+
/** @internal */
|
59
|
+
getDevicePixelRatio(): number;
|
58
60
|
/**
|
59
61
|
* Rerenders the text-based display.
|
60
62
|
* The text-based display is intended for screen readers and can be navigated to by pressing `tab`.
|
@@ -199,6 +199,10 @@ export default class Display {
|
|
199
199
|
}
|
200
200
|
return undefined;
|
201
201
|
}
|
202
|
+
/** @internal */
|
203
|
+
getDevicePixelRatio() {
|
204
|
+
return this.devicePixelRatio;
|
205
|
+
}
|
202
206
|
/**
|
203
207
|
* Rerenders the text-based display.
|
204
208
|
* The text-based display is intended for screen readers and can be navigated to by pressing `tab`.
|
@@ -21,12 +21,14 @@ import AbstractToolbar, { SpacerOptions } from './AbstractToolbar';
|
|
21
21
|
* });
|
22
22
|
* ```
|
23
23
|
*
|
24
|
+
* Returns a subclass of {@link AbstractToolbar}.
|
25
|
+
*
|
24
26
|
* @see
|
25
27
|
* - {@link makeEdgeToolbar}
|
26
28
|
* - {@link AbstractToolbar.addSaveButton}
|
27
29
|
* - {@link AbstractToolbar.addExitButton}
|
28
30
|
*/
|
29
|
-
export declare const makeDropdownToolbar: (editor: Editor) =>
|
31
|
+
export declare const makeDropdownToolbar: (editor: Editor) => DropdownToolbar;
|
30
32
|
export default class DropdownToolbar extends AbstractToolbar {
|
31
33
|
protected container: HTMLElement;
|
32
34
|
private resizeObserver;
|
@@ -136,8 +136,10 @@ export default class EdgeToolbar extends AbstractToolbar {
|
|
136
136
|
this.sidebarContainer.style.animation = `${animationProperties} ${toolbarCSSPrefix}-edgemenu-transition-in`;
|
137
137
|
this.menuContainer.style.animation = `${animationProperties} ${toolbarCSSPrefix}-edgemenu-container-transition-in`;
|
138
138
|
this.menuContainer.style.opacity = '1';
|
139
|
-
// Focus the close button when first shown
|
140
|
-
|
139
|
+
// Focus the close button when first shown, but prevent scroll because the button
|
140
|
+
// is likely at the bottom of the screen (and we want the full sidebar to remain
|
141
|
+
// visible).
|
142
|
+
this.closeButton.focus({ preventScroll: true, });
|
141
143
|
}
|
142
144
|
else {
|
143
145
|
this.closeColorPickers();
|
@@ -265,9 +267,8 @@ export default class EdgeToolbar extends AbstractToolbar {
|
|
265
267
|
if (event.target === this.menuContainer) {
|
266
268
|
if (eventName === 'pointerdown') {
|
267
269
|
this.sidebarVisible.set(false);
|
268
|
-
|
269
|
-
|
270
|
-
this.editor.focus();
|
270
|
+
// A delay seems necessary for the editor
|
271
|
+
setTimeout(() => this.editor.focus(), 0);
|
271
272
|
}
|
272
273
|
return true;
|
273
274
|
}
|
@@ -14,6 +14,7 @@ import ToolbarShortcutHandler from '../../tools/ToolbarShortcutHandler.mjs';
|
|
14
14
|
import { keyPressEventFromHTMLEvent, keyUpEventFromHTMLEvent } from '../../inputEvents.mjs';
|
15
15
|
import { toolbarCSSPrefix } from '../constants.mjs';
|
16
16
|
import DropdownLayoutManager from './layout/DropdownLayoutManager.mjs';
|
17
|
+
import addLongPressOrHoverCssClasses from '../../util/addLongPressOrHoverCssClasses.mjs';
|
17
18
|
/**
|
18
19
|
* A set of labels that allow toolbar themes to treat buttons differently.
|
19
20
|
*/
|
@@ -60,6 +61,7 @@ class BaseWidget {
|
|
60
61
|
this.button.oncontextmenu = event => {
|
61
62
|
event.preventDefault();
|
62
63
|
};
|
64
|
+
addLongPressOrHoverCssClasses(this.button);
|
63
65
|
}
|
64
66
|
/**
|
65
67
|
* Should return a constant true or false value. If true (the default),
|