From 00af35c6926d6788bfd1503aac26602998609a35 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Thu, 29 Aug 2024 00:42:46 +0200 Subject: [PATCH] feat: enable panning/zoom while in wysiwyg (#8437) --- packages/excalidraw/components/Actions.tsx | 3 +- packages/excalidraw/components/App.tsx | 35 ++++++++++---- packages/excalidraw/constants.ts | 1 + packages/excalidraw/element/textWysiwyg.tsx | 53 +++++++++++++++------ 4 files changed, 67 insertions(+), 25 deletions(-) diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index 3673eafad..b818d1a23 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -49,6 +49,7 @@ import { } from "./icons"; import { KEYS } from "../keys"; import { useTunnels } from "../context/tunnels"; +import { CLASSES } from "../constants"; export const canChangeStrokeColor = ( appState: UIAppState, @@ -426,7 +427,7 @@ export const ZoomActions = ({ renderAction: ActionManager["renderAction"]; zoom: Zoom; }) => ( - + {renderAction("zoomOut")} {renderAction("resetZoom")} diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index b54bf0645..ef5d7bc22 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -2578,6 +2578,11 @@ class App extends React.Component { addEventListener(window, EVENT.RESIZE, this.onResize, false), addEventListener(window, EVENT.UNLOAD, this.onUnload, false), addEventListener(window, EVENT.BLUR, this.onBlur, false), + addEventListener( + this.excalidrawContainerRef.current, + EVENT.WHEEL, + this.handleWheel, + ), addEventListener( this.excalidrawContainerRef.current, EVENT.DRAG_OVER, @@ -6384,8 +6389,8 @@ class App extends React.Component { }; // Returns whether the event is a panning - private handleCanvasPanUsingWheelOrSpaceDrag = ( - event: React.PointerEvent, + public handleCanvasPanUsingWheelOrSpaceDrag = ( + event: React.PointerEvent | MouseEvent, ): boolean => { if ( !( @@ -6394,13 +6399,16 @@ class App extends React.Component { (event.button === POINTER_BUTTON.MAIN && isHoldingSpace) || isHandToolActive(this.state) || this.state.viewModeEnabled) - ) || - this.state.editingTextElement + ) ) { return false; } isPanning = true; - event.preventDefault(); + + if (!this.state.editingTextElement) { + // preventing defualt while text editing messes with cursor/focus + event.preventDefault(); + } let nextPastePrevented = false; const isLinux = @@ -9472,7 +9480,6 @@ class App extends React.Component { // NOTE wheel, touchstart, touchend events must be registered outside // of react because react binds them them passively (so we can't prevent // default on them) - this.interactiveCanvas.addEventListener(EVENT.WHEEL, this.handleWheel); this.interactiveCanvas.addEventListener( EVENT.TOUCH_START, this.onTouchStart, @@ -9480,10 +9487,6 @@ class App extends React.Component { this.interactiveCanvas.addEventListener(EVENT.TOUCH_END, this.onTouchEnd); // ----------------------------------------------------------------------- } else { - this.interactiveCanvas?.removeEventListener( - EVENT.WHEEL, - this.handleWheel, - ); this.interactiveCanvas?.removeEventListener( EVENT.TOUCH_START, this.onTouchStart, @@ -10078,7 +10081,19 @@ class App extends React.Component { ( event: WheelEvent | React.WheelEvent, ) => { + // if not scrolling on canvas/wysiwyg, ignore + if ( + !( + event.target instanceof HTMLCanvasElement || + event.target instanceof HTMLTextAreaElement || + event.target instanceof HTMLIFrameElement + ) + ) { + return; + } + event.preventDefault(); + if (isPanning) { return; } diff --git a/packages/excalidraw/constants.ts b/packages/excalidraw/constants.ts index 27bbfb4e0..9601807f6 100644 --- a/packages/excalidraw/constants.ts +++ b/packages/excalidraw/constants.ts @@ -112,6 +112,7 @@ export const ENV = { export const CLASSES = { SHAPE_ACTIONS_MENU: "App-menu__left", + ZOOM_ACTIONS: "zoom-actions", }; /** diff --git a/packages/excalidraw/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx index 5093a2624..2281a0cc3 100644 --- a/packages/excalidraw/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -11,7 +11,7 @@ import { isBoundToContainer, isTextElement, } from "./typeChecks"; -import { CLASSES, isSafari } from "../constants"; +import { CLASSES, isSafari, POINTER_BUTTON } from "../constants"; import type { ExcalidrawElement, ExcalidrawLinearElement, @@ -38,7 +38,11 @@ import { actionDecreaseFontSize, actionIncreaseFontSize, } from "../actions/actionProperties"; -import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas"; +import { + actionResetZoom, + actionZoomIn, + actionZoomOut, +} from "../actions/actionCanvas"; import type App from "../components/App"; import { LinearElementEditor } from "./linearElementEditor"; import { parseClipboard } from "../clipboard"; @@ -379,6 +383,10 @@ export const textWysiwyg = ({ event.preventDefault(); app.actionManager.executeAction(actionZoomOut); updateWysiwygStyle(); + } else if (!event.shiftKey && actionResetZoom.keyTest(event)) { + event.preventDefault(); + app.actionManager.executeAction(actionResetZoom); + updateWysiwygStyle(); } else if (actionDecreaseFontSize.keyTest(event)) { app.actionManager.executeAction(actionDecreaseFontSize); } else if (actionIncreaseFontSize.keyTest(event)) { @@ -593,6 +601,7 @@ export const textWysiwyg = ({ window.removeEventListener("blur", handleSubmit); window.removeEventListener("beforeunload", handleSubmit); unbindUpdate(); + unbindOnScroll(); 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 const onPointerDown = (event: MouseEvent) => { 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 = target instanceof HTMLElement && target.classList.contains("properties-trigger"); @@ -630,17 +658,14 @@ export const textWysiwyg = ({ if ( ((event.target instanceof HTMLElement || event.target instanceof SVGElement) && - event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) && + event.target.closest( + `.${CLASSES.SHAPE_ACTIONS_MENU}, .${CLASSES.ZOOM_ACTIONS}`, + ) && !isWritableElement(event.target)) || isPropertiesTrigger ) { - 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); + temporarilyDisableSubmit(); } else if ( - event.target instanceof HTMLElement && event.target instanceof HTMLCanvasElement && // Vitest simply ignores stopPropagation, capture-mode, or rAF // so without introducing crazier hacks, nothing we can do @@ -659,7 +684,7 @@ export const textWysiwyg = ({ }; // handle updates of textElement properties of editing element - const unbindUpdate = Scene.getScene(element)!.onUpdate(() => { + const unbindUpdate = app.scene.onUpdate(() => { updateWysiwygStyle(); const isPopupOpened = !!document.activeElement?.closest( ".properties-content", @@ -669,6 +694,10 @@ export const textWysiwyg = ({ } }); + const unbindOnScroll = app.onScrollChangeEmitter.on(() => { + updateWysiwygStyle(); + }); + // --------------------------------------------------------------------------- let isDestroyed = false; @@ -699,10 +728,6 @@ export const textWysiwyg = ({ requestAnimationFrame(() => { window.addEventListener("pointerdown", onPointerDown, { capture: true }); }); - window.addEventListener("wheel", stopEvent, { - passive: false, - capture: true, - }); window.addEventListener("beforeunload", handleSubmit); excalidrawContainer ?.querySelector(".excalidraw-textEditorContainer")!