Pull onPointerDown, onDoubleClick, onPointerMove into instance methods (#876)

* Pull onPointerDown, onDoubleClick, onPointerMove into instance methods

* Use bound instance methods
This commit is contained in:
Pete Hunt 2020-03-08 18:09:45 -07:00 committed by GitHub
parent c89584832d
commit 92ba401da8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -571,7 +571,255 @@ export class App extends React.Component<any, AppState> {
left: event.clientX, left: event.clientX,
}); });
}} }}
onPointerDown={event => { onPointerDown={this.handleCanvasPointerDown}
onDoubleClick={this.handleCanvasDoubleClick}
onPointerMove={this.handleCanvasPointerMove}
onPointerUp={this.removePointer}
onPointerCancel={this.removePointer}
onDrop={event => {
const file = event.dataTransfer.files[0];
if (
file?.type === "application/json" ||
file?.name.endsWith(".excalidraw")
) {
loadFromBlob(file)
.then(({ elements, appState }) =>
this.syncActionResult({ elements, appState }),
)
.catch(error => console.error(error));
}
}}
>
{t("labels.drawingCanvas")}
</canvas>
</main>
</div>
);
}
private handleCanvasDoubleClick = (
event: React.MouseEvent<HTMLCanvasElement>,
) => {
resetCursor();
const { x, y } = viewportCoordsToSceneCoords(
event,
this.state,
this.canvas,
);
const elementAtPosition = getElementAtPosition(
elements,
this.state,
x,
y,
this.state.zoom,
);
const element =
elementAtPosition && isTextElement(elementAtPosition)
? elementAtPosition
: newTextElement(
newElement(
"text",
x,
y,
this.state.currentItemStrokeColor,
this.state.currentItemBackgroundColor,
this.state.currentItemFillStyle,
this.state.currentItemStrokeWidth,
this.state.currentItemRoughness,
this.state.currentItemOpacity,
),
"", // default text
this.state.currentItemFont, // default font
);
this.setState({ editingElement: element });
let textX = event.clientX;
let textY = event.clientY;
if (elementAtPosition && isTextElement(elementAtPosition)) {
elements = elements.filter(
element => element.id !== elementAtPosition.id,
);
this.setState({});
const centerElementX = elementAtPosition.x + elementAtPosition.width / 2;
const centerElementY = elementAtPosition.y + elementAtPosition.height / 2;
const {
x: centerElementXInViewport,
y: centerElementYInViewport,
} = sceneCoordsToViewportCoords(
{ sceneX: centerElementX, sceneY: centerElementY },
this.state,
this.canvas,
);
textX = centerElementXInViewport;
textY = centerElementYInViewport;
// x and y will change after calling newTextElement function
element.x = centerElementX;
element.y = centerElementY;
} else if (!event.altKey) {
const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
x,
y,
);
if (snappedToCenterPosition) {
element.x = snappedToCenterPosition.elementCenterX;
element.y = snappedToCenterPosition.elementCenterY;
textX = snappedToCenterPosition.wysiwygX;
textY = snappedToCenterPosition.wysiwygY;
}
}
const resetSelection = () => {
this.setState({
draggingElement: null,
editingElement: null,
});
};
textWysiwyg({
initText: element.text,
x: textX,
y: textY,
strokeColor: element.strokeColor,
font: element.font,
opacity: this.state.currentItemOpacity,
zoom: this.state.zoom,
onSubmit: text => {
if (text) {
elements = [
...elements,
{
// we need to recreate the element to update dimensions &
// position
...newTextElement(element, text, element.font),
},
];
}
this.setState(prevState => ({
selectedElementIds: {
...prevState.selectedElementIds,
[element.id]: true,
},
}));
history.resumeRecording();
resetSelection();
},
onCancel: () => {
resetSelection();
},
});
};
private handleCanvasPointerMove = (
event: React.PointerEvent<HTMLCanvasElement>,
) => {
gesture.pointers = gesture.pointers.map(pointer =>
pointer.id === event.pointerId
? {
id: event.pointerId,
x: event.clientX,
y: event.clientY,
}
: pointer,
);
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 || isDraggingScrollBar) {
return;
}
const {
isOverHorizontalScrollBar,
isOverVerticalScrollBar,
} = isOverScrollBars(currentScrollBars, event.clientX, event.clientY);
const isOverScrollBar =
isOverVerticalScrollBar || isOverHorizontalScrollBar;
if (!this.state.draggingElement && !this.state.multiElement) {
if (isOverScrollBar) {
resetCursor();
} else {
setCursorForShape(this.state.elementType);
}
}
const { x, y } = viewportCoordsToSceneCoords(
event,
this.state,
this.canvas,
);
if (this.state.multiElement) {
const { multiElement } = this.state;
const originX = multiElement.x;
const originY = multiElement.y;
const points = multiElement.points;
const pnt = points[points.length - 1];
pnt[0] = x - originX;
pnt[1] = y - originY;
invalidateShapeForElement(multiElement);
this.setState({});
return;
}
const hasDeselectedButton = Boolean(event.buttons);
if (hasDeselectedButton || this.state.elementType !== "selection") {
return;
}
const selectedElements = getSelectedElements(elements, this.state);
if (selectedElements.length === 1 && !isOverScrollBar) {
const resizeElement = getElementWithResizeHandler(
elements,
this.state,
{ x, y },
this.state.zoom,
event.pointerType,
);
if (resizeElement && resizeElement.resizeHandle) {
document.documentElement.style.cursor = getCursorForResizingElement(
resizeElement,
);
return;
}
}
const hitElement = getElementAtPosition(
elements,
this.state,
x,
y,
this.state.zoom,
);
document.documentElement.style.cursor =
hitElement && !isOverScrollBar ? "move" : "";
};
private handleCanvasPointerDown = (
event: React.PointerEvent<HTMLCanvasElement>,
) => {
if (lastPointerUp !== null) { if (lastPointerUp !== null) {
// Unfortunately, sometimes we don't get a pointerup after a pointerdown, // 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
@ -664,11 +912,7 @@ export class App extends React.Component<any, AppState> {
const { const {
isOverHorizontalScrollBar, isOverHorizontalScrollBar,
isOverVerticalScrollBar, isOverVerticalScrollBar,
} = isOverScrollBars( } = isOverScrollBars(currentScrollBars, event.clientX, event.clientY);
currentScrollBars,
event.clientX,
event.clientY,
);
const { x, y } = viewportCoordsToSceneCoords( const { x, y } = viewportCoordsToSceneCoords(
event, event,
@ -695,9 +939,7 @@ export class App extends React.Component<any, AppState> {
const x = event.clientX; const x = event.clientX;
const dx = x - lastX; const dx = x - lastX;
this.setState({ this.setState({
scrollX: normalizeScroll( scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
this.state.scrollX - dx / this.state.zoom,
),
}); });
lastX = x; lastX = x;
return; return;
@ -707,9 +949,7 @@ export class App extends React.Component<any, AppState> {
const y = event.clientY; const y = event.clientY;
const dy = y - lastY; const dy = y - lastY;
this.setState({ this.setState({
scrollY: normalizeScroll( scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
this.state.scrollY - dy / this.state.zoom,
),
}); });
lastY = y; lastY = y;
} }
@ -746,11 +986,7 @@ export class App extends React.Component<any, AppState> {
); );
if (isTextElement(element)) { if (isTextElement(element)) {
element = newTextElement( element = newTextElement(element, "", this.state.currentItemFont);
element,
"",
this.state.currentItemFont,
);
} }
type ResizeTestType = ReturnType<typeof resizeTest>; type ResizeTestType = ReturnType<typeof resizeTest>;
@ -768,15 +1004,10 @@ export class App extends React.Component<any, AppState> {
event.pointerType, event.pointerType,
); );
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(elements, this.state);
elements,
this.state,
);
if (selectedElements.length === 1 && resizeElement) { if (selectedElements.length === 1 && resizeElement) {
this.setState({ this.setState({
resizingElement: resizeElement resizingElement: resizeElement ? resizeElement.element : null,
? resizeElement.element
: null,
}); });
resizeHandle = resizeElement.resizeHandle; resizeHandle = resizeElement.resizeHandle;
@ -794,9 +1025,7 @@ export class App extends React.Component<any, AppState> {
); );
// clear selection if shift is not clicked // clear selection if shift is not clicked
if ( if (
!( !(hitElement && this.state.selectedElementIds[hitElement.id]) &&
hitElement && this.state.selectedElementIds[hitElement.id]
) &&
!event.shiftKey !event.shiftKey
) { ) {
this.setState({ selectedElementIds: {} }); this.setState({ selectedElementIds: {} });
@ -886,11 +1115,7 @@ export class App extends React.Component<any, AppState> {
elements = [ elements = [
...elements, ...elements,
{ {
...newTextElement( ...newTextElement(element, text, this.state.currentItemFont),
element,
text,
this.state.currentItemFont,
),
}, },
]; ];
} }
@ -1039,9 +1264,7 @@ export class App extends React.Component<any, AppState> {
const x = event.clientX; const x = event.clientX;
const dx = x - lastX; const dx = x - lastX;
this.setState({ this.setState({
scrollX: normalizeScroll( scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
this.state.scrollX - dx / this.state.zoom,
),
}); });
lastX = x; lastX = x;
return; return;
@ -1051,9 +1274,7 @@ export class App extends React.Component<any, AppState> {
const y = event.clientY; const y = event.clientY;
const dy = y - lastY; const dy = y - lastY;
this.setState({ this.setState({
scrollY: normalizeScroll( scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
this.state.scrollY - dy / this.state.zoom,
),
}); });
lastY = y; lastY = y;
return; return;
@ -1081,10 +1302,7 @@ export class App extends React.Component<any, AppState> {
if (isResizingElements && this.state.resizingElement) { if (isResizingElements && this.state.resizingElement) {
this.setState({ isResizing: true }); this.setState({ isResizing: true });
const el = this.state.resizingElement; const el = this.state.resizingElement;
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(elements, this.state);
elements,
this.state,
);
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
const { x, y } = viewportCoordsToSceneCoords( const { x, y } = viewportCoordsToSceneCoords(
event, event,
@ -1094,8 +1312,7 @@ export class App extends React.Component<any, AppState> {
const deltaX = x - lastX; const deltaX = x - lastX;
const deltaY = y - lastY; const deltaY = y - lastY;
const element = selectedElements[0]; const element = selectedElements[0];
const isLinear = const isLinear = element.type === "line" || element.type === "arrow";
element.type === "line" || element.type === "arrow";
switch (resizeHandle) { switch (resizeHandle) {
case "nw": case "nw":
if (isLinear && element.points.length === 2) { if (isLinear && element.points.length === 2) {
@ -1225,9 +1442,7 @@ export class App extends React.Component<any, AppState> {
if (element.points.length > 0) { if (element.points.length > 0) {
const len = element.points.length; const len = element.points.length;
const points = [...element.points].sort( const points = [...element.points].sort((a, b) => a[1] - b[1]);
(a, b) => a[1] - b[1],
);
for (let i = 1; i < points.length; ++i) { for (let i = 1; i < points.length; ++i) {
const pnt = points[i]; const pnt = points[i];
@ -1242,9 +1457,7 @@ export class App extends React.Component<any, AppState> {
if (element.points.length > 0) { if (element.points.length > 0) {
const len = element.points.length; const len = element.points.length;
const points = [...element.points].sort( const points = [...element.points].sort((a, b) => a[0] - b[0]);
(a, b) => a[0] - b[0],
);
for (let i = 0; i < points.length; ++i) { for (let i = 0; i < points.length; ++i) {
const pnt = points[i]; const pnt = points[i];
@ -1257,9 +1470,7 @@ export class App extends React.Component<any, AppState> {
element.height += deltaY; element.height += deltaY;
if (element.points.length > 0) { if (element.points.length > 0) {
const len = element.points.length; const len = element.points.length;
const points = [...element.points].sort( const points = [...element.points].sort((a, b) => a[1] - b[1]);
(a, b) => a[1] - b[1],
);
for (let i = 1; i < points.length; ++i) { for (let i = 1; i < points.length; ++i) {
const pnt = points[i]; const pnt = points[i];
@ -1272,9 +1483,7 @@ export class App extends React.Component<any, AppState> {
element.width += deltaX; element.width += deltaX;
if (element.points.length > 0) { if (element.points.length > 0) {
const len = element.points.length; const len = element.points.length;
const points = [...element.points].sort( const points = [...element.points].sort((a, b) => a[0] - b[0]);
(a, b) => a[0] - b[0],
);
for (let i = 1; i < points.length; ++i) { for (let i = 1; i < points.length; ++i) {
const pnt = points[i]; const pnt = points[i];
@ -1286,16 +1495,14 @@ export class App extends React.Component<any, AppState> {
} }
if (resizeHandle) { if (resizeHandle) {
resizeHandle = normalizeResizeHandle( resizeHandle = normalizeResizeHandle(element, resizeHandle);
element,
resizeHandle,
);
} }
normalizeDimensions(element); normalizeDimensions(element);
document.documentElement.style.cursor = getCursorForResizingElement( document.documentElement.style.cursor = getCursorForResizingElement({
{ element, resizeHandle }, element,
); resizeHandle,
});
el.x = element.x; el.x = element.x;
el.y = element.y; el.y = element.y;
invalidateShapeForElement(el); invalidateShapeForElement(el);
@ -1307,17 +1514,11 @@ export class App extends React.Component<any, AppState> {
} }
} }
if ( if (hitElement && this.state.selectedElementIds[hitElement.id]) {
hitElement &&
this.state.selectedElementIds[hitElement.id]
) {
// Marking that click was used for dragging to check // Marking that click was used for dragging to check
// if elements should be deselected on pointerup // if elements should be deselected on pointerup
draggingOccurred = true; draggingOccurred = true;
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(elements, this.state);
elements,
this.state,
);
if (selectedElements.length > 0) { if (selectedElements.length > 0) {
const { x, y } = viewportCoordsToSceneCoords( const { x, y } = viewportCoordsToSceneCoords(
event, event,
@ -1353,8 +1554,7 @@ export class App extends React.Component<any, AppState> {
let height = distance(originY, y); let height = distance(originY, y);
const isLinear = const isLinear =
this.state.elementType === "line" || this.state.elementType === "line" || this.state.elementType === "arrow";
this.state.elementType === "arrow";
if (isLinear) { if (isLinear) {
draggingOccurred = true; draggingOccurred = true;
@ -1400,10 +1600,7 @@ export class App extends React.Component<any, AppState> {
invalidateShapeForElement(draggingElement); invalidateShapeForElement(draggingElement);
if (this.state.elementType === "selection") { if (this.state.elementType === "selection") {
if ( if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
!event.shiftKey &&
isSomeElementSelected(elements, this.state)
) {
this.setState({ selectedElementIds: {} }); this.setState({ selectedElementIds: {} });
} }
const elementsWithinSelection = getElementsWithinSelection( const elementsWithinSelection = getElementsWithinSelection(
@ -1414,10 +1611,7 @@ export class App extends React.Component<any, AppState> {
selectedElementIds: { selectedElementIds: {
...prevState.selectedElementIds, ...prevState.selectedElementIds,
...Object.fromEntries( ...Object.fromEntries(
elementsWithinSelection.map(element => [ elementsWithinSelection.map(element => [element.id, true]),
element.id,
true,
]),
), ),
}, },
})); }));
@ -1508,13 +1702,8 @@ export class App extends React.Component<any, AppState> {
this.setState({}); this.setState({});
} }
if ( if (resizingElement && isInvisiblySmallElement(resizingElement)) {
resizingElement && elements = elements.filter(el => el.id !== resizingElement.id);
isInvisiblySmallElement(resizingElement)
) {
elements = elements.filter(
el => el.id !== resizingElement.id,
);
} }
// If click occurred on already selected element // If click occurred on already selected element
@ -1525,11 +1714,7 @@ export class App extends React.Component<any, AppState> {
// 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 pointerdown phase) we need to keep // was added to selection (on pointerdown phase) we need to keep
// selection unchanged // selection unchanged
if ( if (hitElement && !draggingOccurred && !elementIsAddedToSelection) {
hitElement &&
!draggingOccurred &&
!elementIsAddedToSelection
) {
if (event.shiftKey) { if (event.shiftKey) {
this.setState(prevState => ({ this.setState(prevState => ({
selectedElementIds: { selectedElementIds: {
@ -1583,260 +1768,8 @@ export class App extends React.Component<any, AppState> {
window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointermove", onPointerMove);
window.addEventListener("pointerup", onPointerUp); window.addEventListener("pointerup", onPointerUp);
}}
onDoubleClick={event => {
resetCursor();
const { x, y } = viewportCoordsToSceneCoords(
event,
this.state,
this.canvas,
);
const elementAtPosition = getElementAtPosition(
elements,
this.state,
x,
y,
this.state.zoom,
);
const element =
elementAtPosition && isTextElement(elementAtPosition)
? elementAtPosition
: newTextElement(
newElement(
"text",
x,
y,
this.state.currentItemStrokeColor,
this.state.currentItemBackgroundColor,
this.state.currentItemFillStyle,
this.state.currentItemStrokeWidth,
this.state.currentItemRoughness,
this.state.currentItemOpacity,
),
"", // default text
this.state.currentItemFont, // default font
);
this.setState({ editingElement: element });
let textX = event.clientX;
let textY = event.clientY;
if (elementAtPosition && isTextElement(elementAtPosition)) {
elements = elements.filter(
element => element.id !== elementAtPosition.id,
);
this.setState({});
const centerElementX =
elementAtPosition.x + elementAtPosition.width / 2;
const centerElementY =
elementAtPosition.y + elementAtPosition.height / 2;
const {
x: centerElementXInViewport,
y: centerElementYInViewport,
} = sceneCoordsToViewportCoords(
{ sceneX: centerElementX, sceneY: centerElementY },
this.state,
this.canvas,
);
textX = centerElementXInViewport;
textY = centerElementYInViewport;
// x and y will change after calling newTextElement function
element.x = centerElementX;
element.y = centerElementY;
} else if (!event.altKey) {
const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
x,
y,
);
if (snappedToCenterPosition) {
element.x = snappedToCenterPosition.elementCenterX;
element.y = snappedToCenterPosition.elementCenterY;
textX = snappedToCenterPosition.wysiwygX;
textY = snappedToCenterPosition.wysiwygY;
}
}
const resetSelection = () => {
this.setState({
draggingElement: null,
editingElement: null,
});
}; };
textWysiwyg({
initText: element.text,
x: textX,
y: textY,
strokeColor: element.strokeColor,
font: element.font,
opacity: this.state.currentItemOpacity,
zoom: this.state.zoom,
onSubmit: text => {
if (text) {
elements = [
...elements,
{
// we need to recreate the element to update dimensions &
// position
...newTextElement(element, text, element.font),
},
];
}
this.setState(prevState => ({
selectedElementIds: {
...prevState.selectedElementIds,
[element.id]: true,
},
}));
history.resumeRecording();
resetSelection();
},
onCancel: () => {
resetSelection();
},
});
}}
onPointerMove={event => {
gesture.pointers = gesture.pointers.map(pointer =>
pointer.id === event.pointerId
? {
id: event.pointerId,
x: event.clientX,
y: event.clientY,
}
: pointer,
);
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 || isDraggingScrollBar) {
return;
}
const {
isOverHorizontalScrollBar,
isOverVerticalScrollBar,
} = isOverScrollBars(
currentScrollBars,
event.clientX,
event.clientY,
);
const isOverScrollBar =
isOverVerticalScrollBar || isOverHorizontalScrollBar;
if (!this.state.draggingElement && !this.state.multiElement) {
if (isOverScrollBar) {
resetCursor();
} else {
setCursorForShape(this.state.elementType);
}
}
const { x, y } = viewportCoordsToSceneCoords(
event,
this.state,
this.canvas,
);
if (this.state.multiElement) {
const { multiElement } = this.state;
const originX = multiElement.x;
const originY = multiElement.y;
const points = multiElement.points;
const pnt = points[points.length - 1];
pnt[0] = x - originX;
pnt[1] = y - originY;
invalidateShapeForElement(multiElement);
this.setState({});
return;
}
const hasDeselectedButton = Boolean(event.buttons);
if (
hasDeselectedButton ||
this.state.elementType !== "selection"
) {
return;
}
const selectedElements = getSelectedElements(
elements,
this.state,
);
if (selectedElements.length === 1 && !isOverScrollBar) {
const resizeElement = getElementWithResizeHandler(
elements,
this.state,
{ x, y },
this.state.zoom,
event.pointerType,
);
if (resizeElement && resizeElement.resizeHandle) {
document.documentElement.style.cursor = getCursorForResizingElement(
resizeElement,
);
return;
}
}
const hitElement = getElementAtPosition(
elements,
this.state,
x,
y,
this.state.zoom,
);
document.documentElement.style.cursor =
hitElement && !isOverScrollBar ? "move" : "";
}}
onPointerUp={this.removePointer}
onPointerCancel={this.removePointer}
onDrop={event => {
const file = event.dataTransfer.files[0];
if (
file?.type === "application/json" ||
file?.name.endsWith(".excalidraw")
) {
loadFromBlob(file)
.then(({ elements, appState }) =>
this.syncActionResult({ elements, appState }),
)
.catch(error => console.error(error));
}
}}
>
{t("labels.drawingCanvas")}
</canvas>
</main>
</div>
);
}
private handleWheel = (event: WheelEvent) => { private handleWheel = (event: WheelEvent) => {
event.preventDefault(); event.preventDefault();
const { deltaX, deltaY } = event; const { deltaX, deltaY } = event;