js-draw 1.18.0 → 1.20.0
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +51 -0
- package/dist/Editor.css +78 -5
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +20 -1
- package/dist/cjs/Editor.js +6 -0
- package/dist/cjs/{SVGLoader.d.ts → SVGLoader/index.d.ts} +1 -1
- package/dist/cjs/{SVGLoader.js → SVGLoader/index.js} +15 -30
- package/dist/cjs/SVGLoader/utils/determineFontSize.d.ts +3 -0
- package/dist/cjs/SVGLoader/utils/determineFontSize.js +27 -0
- package/dist/cjs/Viewport.d.ts +33 -1
- package/dist/cjs/components/TextComponent.js +3 -1
- package/dist/cjs/image/EditorImage.d.ts +2 -1
- package/dist/cjs/image/EditorImage.js +101 -5
- package/dist/cjs/rendering/caching/RenderingCacheNode.js +20 -15
- package/dist/cjs/rendering/renderers/CanvasRenderer.js +4 -4
- package/dist/cjs/testing/findNodeWithText.d.ts +3 -0
- package/dist/cjs/testing/findNodeWithText.js +16 -0
- package/dist/cjs/testing/firstElementAncestorOfNode.d.ts +3 -0
- package/dist/cjs/testing/firstElementAncestorOfNode.js +13 -0
- package/dist/cjs/testing/sendKeyPressRelease.d.ts +3 -0
- package/dist/cjs/testing/sendKeyPressRelease.js +8 -0
- package/dist/cjs/testing/sendPenEvent.d.ts +2 -2
- package/dist/cjs/testing/sendPenEvent.js +26 -3
- package/dist/cjs/toolbar/localization.d.ts +3 -0
- package/dist/cjs/toolbar/localization.js +3 -0
- package/dist/cjs/toolbar/widgets/BaseWidget.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/BaseWidget.js +1 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +23 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.js +65 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/fileToImages.d.ts +3 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/fileToImages.js +21 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/index.d.ts +37 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/index.js +289 -0
- package/dist/cjs/toolbar/widgets/TextToolWidget.js +5 -3
- package/dist/cjs/toolbar/widgets/TextToolWidget.test.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/components/makeFileInput.d.ts +12 -2
- package/dist/cjs/toolbar/widgets/components/makeFileInput.js +113 -45
- package/dist/cjs/toolbar/widgets/components/makeFileInput.test.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/components/makeSnappedList.d.ts +15 -0
- package/dist/cjs/toolbar/widgets/components/makeSnappedList.js +168 -0
- package/dist/cjs/tools/Eraser.d.ts +7 -2
- package/dist/cjs/tools/Eraser.js +76 -6
- package/dist/cjs/tools/PanZoom.d.ts +54 -0
- package/dist/cjs/tools/PanZoom.js +54 -2
- package/dist/cjs/tools/SelectionTool/Selection.d.ts +2 -2
- package/dist/cjs/tools/SelectionTool/Selection.js +20 -20
- package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +8 -2
- package/dist/cjs/tools/SelectionTool/SelectionHandle.js +6 -0
- package/dist/cjs/tools/SelectionTool/SelectionTool.js +1 -1
- package/dist/cjs/tools/SelectionTool/types.d.ts +19 -0
- package/dist/cjs/tools/TextTool.js +2 -1
- package/dist/cjs/tools/TextTool.test.d.ts +1 -0
- package/dist/cjs/tools/ToolController.d.ts +2 -0
- package/dist/cjs/tools/ToolController.js +10 -1
- package/dist/cjs/util/ReactiveValue.d.ts +6 -0
- package/dist/cjs/util/ReactiveValue.js +16 -0
- package/dist/cjs/util/bytesToSizeString.d.ts +8 -0
- package/dist/cjs/util/bytesToSizeString.js +26 -0
- package/dist/cjs/util/bytesToSizeString.test.d.ts +1 -0
- package/dist/cjs/util/stopPropagationOfScrollingWheelEvents.js +10 -6
- package/dist/cjs/util/waitForAll.d.ts +2 -0
- package/dist/cjs/util/waitForAll.js +2 -0
- package/dist/cjs/util/waitForImageLoaded.js +3 -0
- package/dist/cjs/util/waitForTimeout.d.ts +1 -0
- package/dist/cjs/util/waitForTimeout.js +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +20 -1
- package/dist/mjs/Editor.mjs +6 -0
- package/dist/mjs/{SVGLoader.d.ts → SVGLoader/index.d.ts} +1 -1
- package/dist/mjs/{SVGLoader.mjs → SVGLoader/index.mjs} +15 -30
- package/dist/mjs/SVGLoader/index.test.d.ts +1 -0
- package/dist/mjs/SVGLoader/utils/determineFontSize.d.ts +3 -0
- package/dist/mjs/SVGLoader/utils/determineFontSize.mjs +25 -0
- package/dist/mjs/Viewport.d.ts +33 -1
- package/dist/mjs/components/TextComponent.mjs +3 -1
- package/dist/mjs/image/EditorImage.d.ts +2 -1
- package/dist/mjs/image/EditorImage.mjs +101 -5
- package/dist/mjs/rendering/caching/RenderingCacheNode.mjs +20 -15
- package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +4 -4
- package/dist/mjs/testing/findNodeWithText.d.ts +3 -0
- package/dist/mjs/testing/findNodeWithText.mjs +14 -0
- package/dist/mjs/testing/firstElementAncestorOfNode.d.ts +3 -0
- package/dist/mjs/testing/firstElementAncestorOfNode.mjs +11 -0
- package/dist/mjs/testing/sendKeyPressRelease.d.ts +3 -0
- package/dist/mjs/testing/sendKeyPressRelease.mjs +6 -0
- package/dist/mjs/testing/sendPenEvent.d.ts +2 -2
- package/dist/mjs/testing/sendPenEvent.mjs +3 -3
- package/dist/mjs/toolbar/localization.d.ts +3 -0
- package/dist/mjs/toolbar/localization.mjs +3 -0
- package/dist/mjs/toolbar/widgets/BaseWidget.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/BaseWidget.mjs +1 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +23 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.mjs +61 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/fileToImages.d.ts +3 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/fileToImages.mjs +16 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/index.d.ts +37 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/index.mjs +284 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/index.test.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/TextToolWidget.mjs +5 -3
- package/dist/mjs/toolbar/widgets/TextToolWidget.test.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/components/makeFileInput.d.ts +12 -2
- package/dist/mjs/toolbar/widgets/components/makeFileInput.mjs +113 -45
- package/dist/mjs/toolbar/widgets/components/makeFileInput.test.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/components/makeSnappedList.d.ts +15 -0
- package/dist/mjs/toolbar/widgets/components/makeSnappedList.mjs +163 -0
- package/dist/mjs/tools/Eraser.d.ts +7 -2
- package/dist/mjs/tools/Eraser.mjs +76 -6
- package/dist/mjs/tools/PanZoom.d.ts +54 -0
- package/dist/mjs/tools/PanZoom.mjs +54 -2
- package/dist/mjs/tools/SelectionTool/Selection.d.ts +2 -2
- package/dist/mjs/tools/SelectionTool/Selection.mjs +20 -20
- package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +8 -2
- package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +6 -0
- package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +1 -1
- package/dist/mjs/tools/SelectionTool/types.d.ts +19 -0
- package/dist/mjs/tools/TextTool.mjs +2 -1
- package/dist/mjs/tools/TextTool.test.d.ts +1 -0
- package/dist/mjs/tools/ToolController.d.ts +2 -0
- package/dist/mjs/tools/ToolController.mjs +10 -1
- package/dist/mjs/util/ReactiveValue.d.ts +6 -0
- package/dist/mjs/util/ReactiveValue.mjs +16 -0
- package/dist/mjs/util/bytesToSizeString.d.ts +8 -0
- package/dist/mjs/util/bytesToSizeString.mjs +24 -0
- package/dist/mjs/util/bytesToSizeString.test.d.ts +1 -0
- package/dist/mjs/util/stopPropagationOfScrollingWheelEvents.mjs +10 -6
- package/dist/mjs/util/waitForAll.d.ts +2 -0
- package/dist/mjs/util/waitForAll.mjs +2 -0
- package/dist/mjs/util/waitForImageLoaded.mjs +3 -0
- package/dist/mjs/util/waitForTimeout.d.ts +1 -0
- package/dist/mjs/util/waitForTimeout.mjs +1 -1
- package/dist/mjs/version.mjs +1 -1
- package/package.json +4 -4
- package/src/toolbar/EdgeToolbar.scss +8 -3
- package/src/toolbar/toolbar.scss +1 -7
- package/src/toolbar/widgets/{InsertImageWidget.scss → InsertImageWidget/index.scss} +3 -2
- package/src/toolbar/widgets/components/components.scss +2 -1
- package/src/toolbar/widgets/components/makeFileInput.scss +14 -1
- package/src/toolbar/widgets/components/makeSnappedList.scss +74 -0
- package/src/toolbar/widgets/widgets.scss +7 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget.d.ts +0 -22
- package/dist/cjs/toolbar/widgets/InsertImageWidget.js +0 -269
- package/dist/mjs/toolbar/widgets/InsertImageWidget.d.ts +0 -22
- package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +0 -264
- /package/dist/cjs/{SVGLoader.test.d.ts → SVGLoader/index.test.d.ts} +0 -0
- /package/dist/{mjs/SVGLoader.test.d.ts → cjs/toolbar/widgets/InsertImageWidget/index.test.d.ts} +0 -0
@@ -0,0 +1,163 @@
|
|
1
|
+
import stopPropagationOfScrollingWheelEvents from '../../../util/stopPropagationOfScrollingWheelEvents.mjs';
|
2
|
+
import { MutableReactiveValue, ReactiveValue } from '../../../util/ReactiveValue.mjs';
|
3
|
+
/**
|
4
|
+
* Creates a list that snaps to each item and reports the selected item.
|
5
|
+
*/
|
6
|
+
const makeSnappedList = (itemsValue) => {
|
7
|
+
const container = document.createElement('div');
|
8
|
+
container.classList.add('toolbar-snapped-scroll-list');
|
9
|
+
const scroller = document.createElement('div');
|
10
|
+
scroller.classList.add('scroller');
|
11
|
+
const visibleIndex = MutableReactiveValue.fromInitialValue(0);
|
12
|
+
let observer = null;
|
13
|
+
const makePageMarkers = () => {
|
14
|
+
const markerContainer = document.createElement('div');
|
15
|
+
markerContainer.classList.add('page-markers');
|
16
|
+
// Keyboard focus should go to the main scrolling list.
|
17
|
+
// TODO: Does it make sense for the page marker list to be focusable?
|
18
|
+
markerContainer.setAttribute('tabindex', '-1');
|
19
|
+
const markers = [];
|
20
|
+
const pairedItems = ReactiveValue.union([visibleIndex, itemsValue]);
|
21
|
+
pairedItems.onUpdateAndNow(([currentVisibleIndex, items]) => {
|
22
|
+
let addedOrRemovedMarkers = false;
|
23
|
+
// Items may have been removed from the list of pages. Make the markers reflect that.
|
24
|
+
while (items.length < markers.length) {
|
25
|
+
markers.pop();
|
26
|
+
addedOrRemovedMarkers = true;
|
27
|
+
}
|
28
|
+
let activeMarker;
|
29
|
+
for (let i = 0; i < items.length; i++) {
|
30
|
+
let marker;
|
31
|
+
if (i >= markers.length) {
|
32
|
+
marker = document.createElement('div');
|
33
|
+
// Use a separate content element to increase the clickable size of
|
34
|
+
// the marker.
|
35
|
+
const content = document.createElement('div');
|
36
|
+
content.classList.add('content');
|
37
|
+
marker.replaceChildren(content);
|
38
|
+
markers.push(marker);
|
39
|
+
addedOrRemovedMarkers = true;
|
40
|
+
}
|
41
|
+
else {
|
42
|
+
marker = markers[i];
|
43
|
+
}
|
44
|
+
marker.classList.add('marker');
|
45
|
+
if (i === currentVisibleIndex) {
|
46
|
+
marker.classList.add('-active');
|
47
|
+
activeMarker = marker;
|
48
|
+
}
|
49
|
+
else {
|
50
|
+
marker.classList.remove('-active');
|
51
|
+
}
|
52
|
+
const markerIndex = i;
|
53
|
+
marker.onclick = () => {
|
54
|
+
wrappedItems.get()[markerIndex]?.element?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
55
|
+
};
|
56
|
+
}
|
57
|
+
// Only call .replaceChildren when necessary -- doing so on every change would
|
58
|
+
// break transitions.
|
59
|
+
if (addedOrRemovedMarkers) {
|
60
|
+
markerContainer.replaceChildren(...markers);
|
61
|
+
}
|
62
|
+
// Handles the case where there are many markers and the current is offscreen
|
63
|
+
if (activeMarker && markerContainer.scrollHeight > container.clientHeight) {
|
64
|
+
activeMarker.scrollIntoView({ block: 'nearest' });
|
65
|
+
}
|
66
|
+
if (markers.length === 1) {
|
67
|
+
markerContainer.classList.add('-one-element');
|
68
|
+
}
|
69
|
+
else {
|
70
|
+
markerContainer.classList.remove('-one-element');
|
71
|
+
}
|
72
|
+
});
|
73
|
+
return markerContainer;
|
74
|
+
};
|
75
|
+
const createObserver = () => {
|
76
|
+
observer = new IntersectionObserver((entries) => {
|
77
|
+
for (const entry of entries) {
|
78
|
+
if (entry.isIntersecting && entry.intersectionRatio > 0.7) {
|
79
|
+
const indexString = entry.target.getAttribute('data-item-index');
|
80
|
+
if (indexString === null)
|
81
|
+
throw new Error('Could not find attribute data-item-index');
|
82
|
+
const index = Number(indexString);
|
83
|
+
visibleIndex.set(index);
|
84
|
+
break;
|
85
|
+
}
|
86
|
+
}
|
87
|
+
}, {
|
88
|
+
// Element to use as the boudning box with which to intersect.
|
89
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
90
|
+
root: scroller,
|
91
|
+
// Fraction of an element that must be visible to trigger the callback:
|
92
|
+
threshold: 0.9,
|
93
|
+
});
|
94
|
+
};
|
95
|
+
const destroyObserver = () => {
|
96
|
+
if (observer) {
|
97
|
+
observer.disconnect();
|
98
|
+
visibleIndex.set(0);
|
99
|
+
observer = null;
|
100
|
+
}
|
101
|
+
};
|
102
|
+
const wrappedItems = ReactiveValue.map(itemsValue, items => {
|
103
|
+
return items.map((item, index) => {
|
104
|
+
const wrapper = document.createElement('div');
|
105
|
+
if (item.element.parentElement)
|
106
|
+
item.element.remove();
|
107
|
+
wrapper.appendChild(item.element);
|
108
|
+
wrapper.classList.add('item');
|
109
|
+
wrapper.setAttribute('data-item-index', `${index}`);
|
110
|
+
return {
|
111
|
+
element: wrapper,
|
112
|
+
data: item.data,
|
113
|
+
};
|
114
|
+
});
|
115
|
+
});
|
116
|
+
const lastItems = [];
|
117
|
+
wrappedItems.onUpdateAndNow(items => {
|
118
|
+
visibleIndex.set(-1);
|
119
|
+
for (const item of lastItems) {
|
120
|
+
observer?.unobserve(item.element);
|
121
|
+
}
|
122
|
+
scroller.replaceChildren();
|
123
|
+
// An observer is only necessary if there are multiple items to scroll through.
|
124
|
+
if (items.length > 1) {
|
125
|
+
createObserver();
|
126
|
+
}
|
127
|
+
else {
|
128
|
+
destroyObserver();
|
129
|
+
}
|
130
|
+
// Different styling is applied when empty
|
131
|
+
if (items.length === 0) {
|
132
|
+
container.classList.add('-empty');
|
133
|
+
}
|
134
|
+
else {
|
135
|
+
container.classList.remove('-empty');
|
136
|
+
}
|
137
|
+
for (const item of items) {
|
138
|
+
scroller.appendChild(item.element);
|
139
|
+
}
|
140
|
+
visibleIndex.set(0);
|
141
|
+
if (observer) {
|
142
|
+
for (const item of items) {
|
143
|
+
observer.observe(item.element);
|
144
|
+
}
|
145
|
+
}
|
146
|
+
});
|
147
|
+
const visibleItem = ReactiveValue.map(visibleIndex, index => {
|
148
|
+
const values = itemsValue.get();
|
149
|
+
if (0 <= index && index < values.length) {
|
150
|
+
return values[index].data;
|
151
|
+
}
|
152
|
+
return null;
|
153
|
+
});
|
154
|
+
// makeSnappedList is generally shown within the toolbar. This allows users to
|
155
|
+
// scroll it with a touchpad.
|
156
|
+
stopPropagationOfScrollingWheelEvents(scroller);
|
157
|
+
container.replaceChildren(makePageMarkers(), scroller);
|
158
|
+
return {
|
159
|
+
container,
|
160
|
+
visibleItem,
|
161
|
+
};
|
162
|
+
};
|
163
|
+
export default makeSnappedList;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { KeyPressEvent, PointerEvt } from '../inputEvents';
|
1
|
+
import { GestureCancelEvt, KeyPressEvent, PointerEvt } from '../inputEvents';
|
2
2
|
import BaseTool from './BaseTool';
|
3
3
|
import Editor from '../Editor';
|
4
4
|
import { MutableReactiveValue } from '../util/ReactiveValue';
|
@@ -22,6 +22,11 @@ export default class Eraser extends BaseTool {
|
|
22
22
|
private eraseCommands;
|
23
23
|
private addCommands;
|
24
24
|
constructor(editor: Editor, description: string, options?: InitialEraserOptions);
|
25
|
+
/**
|
26
|
+
* @returns a tool that briefly enables the eraser when a physical eraser is used.
|
27
|
+
* This tool should be added to the tool list after the primary tools.
|
28
|
+
*/
|
29
|
+
makeEraserSwitcherTool(): BaseTool;
|
25
30
|
private clearPreview;
|
26
31
|
private getSizeOnCanvas;
|
27
32
|
private drawPreviewAt;
|
@@ -36,7 +41,7 @@ export default class Eraser extends BaseTool {
|
|
36
41
|
onPointerDown(event: PointerEvt): boolean;
|
37
42
|
onPointerMove(event: PointerEvt): void;
|
38
43
|
onPointerUp(event: PointerEvt): void;
|
39
|
-
onGestureCancel(): void;
|
44
|
+
onGestureCancel(_event: GestureCancelEvt): void;
|
40
45
|
onKeyPress(event: KeyPressEvent): boolean;
|
41
46
|
/** Returns the side-length of the tip of this eraser. */
|
42
47
|
getThickness(): number;
|
@@ -13,12 +13,59 @@ export var EraserMode;
|
|
13
13
|
EraserMode["PartialStroke"] = "partial-stroke";
|
14
14
|
EraserMode["FullStroke"] = "full-stroke";
|
15
15
|
})(EraserMode || (EraserMode = {}));
|
16
|
+
/** Handles switching from other primary tools to the eraser and back */
|
17
|
+
class EraserSwitcher extends BaseTool {
|
18
|
+
constructor(editor, eraser) {
|
19
|
+
super(editor.notifier, editor.localization.changeTool);
|
20
|
+
this.editor = editor;
|
21
|
+
this.eraser = eraser;
|
22
|
+
}
|
23
|
+
onPointerDown(event) {
|
24
|
+
if (event.allPointers.length === 1 && event.current.device === PointerDevice.Eraser) {
|
25
|
+
const toolController = this.editor.toolController;
|
26
|
+
const enabledPrimaryTools = toolController.getPrimaryTools().filter(tool => tool.isEnabled());
|
27
|
+
if (enabledPrimaryTools.length) {
|
28
|
+
this.previousEnabledTool = enabledPrimaryTools[0];
|
29
|
+
}
|
30
|
+
else {
|
31
|
+
this.previousEnabledTool = null;
|
32
|
+
}
|
33
|
+
this.previousEraserEnabledState = this.eraser.isEnabled();
|
34
|
+
this.eraser.setEnabled(true);
|
35
|
+
if (this.eraser.onPointerDown(event)) {
|
36
|
+
return true;
|
37
|
+
}
|
38
|
+
else {
|
39
|
+
this.restoreOriginalTool();
|
40
|
+
}
|
41
|
+
}
|
42
|
+
return false;
|
43
|
+
}
|
44
|
+
onPointerMove(event) {
|
45
|
+
this.eraser.onPointerMove(event);
|
46
|
+
}
|
47
|
+
restoreOriginalTool() {
|
48
|
+
this.eraser.setEnabled(this.previousEraserEnabledState);
|
49
|
+
if (this.previousEnabledTool) {
|
50
|
+
this.previousEnabledTool.setEnabled(true);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
onPointerUp(event) {
|
54
|
+
this.eraser.onPointerUp(event);
|
55
|
+
this.restoreOriginalTool();
|
56
|
+
}
|
57
|
+
onGestureCancel(event) {
|
58
|
+
this.eraser.onGestureCancel(event);
|
59
|
+
this.restoreOriginalTool();
|
60
|
+
}
|
61
|
+
}
|
16
62
|
export default class Eraser extends BaseTool {
|
17
63
|
constructor(editor, description, options) {
|
18
64
|
super(editor.notifier, description);
|
19
65
|
this.editor = editor;
|
20
66
|
this.lastPoint = null;
|
21
67
|
this.isFirstEraseEvt = true;
|
68
|
+
this.toAdd = new Set();
|
22
69
|
// Commands that each remove one element
|
23
70
|
this.eraseCommands = [];
|
24
71
|
this.addCommands = [];
|
@@ -39,6 +86,13 @@ export default class Eraser extends BaseTool {
|
|
39
86
|
});
|
40
87
|
});
|
41
88
|
}
|
89
|
+
/**
|
90
|
+
* @returns a tool that briefly enables the eraser when a physical eraser is used.
|
91
|
+
* This tool should be added to the tool list after the primary tools.
|
92
|
+
*/
|
93
|
+
makeEraserSwitcherTool() {
|
94
|
+
return new EraserSwitcher(this.editor, this);
|
95
|
+
}
|
42
96
|
clearPreview() {
|
43
97
|
this.editor.clearWetInk();
|
44
98
|
}
|
@@ -121,15 +175,17 @@ export default class Eraser extends BaseTool {
|
|
121
175
|
newAddCommands.forEach(command => command.apply(this.editor));
|
122
176
|
const finalToErase = [];
|
123
177
|
for (const item of toErase) {
|
124
|
-
if (this.toAdd.
|
125
|
-
this.toAdd
|
178
|
+
if (this.toAdd.has(item)) {
|
179
|
+
this.toAdd.delete(item);
|
126
180
|
}
|
127
181
|
else {
|
128
182
|
finalToErase.push(item);
|
129
183
|
}
|
130
184
|
}
|
131
185
|
this.toRemove.push(...finalToErase);
|
132
|
-
|
186
|
+
for (const item of toAdd) {
|
187
|
+
this.toAdd.add(item);
|
188
|
+
}
|
133
189
|
this.eraseCommands.push(new Erase(finalToErase));
|
134
190
|
this.addCommands.push(...newAddCommands);
|
135
191
|
}
|
@@ -140,7 +196,7 @@ export default class Eraser extends BaseTool {
|
|
140
196
|
if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
|
141
197
|
this.lastPoint = event.current.canvasPos;
|
142
198
|
this.toRemove = [];
|
143
|
-
this.toAdd
|
199
|
+
this.toAdd.clear();
|
144
200
|
this.isFirstEraseEvt = true;
|
145
201
|
this.drawPreviewAt(event.current.canvasPos);
|
146
202
|
return true;
|
@@ -156,7 +212,21 @@ export default class Eraser extends BaseTool {
|
|
156
212
|
const commands = [];
|
157
213
|
if (this.addCommands.length > 0) {
|
158
214
|
this.addCommands.forEach(cmd => cmd.unapply(this.editor));
|
159
|
-
|
215
|
+
// Remove items from toAdd that are also present in toRemove -- adding, then
|
216
|
+
// removing these does nothing, and can break undo/redo.
|
217
|
+
for (const item of this.toAdd) {
|
218
|
+
if (this.toRemove.includes(item)) {
|
219
|
+
this.toAdd.delete(item);
|
220
|
+
this.toRemove = this.toRemove.filter(other => other !== item);
|
221
|
+
}
|
222
|
+
}
|
223
|
+
for (const item of this.toRemove) {
|
224
|
+
if (this.toAdd.has(item)) {
|
225
|
+
this.toAdd.delete(item);
|
226
|
+
this.toRemove = this.toRemove.filter(other => other !== item);
|
227
|
+
}
|
228
|
+
}
|
229
|
+
commands.push(...[...this.toAdd].map(a => EditorImage.addElement(a)));
|
160
230
|
this.addCommands = [];
|
161
231
|
}
|
162
232
|
if (this.eraseCommands.length > 0) {
|
@@ -174,7 +244,7 @@ export default class Eraser extends BaseTool {
|
|
174
244
|
}
|
175
245
|
this.clearPreview();
|
176
246
|
}
|
177
|
-
onGestureCancel() {
|
247
|
+
onGestureCancel(_event) {
|
178
248
|
this.addCommands.forEach(cmd => cmd.unapply(this.editor));
|
179
249
|
this.eraseCommands.forEach(cmd => cmd.unapply(this.editor));
|
180
250
|
this.eraseCommands = [];
|
@@ -10,13 +10,27 @@ interface PinchData {
|
|
10
10
|
dist: number;
|
11
11
|
}
|
12
12
|
export declare enum PanZoomMode {
|
13
|
+
/** Touch gestures with a single pointer. Ignores non-touch gestures. */
|
13
14
|
OneFingerTouchGestures = 1,
|
15
|
+
/** Touch gestures with exactly two pointers. Ignores non-touch gestures. */
|
14
16
|
TwoFingerTouchGestures = 2,
|
15
17
|
RightClickDrags = 4,
|
18
|
+
/** Single-pointer gestures of *any* type (including touch). */
|
16
19
|
SinglePointerGestures = 8,
|
20
|
+
/** Keyboard navigation (e.g. LeftArrow to move left). */
|
17
21
|
Keyboard = 16,
|
22
|
+
/** If provided, prevents **this** tool from rotating the viewport (other tools may still do so). */
|
18
23
|
RotationLocked = 32
|
19
24
|
}
|
25
|
+
/**
|
26
|
+
* This tool moves the viewport in response to touchpad, touchscreen, mouse, and keyboard events.
|
27
|
+
*
|
28
|
+
* Which events are handled, and which are skipped, are determined by the tool's `mode`. For example,
|
29
|
+
* a `PanZoom` tool with `mode = PanZoomMode.TwoFingerTouchGestures|PanZoomMode.RightClickDrags` would
|
30
|
+
* respond to right-click drag events and two-finger touch gestures.
|
31
|
+
*
|
32
|
+
* @see {@link setModeEnabled}
|
33
|
+
*/
|
20
34
|
export default class PanZoom extends BaseTool {
|
21
35
|
private editor;
|
22
36
|
private mode;
|
@@ -58,8 +72,48 @@ export default class PanZoom extends BaseTool {
|
|
58
72
|
onWheel({ delta, screenPos }: WheelEvt): boolean;
|
59
73
|
onKeyPress(event: KeyPressEvent): boolean;
|
60
74
|
private isRotationLocked;
|
75
|
+
/**
|
76
|
+
* Changes the types of gestures used by this pan/zoom tool.
|
77
|
+
*
|
78
|
+
* @see {@link PanZoomMode} {@link setMode}
|
79
|
+
*
|
80
|
+
* @example
|
81
|
+
* ```ts,runnable
|
82
|
+
* import { Editor, PanZoomTool, PanZoomMode } from 'js-draw';
|
83
|
+
*
|
84
|
+
* const editor = new Editor(document.body);
|
85
|
+
*
|
86
|
+
* // By default, there are multiple PanZoom tools that handle different events.
|
87
|
+
* // This gets all PanZoomTools.
|
88
|
+
* const panZoomToolList = editor.toolController.getMatchingTools(PanZoomTool);
|
89
|
+
*
|
90
|
+
* // The first PanZoomTool is the highest priority -- by default,
|
91
|
+
* // this tool is responsible for handling multi-finger touch gestures.
|
92
|
+
* //
|
93
|
+
* // Lower-priority PanZoomTools handle one-finger touch gestures and
|
94
|
+
* // key-presses.
|
95
|
+
* const panZoomTool = panZoomToolList[0];
|
96
|
+
*
|
97
|
+
* // Lock rotation for multi-finger touch gestures.
|
98
|
+
* panZoomTool.setModeEnabled(PanZoomMode.RotationLocked, true);
|
99
|
+
* ```
|
100
|
+
*/
|
61
101
|
setModeEnabled(mode: PanZoomMode, enabled: boolean): void;
|
102
|
+
/**
|
103
|
+
* Sets all modes for this tool using a bitmask.
|
104
|
+
*
|
105
|
+
* @see {@link setModeEnabled}
|
106
|
+
*
|
107
|
+
* @example
|
108
|
+
* ```ts
|
109
|
+
* tool.setMode(PanZoomMode.RotationLocked|PanZoomMode.TwoFingerTouchGestures);
|
110
|
+
* ```
|
111
|
+
*/
|
62
112
|
setMode(mode: PanZoomMode): void;
|
113
|
+
/**
|
114
|
+
* Returns a bitmask indicating the currently-enabled modes.
|
115
|
+
* @see {@link setModeEnabled}
|
116
|
+
*/
|
63
117
|
getMode(): PanZoomMode;
|
64
118
|
}
|
65
119
|
export {};
|
@@ -7,11 +7,16 @@ import BaseTool from './BaseTool.mjs';
|
|
7
7
|
import { moveDownKeyboardShortcutId, moveLeftKeyboardShortcutId, moveRightKeyboardShortcutId, moveUpKeyboardShortcutId, rotateClockwiseKeyboardShortcutId, rotateCounterClockwiseKeyboardShortcutId, zoomInKeyboardShortcutId, zoomOutKeyboardShortcutId } from './keybindings.mjs';
|
8
8
|
export var PanZoomMode;
|
9
9
|
(function (PanZoomMode) {
|
10
|
+
/** Touch gestures with a single pointer. Ignores non-touch gestures. */
|
10
11
|
PanZoomMode[PanZoomMode["OneFingerTouchGestures"] = 1] = "OneFingerTouchGestures";
|
12
|
+
/** Touch gestures with exactly two pointers. Ignores non-touch gestures. */
|
11
13
|
PanZoomMode[PanZoomMode["TwoFingerTouchGestures"] = 2] = "TwoFingerTouchGestures";
|
12
14
|
PanZoomMode[PanZoomMode["RightClickDrags"] = 4] = "RightClickDrags";
|
15
|
+
/** Single-pointer gestures of *any* type (including touch). */
|
13
16
|
PanZoomMode[PanZoomMode["SinglePointerGestures"] = 8] = "SinglePointerGestures";
|
17
|
+
/** Keyboard navigation (e.g. LeftArrow to move left). */
|
14
18
|
PanZoomMode[PanZoomMode["Keyboard"] = 16] = "Keyboard";
|
19
|
+
/** If provided, prevents **this** tool from rotating the viewport (other tools may still do so). */
|
15
20
|
PanZoomMode[PanZoomMode["RotationLocked"] = 32] = "RotationLocked";
|
16
21
|
})(PanZoomMode || (PanZoomMode = {}));
|
17
22
|
class InertialScroller {
|
@@ -59,6 +64,15 @@ class InertialScroller {
|
|
59
64
|
}
|
60
65
|
}
|
61
66
|
}
|
67
|
+
/**
|
68
|
+
* This tool moves the viewport in response to touchpad, touchscreen, mouse, and keyboard events.
|
69
|
+
*
|
70
|
+
* Which events are handled, and which are skipped, are determined by the tool's `mode`. For example,
|
71
|
+
* a `PanZoom` tool with `mode = PanZoomMode.TwoFingerTouchGestures|PanZoomMode.RightClickDrags` would
|
72
|
+
* respond to right-click drag events and two-finger touch gestures.
|
73
|
+
*
|
74
|
+
* @see {@link setModeEnabled}
|
75
|
+
*/
|
62
76
|
export default class PanZoom extends BaseTool {
|
63
77
|
constructor(editor, mode, description) {
|
64
78
|
super(editor.notifier, description);
|
@@ -422,8 +436,32 @@ export default class PanZoom extends BaseTool {
|
|
422
436
|
isRotationLocked() {
|
423
437
|
return !!(this.mode & PanZoomMode.RotationLocked);
|
424
438
|
}
|
425
|
-
|
426
|
-
|
439
|
+
/**
|
440
|
+
* Changes the types of gestures used by this pan/zoom tool.
|
441
|
+
*
|
442
|
+
* @see {@link PanZoomMode} {@link setMode}
|
443
|
+
*
|
444
|
+
* @example
|
445
|
+
* ```ts,runnable
|
446
|
+
* import { Editor, PanZoomTool, PanZoomMode } from 'js-draw';
|
447
|
+
*
|
448
|
+
* const editor = new Editor(document.body);
|
449
|
+
*
|
450
|
+
* // By default, there are multiple PanZoom tools that handle different events.
|
451
|
+
* // This gets all PanZoomTools.
|
452
|
+
* const panZoomToolList = editor.toolController.getMatchingTools(PanZoomTool);
|
453
|
+
*
|
454
|
+
* // The first PanZoomTool is the highest priority -- by default,
|
455
|
+
* // this tool is responsible for handling multi-finger touch gestures.
|
456
|
+
* //
|
457
|
+
* // Lower-priority PanZoomTools handle one-finger touch gestures and
|
458
|
+
* // key-presses.
|
459
|
+
* const panZoomTool = panZoomToolList[0];
|
460
|
+
*
|
461
|
+
* // Lock rotation for multi-finger touch gestures.
|
462
|
+
* panZoomTool.setModeEnabled(PanZoomMode.RotationLocked, true);
|
463
|
+
* ```
|
464
|
+
*/
|
427
465
|
setModeEnabled(mode, enabled) {
|
428
466
|
let newMode = this.mode;
|
429
467
|
if (enabled) {
|
@@ -434,6 +472,16 @@ export default class PanZoom extends BaseTool {
|
|
434
472
|
}
|
435
473
|
this.setMode(newMode);
|
436
474
|
}
|
475
|
+
/**
|
476
|
+
* Sets all modes for this tool using a bitmask.
|
477
|
+
*
|
478
|
+
* @see {@link setModeEnabled}
|
479
|
+
*
|
480
|
+
* @example
|
481
|
+
* ```ts
|
482
|
+
* tool.setMode(PanZoomMode.RotationLocked|PanZoomMode.TwoFingerTouchGestures);
|
483
|
+
* ```
|
484
|
+
*/
|
437
485
|
setMode(mode) {
|
438
486
|
if (mode !== this.mode) {
|
439
487
|
this.mode = mode;
|
@@ -443,6 +491,10 @@ export default class PanZoom extends BaseTool {
|
|
443
491
|
});
|
444
492
|
}
|
445
493
|
}
|
494
|
+
/**
|
495
|
+
* Returns a bitmask indicating the currently-enabled modes.
|
496
|
+
* @see {@link setModeEnabled}
|
497
|
+
*/
|
446
498
|
getMode() {
|
447
499
|
return this.mode;
|
448
500
|
}
|
@@ -10,7 +10,7 @@ import AbstractComponent from '../../components/AbstractComponent';
|
|
10
10
|
import Command from '../../commands/Command';
|
11
11
|
export default class Selection {
|
12
12
|
private editor;
|
13
|
-
private
|
13
|
+
private childwidgets;
|
14
14
|
private originalRegion;
|
15
15
|
private selectionTightBoundingBox;
|
16
16
|
private transformers;
|
@@ -52,7 +52,7 @@ export default class Selection {
|
|
52
52
|
private removedFromImage;
|
53
53
|
private addRemoveSelectionFromImage;
|
54
54
|
private removeDeletedElemsFromSelection;
|
55
|
-
private
|
55
|
+
private activeHandle;
|
56
56
|
private backgroundDragging;
|
57
57
|
onDragStart(pointer: Pointer): boolean;
|
58
58
|
onDragUpdate(pointer: Pointer): void;
|
@@ -29,7 +29,7 @@ class Selection {
|
|
29
29
|
this.hasParent = true;
|
30
30
|
// Maps IDs to whether we removed the component from the image
|
31
31
|
this.removedFromImage = {};
|
32
|
-
this.
|
32
|
+
this.activeHandle = null;
|
33
33
|
this.backgroundDragging = false;
|
34
34
|
this.selectionDuplicatedAnimationTimeout = null;
|
35
35
|
this.originalRegion = new Rect2(startPoint.x, startPoint.y, 0, 0);
|
@@ -71,14 +71,14 @@ class Selection {
|
|
71
71
|
side: Vec2.of(0.5, 0),
|
72
72
|
icon: this.editor.icons.makeRotateIcon(),
|
73
73
|
}, this, this.editor.viewport, (startPoint) => this.transformers.rotate.onDragStart(startPoint), (currentPoint) => this.transformers.rotate.onDragUpdate(currentPoint), () => this.transformers.rotate.onDragEnd());
|
74
|
-
this.
|
74
|
+
this.childwidgets = [
|
75
75
|
resizeBothHandle,
|
76
76
|
...resizeHorizontalHandles,
|
77
77
|
resizeVerticalHandle,
|
78
78
|
rotationHandle,
|
79
79
|
];
|
80
|
-
for (const
|
81
|
-
|
80
|
+
for (const widget of this.childwidgets) {
|
81
|
+
widget.addTo(this.backgroundElem);
|
82
82
|
}
|
83
83
|
this.updateUI();
|
84
84
|
}
|
@@ -293,8 +293,8 @@ class Selection {
|
|
293
293
|
else {
|
294
294
|
this.innerContainer.classList.remove('-empty');
|
295
295
|
}
|
296
|
-
for (const
|
297
|
-
|
296
|
+
for (const widget of this.childwidgets) {
|
297
|
+
widget.updatePosition(this.getScreenRegion());
|
298
298
|
}
|
299
299
|
}
|
300
300
|
// Add/remove the contents of this seleciton from the editor.
|
@@ -348,16 +348,16 @@ class Selection {
|
|
348
348
|
onDragStart(pointer) {
|
349
349
|
// Clear the HTML selection (prevent HTML drag and drop being triggered by this drag)
|
350
350
|
document.getSelection()?.removeAllRanges();
|
351
|
-
this.
|
351
|
+
this.activeHandle = null;
|
352
352
|
let result = false;
|
353
353
|
this.backgroundDragging = false;
|
354
354
|
if (this.region.containsPoint(pointer.canvasPos)) {
|
355
355
|
this.backgroundDragging = true;
|
356
356
|
result = true;
|
357
357
|
}
|
358
|
-
for (const
|
359
|
-
if (
|
360
|
-
this.
|
358
|
+
for (const widget of this.childwidgets) {
|
359
|
+
if (widget.containsPoint(pointer.canvasPos)) {
|
360
|
+
this.activeHandle = widget;
|
361
361
|
this.backgroundDragging = false;
|
362
362
|
result = true;
|
363
363
|
}
|
@@ -366,8 +366,8 @@ class Selection {
|
|
366
366
|
this.removeDeletedElemsFromSelection();
|
367
367
|
this.addRemoveSelectionFromImage(false);
|
368
368
|
}
|
369
|
-
if (this.
|
370
|
-
this.
|
369
|
+
if (this.activeHandle) {
|
370
|
+
this.activeHandle.handleDragStart(pointer);
|
371
371
|
}
|
372
372
|
if (this.backgroundDragging) {
|
373
373
|
this.transformers.drag.onDragStart(pointer.canvasPos);
|
@@ -378,25 +378,25 @@ class Selection {
|
|
378
378
|
if (this.backgroundDragging) {
|
379
379
|
this.transformers.drag.onDragUpdate(pointer.canvasPos);
|
380
380
|
}
|
381
|
-
if (this.
|
382
|
-
this.
|
381
|
+
if (this.activeHandle) {
|
382
|
+
this.activeHandle.handleDragUpdate(pointer);
|
383
383
|
}
|
384
384
|
}
|
385
385
|
onDragEnd() {
|
386
386
|
if (this.backgroundDragging) {
|
387
387
|
this.transformers.drag.onDragEnd();
|
388
388
|
}
|
389
|
-
else if (this.
|
390
|
-
this.
|
389
|
+
else if (this.activeHandle) {
|
390
|
+
this.activeHandle.handleDragEnd();
|
391
391
|
}
|
392
392
|
this.addRemoveSelectionFromImage(true);
|
393
393
|
this.backgroundDragging = false;
|
394
|
-
this.
|
394
|
+
this.activeHandle = null;
|
395
395
|
this.updateUI();
|
396
396
|
}
|
397
397
|
onDragCancel() {
|
398
398
|
this.backgroundDragging = false;
|
399
|
-
this.
|
399
|
+
this.activeHandle = null;
|
400
400
|
this.setTransform(Mat33.identity);
|
401
401
|
this.addRemoveSelectionFromImage(true);
|
402
402
|
this.updateUI();
|
@@ -424,7 +424,7 @@ class Selection {
|
|
424
424
|
return false;
|
425
425
|
}
|
426
426
|
deleteSelectedObjects() {
|
427
|
-
if (this.backgroundDragging || this.
|
427
|
+
if (this.backgroundDragging || this.activeHandle) {
|
428
428
|
this.onDragEnd();
|
429
429
|
}
|
430
430
|
return new Erase(this.selectedElems);
|
@@ -441,7 +441,7 @@ class Selection {
|
|
441
441
|
}, animationDuration);
|
442
442
|
}
|
443
443
|
async duplicateSelectedObjects() {
|
444
|
-
const wasTransforming = this.backgroundDragging || this.
|
444
|
+
const wasTransforming = this.backgroundDragging || this.activeHandle;
|
445
445
|
let tmpApplyCommand = null;
|
446
446
|
if (!wasTransforming) {
|
447
447
|
this.runSelectionDuplicatedAnimation();
|
@@ -2,6 +2,7 @@ import { Point2, Vec2 } from '@js-draw/math';
|
|
2
2
|
import Selection from './Selection';
|
3
3
|
import Pointer from '../../Pointer';
|
4
4
|
import Viewport from '../../Viewport';
|
5
|
+
import { SelectionBoxChild } from './types';
|
5
6
|
export declare enum HandleAction {
|
6
7
|
ResizeXY = "resize-xy",
|
7
8
|
Rotate = "rotate",
|
@@ -17,7 +18,7 @@ export declare const handleSize = 30;
|
|
17
18
|
export type DragStartCallback = (startPoint: Point2) => void;
|
18
19
|
export type DragUpdateCallback = (canvasPoint: Point2) => void;
|
19
20
|
export type DragEndCallback = () => Promise<void> | void;
|
20
|
-
export default class SelectionHandle {
|
21
|
+
export default class SelectionHandle implements SelectionBoxChild {
|
21
22
|
readonly presentation: HandlePresentation;
|
22
23
|
private readonly parent;
|
23
24
|
private readonly viewport;
|
@@ -34,6 +35,11 @@ export default class SelectionHandle {
|
|
34
35
|
* element visible on the screen.
|
35
36
|
*/
|
36
37
|
addTo(container: HTMLElement): void;
|
38
|
+
/**
|
39
|
+
* Removes this element from its container. Should only be called
|
40
|
+
* after {@link addTo}.
|
41
|
+
*/
|
42
|
+
remove(): void;
|
37
43
|
/**
|
38
44
|
* Returns this handle's bounding box relative to the top left of the
|
39
45
|
* selection box.
|
@@ -48,7 +54,7 @@ export default class SelectionHandle {
|
|
48
54
|
/** @returns true iff `point` (in editor **canvas** coordinates) is in this. */
|
49
55
|
containsPoint(point: Point2): boolean;
|
50
56
|
private dragLastPos;
|
51
|
-
handleDragStart(pointer: Pointer):
|
57
|
+
handleDragStart(pointer: Pointer): boolean;
|
52
58
|
handleDragUpdate(pointer: Pointer): void;
|
53
59
|
handleDragEnd(): void | Promise<void>;
|
54
60
|
setSnapToGrid(snap: boolean): void;
|