|
@@ -41,7 +41,7 @@ import {
|
|
} from "./scene";
|
|
} from "./scene";
|
|
|
|
|
|
import { renderScene } from "./renderer";
|
|
import { renderScene } from "./renderer";
|
|
-import { AppState, FlooredNumber } from "./types";
|
|
|
|
|
|
+import { AppState, FlooredNumber, Gesture } from "./types";
|
|
import { ExcalidrawElement } from "./element/types";
|
|
import { ExcalidrawElement } from "./element/types";
|
|
|
|
|
|
import {
|
|
import {
|
|
@@ -108,6 +108,7 @@ import useIsMobile, { IsMobileProvider } from "./is-mobile";
|
|
|
|
|
|
import { copyToAppClipboard, getClipboardContent } from "./clipboard";
|
|
import { copyToAppClipboard, getClipboardContent } from "./clipboard";
|
|
import { normalizeScroll } from "./scene/data";
|
|
import { normalizeScroll } from "./scene/data";
|
|
|
|
+import { getCenter, getDistance } from "./gesture";
|
|
|
|
|
|
let { elements } = createScene();
|
|
let { elements } = createScene();
|
|
const { history } = createHistory();
|
|
const { history } = createHistory();
|
|
@@ -130,10 +131,11 @@ const CURSOR_TYPE = {
|
|
CROSSHAIR: "crosshair",
|
|
CROSSHAIR: "crosshair",
|
|
GRABBING: "grabbing",
|
|
GRABBING: "grabbing",
|
|
};
|
|
};
|
|
-const MOUSE_BUTTON = {
|
|
|
|
|
|
+const POINTER_BUTTON = {
|
|
MAIN: 0,
|
|
MAIN: 0,
|
|
WHEEL: 1,
|
|
WHEEL: 1,
|
|
SECONDARY: 2,
|
|
SECONDARY: 2,
|
|
|
|
+ TOUCH: -1,
|
|
};
|
|
};
|
|
|
|
|
|
// Block pinch-zooming on iOS outside of the content area
|
|
// Block pinch-zooming on iOS outside of the content area
|
|
@@ -148,7 +150,13 @@ document.addEventListener(
|
|
{ passive: false },
|
|
{ passive: false },
|
|
);
|
|
);
|
|
|
|
|
|
-let lastMouseUp: ((e: any) => void) | null = null;
|
|
|
|
|
|
+let lastPointerUp: ((e: any) => void) | null = null;
|
|
|
|
+const gesture: Gesture = {
|
|
|
|
+ pointers: [],
|
|
|
|
+ lastCenter: null,
|
|
|
|
+ initialDistance: null,
|
|
|
|
+ initialScale: null,
|
|
|
|
+};
|
|
|
|
|
|
export function viewportCoordsToSceneCoords(
|
|
export function viewportCoordsToSceneCoords(
|
|
{ clientX, clientY }: { clientX: number; clientY: number },
|
|
{ clientX, clientY }: { clientX: number; clientY: number },
|
|
@@ -202,7 +210,6 @@ let cursorX = 0;
|
|
let cursorY = 0;
|
|
let cursorY = 0;
|
|
let isHoldingSpace: boolean = false;
|
|
let isHoldingSpace: boolean = false;
|
|
let isPanning: boolean = false;
|
|
let isPanning: boolean = false;
|
|
-let isHoldingMouseButton: boolean = false;
|
|
|
|
|
|
|
|
interface LayerUIProps {
|
|
interface LayerUIProps {
|
|
actionManager: ActionManager;
|
|
actionManager: ActionManager;
|
|
@@ -279,17 +286,15 @@ const LayerUI = React.memo(
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
- function renderSelectedShapeActions(
|
|
|
|
- elements: readonly ExcalidrawElement[],
|
|
|
|
- ) {
|
|
|
|
|
|
+ const showSelectedShapeActions =
|
|
|
|
+ (appState.editingElement || getSelectedElements(elements).length) &&
|
|
|
|
+ appState.elementType === "selection";
|
|
|
|
+
|
|
|
|
+ function renderSelectedShapeActions() {
|
|
const { elementType, editingElement } = appState;
|
|
const { elementType, editingElement } = appState;
|
|
const targetElements = editingElement
|
|
const targetElements = editingElement
|
|
? [editingElement]
|
|
? [editingElement]
|
|
: getSelectedElements(elements);
|
|
: getSelectedElements(elements);
|
|
- if (!targetElements.length && elementType === "selection") {
|
|
|
|
- return null;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
return (
|
|
return (
|
|
<div className="panelColumn">
|
|
<div className="panelColumn">
|
|
{actionManager.renderAction("changeStrokeColor")}
|
|
{actionManager.renderAction("changeStrokeColor")}
|
|
@@ -331,8 +336,6 @@ const LayerUI = React.memo(
|
|
{actionManager.renderAction("bringForward")}
|
|
{actionManager.renderAction("bringForward")}
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
</fieldset>
|
|
-
|
|
|
|
- {actionManager.renderAction("deleteSelectedElements")}
|
|
|
|
</div>
|
|
</div>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
@@ -418,7 +421,7 @@ const LayerUI = React.memo(
|
|
</Stack.Col>
|
|
</Stack.Col>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</section>
|
|
- ) : appState.openedMenu === "shape" ? (
|
|
|
|
|
|
+ ) : appState.openedMenu === "shape" && showSelectedShapeActions ? (
|
|
<section
|
|
<section
|
|
className="App-mobile-menu"
|
|
className="App-mobile-menu"
|
|
aria-labelledby="selected-shape-title"
|
|
aria-labelledby="selected-shape-title"
|
|
@@ -427,7 +430,7 @@ const LayerUI = React.memo(
|
|
{t("headings.selectedShapeActions")}
|
|
{t("headings.selectedShapeActions")}
|
|
</h2>
|
|
</h2>
|
|
<div className="App-mobile-menu-scroller">
|
|
<div className="App-mobile-menu-scroller">
|
|
- {renderSelectedShapeActions(elements)}
|
|
|
|
|
|
+ {renderSelectedShapeActions()}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</section>
|
|
) : null}
|
|
) : null}
|
|
@@ -444,6 +447,12 @@ const LayerUI = React.memo(
|
|
</Stack.Row>
|
|
</Stack.Row>
|
|
</Stack.Col>
|
|
</Stack.Col>
|
|
</section>
|
|
</section>
|
|
|
|
+ <HintViewer
|
|
|
|
+ elementType={appState.elementType}
|
|
|
|
+ multiMode={appState.multiElement !== null}
|
|
|
|
+ isResizing={appState.isResizing}
|
|
|
|
+ elements={elements}
|
|
|
|
+ />
|
|
</FixedSideContainer>
|
|
</FixedSideContainer>
|
|
<footer className="App-toolbar">
|
|
<footer className="App-toolbar">
|
|
<div className="App-toolbar-content">
|
|
<div className="App-toolbar-content">
|
|
@@ -459,7 +468,18 @@ const LayerUI = React.memo(
|
|
}))
|
|
}))
|
|
}
|
|
}
|
|
/>
|
|
/>
|
|
|
|
+ <div
|
|
|
|
+ style={{
|
|
|
|
+ visibility: isSomeElementSelected(elements)
|
|
|
|
+ ? "visible"
|
|
|
|
+ : "hidden",
|
|
|
|
+ }}
|
|
|
|
+ >
|
|
|
|
+ {" "}
|
|
|
|
+ {actionManager.renderAction("deleteSelectedElements")}
|
|
|
|
+ </div>
|
|
{lockButton}
|
|
{lockButton}
|
|
|
|
+ {actionManager.renderAction("finalize")}
|
|
<div
|
|
<div
|
|
style={{
|
|
style={{
|
|
visibility: isSomeElementSelected(elements)
|
|
visibility: isSomeElementSelected(elements)
|
|
@@ -482,12 +502,6 @@ const LayerUI = React.memo(
|
|
}
|
|
}
|
|
/>
|
|
/>
|
|
</div>
|
|
</div>
|
|
- <HintViewer
|
|
|
|
- elementType={appState.elementType}
|
|
|
|
- multiMode={appState.multiElement !== null}
|
|
|
|
- isResizing={appState.isResizing}
|
|
|
|
- elements={elements}
|
|
|
|
- />
|
|
|
|
{appState.scrolledOutside && (
|
|
{appState.scrolledOutside && (
|
|
<button
|
|
<button
|
|
className="scroll-back-to-content"
|
|
className="scroll-back-to-content"
|
|
@@ -525,17 +539,17 @@ const LayerUI = React.memo(
|
|
</Stack.Col>
|
|
</Stack.Col>
|
|
</Island>
|
|
</Island>
|
|
</section>
|
|
</section>
|
|
- <section
|
|
|
|
- className="App-right-menu"
|
|
|
|
- aria-labelledby="selected-shape-title"
|
|
|
|
- >
|
|
|
|
- <h2 className="visually-hidden" id="selected-shape-title">
|
|
|
|
- {t("headings.selectedShapeActions")}
|
|
|
|
- </h2>
|
|
|
|
- <Island padding={4}>
|
|
|
|
- {renderSelectedShapeActions(elements)}
|
|
|
|
- </Island>
|
|
|
|
- </section>
|
|
|
|
|
|
+ {showSelectedShapeActions ? (
|
|
|
|
+ <section
|
|
|
|
+ className="App-right-menu"
|
|
|
|
+ aria-labelledby="selected-shape-title"
|
|
|
|
+ >
|
|
|
|
+ <h2 className="visually-hidden" id="selected-shape-title">
|
|
|
|
+ {t("headings.selectedShapeActions")}
|
|
|
|
+ </h2>
|
|
|
|
+ <Island padding={4}>{renderSelectedShapeActions()}</Island>
|
|
|
|
+ </section>
|
|
|
|
+ ) : null}
|
|
</Stack.Col>
|
|
</Stack.Col>
|
|
<section aria-labelledby="shapes-title">
|
|
<section aria-labelledby="shapes-title">
|
|
<Stack.Col gap={4} align="start">
|
|
<Stack.Col gap={4} align="start">
|
|
@@ -858,7 +872,7 @@ export class App extends React.Component<any, AppState> {
|
|
this.setState({ ...data.appState });
|
|
this.setState({ ...data.appState });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- } else if (event.key === KEYS.SPACE && !isHoldingMouseButton) {
|
|
|
|
|
|
+ } else if (event.key === KEYS.SPACE && gesture.pointers.length === 0) {
|
|
isHoldingSpace = true;
|
|
isHoldingSpace = true;
|
|
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
|
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
|
}
|
|
}
|
|
@@ -953,6 +967,10 @@ export class App extends React.Component<any, AppState> {
|
|
this.setState({});
|
|
this.setState({});
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+ removePointer = (e: React.PointerEvent<HTMLElement>) => {
|
|
|
|
+ gesture.pointers = gesture.pointers.filter(p => p.id !== e.pointerId);
|
|
|
|
+ };
|
|
|
|
+
|
|
public render() {
|
|
public render() {
|
|
const canvasDOMWidth = window.innerWidth;
|
|
const canvasDOMWidth = window.innerWidth;
|
|
const canvasDOMHeight = window.innerHeight;
|
|
const canvasDOMHeight = window.innerHeight;
|
|
@@ -1055,12 +1073,12 @@ export class App extends React.Component<any, AppState> {
|
|
left: e.clientX,
|
|
left: e.clientX,
|
|
});
|
|
});
|
|
}}
|
|
}}
|
|
- onMouseDown={e => {
|
|
|
|
- if (lastMouseUp !== null) {
|
|
|
|
- // Unfortunately, sometimes we don't get a mouseup after a mousedown,
|
|
|
|
|
|
+ onPointerDown={e => {
|
|
|
|
+ if (lastPointerUp !== null) {
|
|
|
|
+ // Unfortunately, sometimes we don't get a pointerup after a pointerdown,
|
|
// this can happen when a contextual menu or alert is triggered. In order to avoid
|
|
// this can happen when a contextual menu or alert is triggered. In order to avoid
|
|
- // being in a weird state, we clean up on the next mousedown
|
|
|
|
- lastMouseUp(e);
|
|
|
|
|
|
+ // being in a weird state, we clean up on the next pointerdown
|
|
|
|
+ lastPointerUp(e);
|
|
}
|
|
}
|
|
|
|
|
|
if (isPanning) {
|
|
if (isPanning) {
|
|
@@ -1069,15 +1087,14 @@ export class App extends React.Component<any, AppState> {
|
|
|
|
|
|
// pan canvas on wheel button drag or space+drag
|
|
// pan canvas on wheel button drag or space+drag
|
|
if (
|
|
if (
|
|
- !isHoldingMouseButton &&
|
|
|
|
- (e.button === MOUSE_BUTTON.WHEEL ||
|
|
|
|
- (e.button === MOUSE_BUTTON.MAIN && isHoldingSpace))
|
|
|
|
|
|
+ gesture.pointers.length === 0 &&
|
|
|
|
+ (e.button === POINTER_BUTTON.WHEEL ||
|
|
|
|
+ (e.button === POINTER_BUTTON.MAIN && isHoldingSpace))
|
|
) {
|
|
) {
|
|
- isHoldingMouseButton = true;
|
|
|
|
isPanning = true;
|
|
isPanning = true;
|
|
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
|
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
|
let { clientX: lastX, clientY: lastY } = e;
|
|
let { clientX: lastX, clientY: lastY } = e;
|
|
- const onMouseMove = (e: MouseEvent) => {
|
|
|
|
|
|
+ const onPointerMove = (e: PointerEvent) => {
|
|
const deltaX = lastX - e.clientX;
|
|
const deltaX = lastX - e.clientX;
|
|
const deltaY = lastY - e.clientY;
|
|
const deltaY = lastY - e.clientY;
|
|
lastX = e.clientX;
|
|
lastX = e.clientX;
|
|
@@ -1092,30 +1109,44 @@ export class App extends React.Component<any, AppState> {
|
|
),
|
|
),
|
|
});
|
|
});
|
|
};
|
|
};
|
|
- const teardown = (lastMouseUp = () => {
|
|
|
|
- lastMouseUp = null;
|
|
|
|
|
|
+ const teardown = (lastPointerUp = () => {
|
|
|
|
+ lastPointerUp = null;
|
|
isPanning = false;
|
|
isPanning = false;
|
|
- isHoldingMouseButton = false;
|
|
|
|
if (!isHoldingSpace) {
|
|
if (!isHoldingSpace) {
|
|
setCursorForShape(this.state.elementType);
|
|
setCursorForShape(this.state.elementType);
|
|
}
|
|
}
|
|
- window.removeEventListener("mousemove", onMouseMove);
|
|
|
|
- window.removeEventListener("mouseup", teardown);
|
|
|
|
|
|
+ window.removeEventListener("pointermove", onPointerMove);
|
|
|
|
+ window.removeEventListener("pointerup", teardown);
|
|
window.removeEventListener("blur", teardown);
|
|
window.removeEventListener("blur", teardown);
|
|
});
|
|
});
|
|
window.addEventListener("blur", teardown);
|
|
window.addEventListener("blur", teardown);
|
|
- window.addEventListener("mousemove", onMouseMove, {
|
|
|
|
|
|
+ window.addEventListener("pointermove", onPointerMove, {
|
|
passive: true,
|
|
passive: true,
|
|
});
|
|
});
|
|
- window.addEventListener("mouseup", teardown);
|
|
|
|
|
|
+ window.addEventListener("pointerup", teardown);
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
- // only handle left mouse button
|
|
|
|
- if (e.button !== MOUSE_BUTTON.MAIN) {
|
|
|
|
|
|
+ // only handle left mouse button or touch
|
|
|
|
+ if (
|
|
|
|
+ e.button !== POINTER_BUTTON.MAIN &&
|
|
|
|
+ e.button !== POINTER_BUTTON.TOUCH
|
|
|
|
+ ) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
- // fixes mousemove causing selection of UI texts #32
|
|
|
|
|
|
+
|
|
|
|
+ gesture.pointers.push({
|
|
|
|
+ id: e.pointerId,
|
|
|
|
+ x: e.clientX,
|
|
|
|
+ y: e.clientY,
|
|
|
|
+ });
|
|
|
|
+ if (gesture.pointers.length === 2) {
|
|
|
|
+ gesture.lastCenter = getCenter(gesture.pointers);
|
|
|
|
+ gesture.initialScale = this.state.zoom;
|
|
|
|
+ gesture.initialDistance = getDistance(gesture.pointers);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // fixes pointermove causing selection of UI texts #32
|
|
e.preventDefault();
|
|
e.preventDefault();
|
|
// Preventing the event above disables default behavior
|
|
// Preventing the event above disables default behavior
|
|
// of defocusing potentially focused element, which is what we
|
|
// of defocusing potentially focused element, which is what we
|
|
@@ -1124,6 +1155,11 @@ export class App extends React.Component<any, AppState> {
|
|
document.activeElement.blur();
|
|
document.activeElement.blur();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ // don't select while panning
|
|
|
|
+ if (gesture.pointers.length > 1) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
// Handle scrollbars dragging
|
|
// Handle scrollbars dragging
|
|
const {
|
|
const {
|
|
isOverHorizontalScrollBar,
|
|
isOverHorizontalScrollBar,
|
|
@@ -1216,7 +1252,7 @@ export class App extends React.Component<any, AppState> {
|
|
elementIsAddedToSelection = true;
|
|
elementIsAddedToSelection = true;
|
|
}
|
|
}
|
|
|
|
|
|
- // We duplicate the selected element if alt is pressed on Mouse down
|
|
|
|
|
|
+ // We duplicate the selected element if alt is pressed on pointer down
|
|
if (e.altKey) {
|
|
if (e.altKey) {
|
|
elements = [
|
|
elements = [
|
|
...elements.map(element => ({
|
|
...elements.map(element => ({
|
|
@@ -1352,8 +1388,8 @@ export class App extends React.Component<any, AppState> {
|
|
p1: Point,
|
|
p1: Point,
|
|
deltaX: number,
|
|
deltaX: number,
|
|
deltaY: number,
|
|
deltaY: number,
|
|
- mouseX: number,
|
|
|
|
- mouseY: number,
|
|
|
|
|
|
+ pointerX: number,
|
|
|
|
+ pointerY: number,
|
|
perfect: boolean,
|
|
perfect: boolean,
|
|
) => void)
|
|
) => void)
|
|
| null = null;
|
|
| null = null;
|
|
@@ -1363,8 +1399,8 @@ export class App extends React.Component<any, AppState> {
|
|
p1: Point,
|
|
p1: Point,
|
|
deltaX: number,
|
|
deltaX: number,
|
|
deltaY: number,
|
|
deltaY: number,
|
|
- mouseX: number,
|
|
|
|
- mouseY: number,
|
|
|
|
|
|
+ pointerX: number,
|
|
|
|
+ pointerY: number,
|
|
perfect: boolean,
|
|
perfect: boolean,
|
|
) => {
|
|
) => {
|
|
if (perfect) {
|
|
if (perfect) {
|
|
@@ -1373,8 +1409,8 @@ export class App extends React.Component<any, AppState> {
|
|
|
|
|
|
const { width, height } = getPerfectElementSize(
|
|
const { width, height } = getPerfectElementSize(
|
|
element.type,
|
|
element.type,
|
|
- mouseX - element.x - p1[0],
|
|
|
|
- mouseY - element.y - p1[1],
|
|
|
|
|
|
+ pointerX - element.x - p1[0],
|
|
|
|
+ pointerY - element.y - p1[1],
|
|
);
|
|
);
|
|
|
|
|
|
const dx = element.x + width + p1[0];
|
|
const dx = element.x + width + p1[0];
|
|
@@ -1396,15 +1432,15 @@ export class App extends React.Component<any, AppState> {
|
|
p1: Point,
|
|
p1: Point,
|
|
deltaX: number,
|
|
deltaX: number,
|
|
deltaY: number,
|
|
deltaY: number,
|
|
- mouseX: number,
|
|
|
|
- mouseY: number,
|
|
|
|
|
|
+ pointerX: number,
|
|
|
|
+ pointerY: number,
|
|
perfect: boolean,
|
|
perfect: boolean,
|
|
) => {
|
|
) => {
|
|
if (perfect) {
|
|
if (perfect) {
|
|
const { width, height } = getPerfectElementSize(
|
|
const { width, height } = getPerfectElementSize(
|
|
element.type,
|
|
element.type,
|
|
- mouseX - element.x,
|
|
|
|
- mouseY - element.y,
|
|
|
|
|
|
+ pointerX - element.x,
|
|
|
|
+ pointerY - element.y,
|
|
);
|
|
);
|
|
p1[0] = width;
|
|
p1[0] = width;
|
|
p1[1] = height;
|
|
p1[1] = height;
|
|
@@ -1414,7 +1450,7 @@ export class App extends React.Component<any, AppState> {
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
- const onMouseMove = (e: MouseEvent) => {
|
|
|
|
|
|
+ const onPointerMove = (e: PointerEvent) => {
|
|
const target = e.target;
|
|
const target = e.target;
|
|
if (!(target instanceof HTMLElement)) {
|
|
if (!(target instanceof HTMLElement)) {
|
|
return;
|
|
return;
|
|
@@ -1447,7 +1483,7 @@ export class App extends React.Component<any, AppState> {
|
|
// for arrows, don't start dragging until a given threshold
|
|
// for arrows, don't start dragging until a given threshold
|
|
// to ensure we don't create a 2-point arrow by mistake when
|
|
// to ensure we don't create a 2-point arrow by mistake when
|
|
// user clicks mouse in a way that it moves a tiny bit (thus
|
|
// user clicks mouse in a way that it moves a tiny bit (thus
|
|
- // triggering mousemove)
|
|
|
|
|
|
+ // triggering pointermove)
|
|
if (
|
|
if (
|
|
!draggingOccurred &&
|
|
!draggingOccurred &&
|
|
(this.state.elementType === "arrow" ||
|
|
(this.state.elementType === "arrow" ||
|
|
@@ -1691,7 +1727,7 @@ export class App extends React.Component<any, AppState> {
|
|
|
|
|
|
if (hitElement?.isSelected) {
|
|
if (hitElement?.isSelected) {
|
|
// Marking that click was used for dragging to check
|
|
// Marking that click was used for dragging to check
|
|
- // if elements should be deselected on mouseup
|
|
|
|
|
|
+ // if elements should be deselected on pointerup
|
|
draggingOccurred = true;
|
|
draggingOccurred = true;
|
|
const selectedElements = getSelectedElements(elements);
|
|
const selectedElements = getSelectedElements(elements);
|
|
if (selectedElements.length > 0) {
|
|
if (selectedElements.length > 0) {
|
|
@@ -1790,7 +1826,7 @@ export class App extends React.Component<any, AppState> {
|
|
this.setState({});
|
|
this.setState({});
|
|
};
|
|
};
|
|
|
|
|
|
- const onMouseUp = (e: MouseEvent) => {
|
|
|
|
|
|
+ const onPointerUp = (e: PointerEvent) => {
|
|
const {
|
|
const {
|
|
draggingElement,
|
|
draggingElement,
|
|
resizingElement,
|
|
resizingElement,
|
|
@@ -1806,10 +1842,9 @@ export class App extends React.Component<any, AppState> {
|
|
});
|
|
});
|
|
|
|
|
|
resizeArrowFn = null;
|
|
resizeArrowFn = null;
|
|
- lastMouseUp = null;
|
|
|
|
- isHoldingMouseButton = false;
|
|
|
|
- window.removeEventListener("mousemove", onMouseMove);
|
|
|
|
- window.removeEventListener("mouseup", onMouseUp);
|
|
|
|
|
|
+ lastPointerUp = null;
|
|
|
|
+ window.removeEventListener("pointermove", onPointerMove);
|
|
|
|
+ window.removeEventListener("pointerup", onPointerUp);
|
|
|
|
|
|
if (elementType === "arrow" || elementType === "line") {
|
|
if (elementType === "arrow" || elementType === "line") {
|
|
if (draggingElement!.points.length > 1) {
|
|
if (draggingElement!.points.length > 1) {
|
|
@@ -1850,7 +1885,7 @@ export class App extends React.Component<any, AppState> {
|
|
draggingElement &&
|
|
draggingElement &&
|
|
isInvisiblySmallElement(draggingElement)
|
|
isInvisiblySmallElement(draggingElement)
|
|
) {
|
|
) {
|
|
- // remove invisible element which was added in onMouseDown
|
|
|
|
|
|
+ // remove invisible element which was added in onPointerDown
|
|
elements = elements.slice(0, -1);
|
|
elements = elements.slice(0, -1);
|
|
this.setState({
|
|
this.setState({
|
|
draggingElement: null,
|
|
draggingElement: null,
|
|
@@ -1882,7 +1917,7 @@ export class App extends React.Component<any, AppState> {
|
|
// from hitted element
|
|
// from hitted element
|
|
//
|
|
//
|
|
// If click occurred and elements were dragged or some element
|
|
// If click occurred and elements were dragged or some element
|
|
- // was added to selection (on mousedown phase) we need to keep
|
|
|
|
|
|
+ // was added to selection (on pointerdown phase) we need to keep
|
|
// selection unchanged
|
|
// selection unchanged
|
|
if (
|
|
if (
|
|
hitElement &&
|
|
hitElement &&
|
|
@@ -1928,10 +1963,10 @@ export class App extends React.Component<any, AppState> {
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
- lastMouseUp = onMouseUp;
|
|
|
|
|
|
+ lastPointerUp = onPointerUp;
|
|
|
|
|
|
- window.addEventListener("mousemove", onMouseMove);
|
|
|
|
- window.addEventListener("mouseup", onMouseUp);
|
|
|
|
|
|
+ window.addEventListener("pointermove", onPointerMove);
|
|
|
|
+ window.addEventListener("pointerup", onPointerUp);
|
|
}}
|
|
}}
|
|
onDoubleClick={e => {
|
|
onDoubleClick={e => {
|
|
resetCursor();
|
|
resetCursor();
|
|
@@ -2048,7 +2083,39 @@ export class App extends React.Component<any, AppState> {
|
|
},
|
|
},
|
|
});
|
|
});
|
|
}}
|
|
}}
|
|
- onMouseMove={e => {
|
|
|
|
|
|
+ onPointerMove={e => {
|
|
|
|
+ gesture.pointers = gesture.pointers.map(p =>
|
|
|
|
+ p.id === e.pointerId
|
|
|
|
+ ? {
|
|
|
|
+ id: e.pointerId,
|
|
|
|
+ x: e.clientX,
|
|
|
|
+ y: e.clientY,
|
|
|
|
+ }
|
|
|
|
+ : p,
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ if (gesture.pointers.length === 2) {
|
|
|
|
+ const center = getCenter(gesture.pointers);
|
|
|
|
+ const deltaX = center.x - gesture.lastCenter!.x;
|
|
|
|
+ const deltaY = center.y - gesture.lastCenter!.y;
|
|
|
|
+ gesture.lastCenter = center;
|
|
|
|
+
|
|
|
|
+ const distance = getDistance(gesture.pointers);
|
|
|
|
+ const scaleFactor = distance / gesture.initialDistance!;
|
|
|
|
+
|
|
|
|
+ this.setState({
|
|
|
|
+ scrollX: normalizeScroll(
|
|
|
|
+ this.state.scrollX + deltaX / this.state.zoom,
|
|
|
|
+ ),
|
|
|
|
+ scrollY: normalizeScroll(
|
|
|
|
+ this.state.scrollY + deltaY / this.state.zoom,
|
|
|
|
+ ),
|
|
|
|
+ zoom: getNormalizedZoom(gesture.initialScale! * scaleFactor),
|
|
|
|
+ });
|
|
|
|
+ } else {
|
|
|
|
+ gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null;
|
|
|
|
+ }
|
|
|
|
+
|
|
if (isHoldingSpace || isPanning) {
|
|
if (isHoldingSpace || isPanning) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
@@ -2101,6 +2168,8 @@ export class App extends React.Component<any, AppState> {
|
|
);
|
|
);
|
|
document.documentElement.style.cursor = hitElement ? "move" : "";
|
|
document.documentElement.style.cursor = hitElement ? "move" : "";
|
|
}}
|
|
}}
|
|
|
|
+ onPointerUp={this.removePointer}
|
|
|
|
+ onPointerCancel={this.removePointer}
|
|
onDrop={e => {
|
|
onDrop={e => {
|
|
const file = e.dataTransfer.files[0];
|
|
const file = e.dataTransfer.files[0];
|
|
if (file?.type === "application/json") {
|
|
if (file?.type === "application/json") {
|