mirror of
https://github.com/excalidraw/excalidraw.git
synced 2024-11-10 11:35:52 +01:00
feat: enable panning/zoom while in wysiwyg (#8437)
This commit is contained in:
parent
ea7c702cfc
commit
00af35c692
@ -49,6 +49,7 @@ import {
|
|||||||
} from "./icons";
|
} from "./icons";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { useTunnels } from "../context/tunnels";
|
import { useTunnels } from "../context/tunnels";
|
||||||
|
import { CLASSES } from "../constants";
|
||||||
|
|
||||||
export const canChangeStrokeColor = (
|
export const canChangeStrokeColor = (
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
@ -426,7 +427,7 @@ export const ZoomActions = ({
|
|||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
zoom: Zoom;
|
zoom: Zoom;
|
||||||
}) => (
|
}) => (
|
||||||
<Stack.Col gap={1} className="zoom-actions">
|
<Stack.Col gap={1} className={CLASSES.ZOOM_ACTIONS}>
|
||||||
<Stack.Row align="center">
|
<Stack.Row align="center">
|
||||||
{renderAction("zoomOut")}
|
{renderAction("zoomOut")}
|
||||||
{renderAction("resetZoom")}
|
{renderAction("resetZoom")}
|
||||||
|
@ -2578,6 +2578,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
addEventListener(window, EVENT.RESIZE, this.onResize, false),
|
addEventListener(window, EVENT.RESIZE, this.onResize, false),
|
||||||
addEventListener(window, EVENT.UNLOAD, this.onUnload, false),
|
addEventListener(window, EVENT.UNLOAD, this.onUnload, false),
|
||||||
addEventListener(window, EVENT.BLUR, this.onBlur, false),
|
addEventListener(window, EVENT.BLUR, this.onBlur, false),
|
||||||
|
addEventListener(
|
||||||
|
this.excalidrawContainerRef.current,
|
||||||
|
EVENT.WHEEL,
|
||||||
|
this.handleWheel,
|
||||||
|
),
|
||||||
addEventListener(
|
addEventListener(
|
||||||
this.excalidrawContainerRef.current,
|
this.excalidrawContainerRef.current,
|
||||||
EVENT.DRAG_OVER,
|
EVENT.DRAG_OVER,
|
||||||
@ -6384,8 +6389,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Returns whether the event is a panning
|
// Returns whether the event is a panning
|
||||||
private handleCanvasPanUsingWheelOrSpaceDrag = (
|
public handleCanvasPanUsingWheelOrSpaceDrag = (
|
||||||
event: React.PointerEvent<HTMLElement>,
|
event: React.PointerEvent<HTMLElement> | MouseEvent,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
@ -6394,13 +6399,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
||||||
isHandToolActive(this.state) ||
|
isHandToolActive(this.state) ||
|
||||||
this.state.viewModeEnabled)
|
this.state.viewModeEnabled)
|
||||||
) ||
|
)
|
||||||
this.state.editingTextElement
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
isPanning = true;
|
isPanning = true;
|
||||||
event.preventDefault();
|
|
||||||
|
if (!this.state.editingTextElement) {
|
||||||
|
// preventing defualt while text editing messes with cursor/focus
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
let nextPastePrevented = false;
|
let nextPastePrevented = false;
|
||||||
const isLinux =
|
const isLinux =
|
||||||
@ -9472,7 +9480,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// NOTE wheel, touchstart, touchend events must be registered outside
|
// NOTE wheel, touchstart, touchend events must be registered outside
|
||||||
// of react because react binds them them passively (so we can't prevent
|
// of react because react binds them them passively (so we can't prevent
|
||||||
// default on them)
|
// default on them)
|
||||||
this.interactiveCanvas.addEventListener(EVENT.WHEEL, this.handleWheel);
|
|
||||||
this.interactiveCanvas.addEventListener(
|
this.interactiveCanvas.addEventListener(
|
||||||
EVENT.TOUCH_START,
|
EVENT.TOUCH_START,
|
||||||
this.onTouchStart,
|
this.onTouchStart,
|
||||||
@ -9480,10 +9487,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.interactiveCanvas.addEventListener(EVENT.TOUCH_END, this.onTouchEnd);
|
this.interactiveCanvas.addEventListener(EVENT.TOUCH_END, this.onTouchEnd);
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
} else {
|
} else {
|
||||||
this.interactiveCanvas?.removeEventListener(
|
|
||||||
EVENT.WHEEL,
|
|
||||||
this.handleWheel,
|
|
||||||
);
|
|
||||||
this.interactiveCanvas?.removeEventListener(
|
this.interactiveCanvas?.removeEventListener(
|
||||||
EVENT.TOUCH_START,
|
EVENT.TOUCH_START,
|
||||||
this.onTouchStart,
|
this.onTouchStart,
|
||||||
@ -10078,7 +10081,19 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
(
|
(
|
||||||
event: WheelEvent | React.WheelEvent<HTMLDivElement | HTMLCanvasElement>,
|
event: WheelEvent | React.WheelEvent<HTMLDivElement | HTMLCanvasElement>,
|
||||||
) => {
|
) => {
|
||||||
|
// if not scrolling on canvas/wysiwyg, ignore
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
event.target instanceof HTMLCanvasElement ||
|
||||||
|
event.target instanceof HTMLTextAreaElement ||
|
||||||
|
event.target instanceof HTMLIFrameElement
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (isPanning) {
|
if (isPanning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -112,6 +112,7 @@ export const ENV = {
|
|||||||
|
|
||||||
export const CLASSES = {
|
export const CLASSES = {
|
||||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||||
|
ZOOM_ACTIONS: "zoom-actions",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { CLASSES, isSafari } from "../constants";
|
import { CLASSES, isSafari, POINTER_BUTTON } from "../constants";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@ -38,7 +38,11 @@ import {
|
|||||||
actionDecreaseFontSize,
|
actionDecreaseFontSize,
|
||||||
actionIncreaseFontSize,
|
actionIncreaseFontSize,
|
||||||
} from "../actions/actionProperties";
|
} from "../actions/actionProperties";
|
||||||
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
import {
|
||||||
|
actionResetZoom,
|
||||||
|
actionZoomIn,
|
||||||
|
actionZoomOut,
|
||||||
|
} from "../actions/actionCanvas";
|
||||||
import type App from "../components/App";
|
import type App from "../components/App";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
@ -379,6 +383,10 @@ export const textWysiwyg = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
app.actionManager.executeAction(actionZoomOut);
|
app.actionManager.executeAction(actionZoomOut);
|
||||||
updateWysiwygStyle();
|
updateWysiwygStyle();
|
||||||
|
} else if (!event.shiftKey && actionResetZoom.keyTest(event)) {
|
||||||
|
event.preventDefault();
|
||||||
|
app.actionManager.executeAction(actionResetZoom);
|
||||||
|
updateWysiwygStyle();
|
||||||
} else if (actionDecreaseFontSize.keyTest(event)) {
|
} else if (actionDecreaseFontSize.keyTest(event)) {
|
||||||
app.actionManager.executeAction(actionDecreaseFontSize);
|
app.actionManager.executeAction(actionDecreaseFontSize);
|
||||||
} else if (actionIncreaseFontSize.keyTest(event)) {
|
} else if (actionIncreaseFontSize.keyTest(event)) {
|
||||||
@ -593,6 +601,7 @@ export const textWysiwyg = ({
|
|||||||
window.removeEventListener("blur", handleSubmit);
|
window.removeEventListener("blur", handleSubmit);
|
||||||
window.removeEventListener("beforeunload", handleSubmit);
|
window.removeEventListener("beforeunload", handleSubmit);
|
||||||
unbindUpdate();
|
unbindUpdate();
|
||||||
|
unbindOnScroll();
|
||||||
|
|
||||||
editable.remove();
|
editable.remove();
|
||||||
};
|
};
|
||||||
@ -619,10 +628,29 @@ export const textWysiwyg = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const temporarilyDisableSubmit = () => {
|
||||||
|
editable.onblur = null;
|
||||||
|
window.addEventListener("pointerup", bindBlurEvent);
|
||||||
|
// handle edge-case where pointerup doesn't fire e.g. due to user
|
||||||
|
// alt-tabbing away
|
||||||
|
window.addEventListener("blur", handleSubmit);
|
||||||
|
};
|
||||||
|
|
||||||
// prevent blur when changing properties from the menu
|
// prevent blur when changing properties from the menu
|
||||||
const onPointerDown = (event: MouseEvent) => {
|
const onPointerDown = (event: MouseEvent) => {
|
||||||
const target = event?.target;
|
const target = event?.target;
|
||||||
|
|
||||||
|
// panning canvas
|
||||||
|
if (event.button === POINTER_BUTTON.WHEEL) {
|
||||||
|
// trying to pan by clicking inside text area itself -> handle here
|
||||||
|
if (target instanceof HTMLTextAreaElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
app.handleCanvasPanUsingWheelOrSpaceDrag(event);
|
||||||
|
}
|
||||||
|
temporarilyDisableSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isPropertiesTrigger =
|
const isPropertiesTrigger =
|
||||||
target instanceof HTMLElement &&
|
target instanceof HTMLElement &&
|
||||||
target.classList.contains("properties-trigger");
|
target.classList.contains("properties-trigger");
|
||||||
@ -630,17 +658,14 @@ export const textWysiwyg = ({
|
|||||||
if (
|
if (
|
||||||
((event.target instanceof HTMLElement ||
|
((event.target instanceof HTMLElement ||
|
||||||
event.target instanceof SVGElement) &&
|
event.target instanceof SVGElement) &&
|
||||||
event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
|
event.target.closest(
|
||||||
|
`.${CLASSES.SHAPE_ACTIONS_MENU}, .${CLASSES.ZOOM_ACTIONS}`,
|
||||||
|
) &&
|
||||||
!isWritableElement(event.target)) ||
|
!isWritableElement(event.target)) ||
|
||||||
isPropertiesTrigger
|
isPropertiesTrigger
|
||||||
) {
|
) {
|
||||||
editable.onblur = null;
|
temporarilyDisableSubmit();
|
||||||
window.addEventListener("pointerup", bindBlurEvent);
|
|
||||||
// handle edge-case where pointerup doesn't fire e.g. due to user
|
|
||||||
// alt-tabbing away
|
|
||||||
window.addEventListener("blur", handleSubmit);
|
|
||||||
} else if (
|
} else if (
|
||||||
event.target instanceof HTMLElement &&
|
|
||||||
event.target instanceof HTMLCanvasElement &&
|
event.target instanceof HTMLCanvasElement &&
|
||||||
// Vitest simply ignores stopPropagation, capture-mode, or rAF
|
// Vitest simply ignores stopPropagation, capture-mode, or rAF
|
||||||
// so without introducing crazier hacks, nothing we can do
|
// so without introducing crazier hacks, nothing we can do
|
||||||
@ -659,7 +684,7 @@ export const textWysiwyg = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// handle updates of textElement properties of editing element
|
// handle updates of textElement properties of editing element
|
||||||
const unbindUpdate = Scene.getScene(element)!.onUpdate(() => {
|
const unbindUpdate = app.scene.onUpdate(() => {
|
||||||
updateWysiwygStyle();
|
updateWysiwygStyle();
|
||||||
const isPopupOpened = !!document.activeElement?.closest(
|
const isPopupOpened = !!document.activeElement?.closest(
|
||||||
".properties-content",
|
".properties-content",
|
||||||
@ -669,6 +694,10 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unbindOnScroll = app.onScrollChangeEmitter.on(() => {
|
||||||
|
updateWysiwygStyle();
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
let isDestroyed = false;
|
let isDestroyed = false;
|
||||||
@ -699,10 +728,6 @@ export const textWysiwyg = ({
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
window.addEventListener("pointerdown", onPointerDown, { capture: true });
|
window.addEventListener("pointerdown", onPointerDown, { capture: true });
|
||||||
});
|
});
|
||||||
window.addEventListener("wheel", stopEvent, {
|
|
||||||
passive: false,
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
window.addEventListener("beforeunload", handleSubmit);
|
window.addEventListener("beforeunload", handleSubmit);
|
||||||
excalidrawContainer
|
excalidrawContainer
|
||||||
?.querySelector(".excalidraw-textEditorContainer")!
|
?.querySelector(".excalidraw-textEditorContainer")!
|
||||||
|
Loading…
Reference in New Issue
Block a user