Improve scrollbar-mouse interaction (#667)
* fix scrollbar detection on high devicePixelRatio devices * don't create a new element on pointerdown over a scrollbar * Return scrollbars from renderScene and use it in isOverScrollBars * remove unneeded setState * show default cursor when hovering or dragging a scrollbar * disable scrollbars when in multielement mode Co-authored-by: David Luzar <luzar.david@gmail.com>
This commit is contained in:
parent
8e0206cc1e
commit
e920c078b9
106
src/index.tsx
106
src/index.tsx
@ -112,6 +112,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";
|
import { getCenter, getDistance } from "./gesture";
|
||||||
|
import { ScrollBars } from "./scene/types";
|
||||||
import { createUndoAction, createRedoAction } from "./actions/actionHistory";
|
import { createUndoAction, createRedoAction } from "./actions/actionHistory";
|
||||||
|
|
||||||
let { elements } = createScene();
|
let { elements } = createScene();
|
||||||
@ -214,6 +215,8 @@ 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 isDraggingScrollBar: boolean = false;
|
||||||
|
let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@ -1159,12 +1162,9 @@ export class App extends React.Component<any, AppState> {
|
|||||||
isOverHorizontalScrollBar,
|
isOverHorizontalScrollBar,
|
||||||
isOverVerticalScrollBar,
|
isOverVerticalScrollBar,
|
||||||
} = isOverScrollBars(
|
} = isOverScrollBars(
|
||||||
elements,
|
currentScrollBars,
|
||||||
event.clientX / window.devicePixelRatio,
|
event.clientX,
|
||||||
event.clientY / window.devicePixelRatio,
|
event.clientY,
|
||||||
canvasWidth / window.devicePixelRatio,
|
|
||||||
canvasHeight / window.devicePixelRatio,
|
|
||||||
this.state,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { x, y } = viewportCoordsToSceneCoords(
|
const { x, y } = viewportCoordsToSceneCoords(
|
||||||
@ -1172,6 +1172,60 @@ export class App extends React.Component<any, AppState> {
|
|||||||
this.state,
|
this.state,
|
||||||
this.canvas,
|
this.canvas,
|
||||||
);
|
);
|
||||||
|
let lastX = x;
|
||||||
|
let lastY = y;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(isOverHorizontalScrollBar || isOverVerticalScrollBar) &&
|
||||||
|
!this.state.multiElement
|
||||||
|
) {
|
||||||
|
isDraggingScrollBar = true;
|
||||||
|
lastX = event.clientX;
|
||||||
|
lastY = event.clientY;
|
||||||
|
const onPointerMove = (event: PointerEvent) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOverHorizontalScrollBar) {
|
||||||
|
const x = event.clientX;
|
||||||
|
const dx = x - lastX;
|
||||||
|
this.setState({
|
||||||
|
scrollX: normalizeScroll(
|
||||||
|
this.state.scrollX - dx / this.state.zoom,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
lastX = x;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOverVerticalScrollBar) {
|
||||||
|
const y = event.clientY;
|
||||||
|
const dy = y - lastY;
|
||||||
|
this.setState({
|
||||||
|
scrollY: normalizeScroll(
|
||||||
|
this.state.scrollY - dy / this.state.zoom,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
lastY = y;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = () => {
|
||||||
|
isDraggingScrollBar = false;
|
||||||
|
setCursorForShape(this.state.elementType);
|
||||||
|
lastPointerUp = null;
|
||||||
|
window.removeEventListener("pointermove", onPointerMove);
|
||||||
|
window.removeEventListener("pointerup", onPointerUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
lastPointerUp = onPointerUp;
|
||||||
|
|
||||||
|
window.addEventListener("pointermove", onPointerMove);
|
||||||
|
window.addEventListener("pointerup", onPointerUp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const originX = x;
|
const originX = x;
|
||||||
const originY = y;
|
const originY = y;
|
||||||
@ -1373,14 +1427,6 @@ export class App extends React.Component<any, AppState> {
|
|||||||
this.setState({ multiElement: null, draggingElement: element });
|
this.setState({ multiElement: null, draggingElement: element });
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastX = x;
|
|
||||||
let lastY = y;
|
|
||||||
|
|
||||||
if (isOverHorizontalScrollBar || isOverVerticalScrollBar) {
|
|
||||||
lastX = event.clientX;
|
|
||||||
lastY = event.clientY;
|
|
||||||
}
|
|
||||||
|
|
||||||
let resizeArrowFn:
|
let resizeArrowFn:
|
||||||
| ((
|
| ((
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
@ -2115,10 +2161,27 @@ export class App extends React.Component<any, AppState> {
|
|||||||
gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null;
|
gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHoldingSpace || isPanning) {
|
if (isHoldingSpace || isPanning || isDraggingScrollBar) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hasDeselectedButton = Boolean(event.buttons);
|
|
||||||
|
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(
|
const { x, y } = viewportCoordsToSceneCoords(
|
||||||
event,
|
event,
|
||||||
@ -2138,6 +2201,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasDeselectedButton = Boolean(event.buttons);
|
||||||
if (
|
if (
|
||||||
hasDeselectedButton ||
|
hasDeselectedButton ||
|
||||||
this.state.elementType !== "selection"
|
this.state.elementType !== "selection"
|
||||||
@ -2146,7 +2210,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(elements);
|
const selectedElements = getSelectedElements(elements);
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1 && !isOverScrollBar) {
|
||||||
const resizeElement = getElementWithResizeHandler(
|
const resizeElement = getElementWithResizeHandler(
|
||||||
elements,
|
elements,
|
||||||
{ x, y },
|
{ x, y },
|
||||||
@ -2166,7 +2230,8 @@ export class App extends React.Component<any, AppState> {
|
|||||||
y,
|
y,
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
);
|
);
|
||||||
document.documentElement.style.cursor = hitElement ? "move" : "";
|
document.documentElement.style.cursor =
|
||||||
|
hitElement && !isOverScrollBar ? "move" : "";
|
||||||
}}
|
}}
|
||||||
onPointerUp={this.removePointer}
|
onPointerUp={this.removePointer}
|
||||||
onPointerLeave={this.removePointer}
|
onPointerLeave={this.removePointer}
|
||||||
@ -2279,7 +2344,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
const atLeastOneVisibleElement = renderScene(
|
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
||||||
elements,
|
elements,
|
||||||
this.state.selectionElement,
|
this.state.selectionElement,
|
||||||
this.rc!,
|
this.rc!,
|
||||||
@ -2294,6 +2359,9 @@ export class App extends React.Component<any, AppState> {
|
|||||||
renderOptimizations: true,
|
renderOptimizations: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
if (scrollBars) {
|
||||||
|
currentScrollBars = scrollBars;
|
||||||
|
}
|
||||||
const scrolledOutside = !atLeastOneVisibleElement && elements.length > 0;
|
const scrolledOutside = !atLeastOneVisibleElement && elements.length > 0;
|
||||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||||
this.setState({ scrolledOutside: scrolledOutside });
|
this.setState({ scrolledOutside: scrolledOutside });
|
||||||
|
@ -36,9 +36,9 @@ export function renderScene(
|
|||||||
renderSelection?: boolean;
|
renderSelection?: boolean;
|
||||||
renderOptimizations?: boolean;
|
renderOptimizations?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
): boolean {
|
) {
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
return false;
|
return { atLeastOneVisibleElement: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext("2d")!;
|
||||||
@ -196,9 +196,10 @@ export function renderScene(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
context.restore();
|
context.restore();
|
||||||
|
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
|
||||||
}
|
}
|
||||||
|
|
||||||
return visibleElements.length > 0;
|
return { atLeastOneVisibleElement: visibleElements.length > 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVisibleElement(
|
function isVisibleElement(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { getCommonBounds } from "../element";
|
import { getCommonBounds } from "../element";
|
||||||
import { FlooredNumber } from "../types";
|
import { FlooredNumber } from "../types";
|
||||||
|
import { ScrollBars } from "./types";
|
||||||
|
|
||||||
const SCROLLBAR_MARGIN = 4;
|
const SCROLLBAR_MARGIN = 4;
|
||||||
export const SCROLLBAR_WIDTH = 6;
|
export const SCROLLBAR_WIDTH = 6;
|
||||||
@ -19,7 +20,7 @@ export function getScrollBars(
|
|||||||
scrollY: FlooredNumber;
|
scrollY: FlooredNumber;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
},
|
},
|
||||||
) {
|
): ScrollBars {
|
||||||
// This is the bounding box of all the elements
|
// This is the bounding box of all the elements
|
||||||
const [
|
const [
|
||||||
elementsMinX,
|
elementsMinX,
|
||||||
@ -83,28 +84,7 @@ export function getScrollBars(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isOverScrollBars(
|
export function isOverScrollBars(scrollBars: ScrollBars, x: number, y: number) {
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
viewportWidth: number,
|
|
||||||
viewportHeight: number,
|
|
||||||
{
|
|
||||||
scrollX,
|
|
||||||
scrollY,
|
|
||||||
zoom,
|
|
||||||
}: {
|
|
||||||
scrollX: FlooredNumber;
|
|
||||||
scrollY: FlooredNumber;
|
|
||||||
zoom: number;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const scrollBars = getScrollBars(elements, viewportWidth, viewportHeight, {
|
|
||||||
scrollX,
|
|
||||||
scrollY,
|
|
||||||
zoom,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [
|
const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [
|
||||||
scrollBars.horizontal,
|
scrollBars.horizontal,
|
||||||
scrollBars.vertical,
|
scrollBars.vertical,
|
||||||
|
@ -19,3 +19,18 @@ export interface Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ExportType = "png" | "clipboard" | "backend" | "svg";
|
export type ExportType = "png" | "clipboard" | "backend" | "svg";
|
||||||
|
|
||||||
|
export type ScrollBars = {
|
||||||
|
horizontal: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
} | null;
|
||||||
|
vertical: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user