From 26d22965785e0b307efaa6dc3b7e9c65f1e84a6b Mon Sep 17 00:00:00 2001 From: spc-28 <139547705+spc-28@users.noreply.github.com> Date: Tue, 27 Aug 2024 03:57:44 +0530 Subject: [PATCH 1/6] fix: fixed copy to clipboard button (#8426) Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- excalidraw-app/collab/RoomDialog.tsx | 218 ------------------ excalidraw-app/share/ShareDialog.scss | 4 +- excalidraw-app/share/ShareDialog.tsx | 36 ++- .../excalidraw/components/FilledButton.scss | 56 ++++- .../excalidraw/components/FilledButton.tsx | 30 ++- .../components/ImageExportDialog.tsx | 19 +- .../components/ShareableLinkDialog.scss | 4 +- .../components/ShareableLinkDialog.tsx | 38 ++- packages/excalidraw/components/Spinner.tsx | 4 +- packages/excalidraw/css/theme.scss | 10 +- .../excalidraw/hooks/useCopiedIndicator.ts | 22 ++ packages/excalidraw/locales/en.json | 1 + 12 files changed, 159 insertions(+), 283 deletions(-) delete mode 100644 excalidraw-app/collab/RoomDialog.tsx create mode 100644 packages/excalidraw/hooks/useCopiedIndicator.ts diff --git a/excalidraw-app/collab/RoomDialog.tsx b/excalidraw-app/collab/RoomDialog.tsx deleted file mode 100644 index 74266d3d91bd..000000000000 --- a/excalidraw-app/collab/RoomDialog.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import { useRef, useState } from "react"; -import * as Popover from "@radix-ui/react-popover"; - -import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard"; -import { trackEvent } from "../../packages/excalidraw/analytics"; -import { getFrame } from "../../packages/excalidraw/utils"; -import { useI18n } from "../../packages/excalidraw/i18n"; -import { KEYS } from "../../packages/excalidraw/keys"; - -import { Dialog } from "../../packages/excalidraw/components/Dialog"; -import { - copyIcon, - playerPlayIcon, - playerStopFilledIcon, - share, - shareIOS, - shareWindows, - tablerCheckIcon, -} from "../../packages/excalidraw/components/icons"; -import { TextField } from "../../packages/excalidraw/components/TextField"; -import { FilledButton } from "../../packages/excalidraw/components/FilledButton"; - -import { ReactComponent as CollabImage } from "../../packages/excalidraw/assets/lock.svg"; -import "./RoomDialog.scss"; - -const getShareIcon = () => { - const navigator = window.navigator as any; - const isAppleBrowser = /Apple/.test(navigator.vendor); - const isWindowsBrowser = navigator.appVersion.indexOf("Win") !== -1; - - if (isAppleBrowser) { - return shareIOS; - } else if (isWindowsBrowser) { - return shareWindows; - } - - return share; -}; - -export type RoomModalProps = { - handleClose: () => void; - activeRoomLink: string; - username: string; - onUsernameChange: (username: string) => void; - onRoomCreate: () => void; - onRoomDestroy: () => void; - setErrorMessage: (message: string) => void; -}; - -export const RoomModal = ({ - activeRoomLink, - onRoomCreate, - onRoomDestroy, - setErrorMessage, - username, - onUsernameChange, - handleClose, -}: RoomModalProps) => { - const { t } = useI18n(); - const [justCopied, setJustCopied] = useState(false); - const timerRef = useRef(0); - const ref = useRef(null); - const isShareSupported = "share" in navigator; - - const copyRoomLink = async () => { - try { - await copyTextToSystemClipboard(activeRoomLink); - } catch (e) { - setErrorMessage(t("errors.copyToSystemClipboardFailed")); - } - setJustCopied(true); - - if (timerRef.current) { - window.clearTimeout(timerRef.current); - } - - timerRef.current = window.setTimeout(() => { - setJustCopied(false); - }, 3000); - - ref.current?.select(); - }; - - const shareRoomLink = async () => { - try { - await navigator.share({ - title: t("roomDialog.shareTitle"), - text: t("roomDialog.shareTitle"), - url: activeRoomLink, - }); - } catch (error: any) { - // Just ignore. - } - }; - - if (activeRoomLink) { - return ( - <> -

- {t("labels.liveCollaboration")} -

- event.key === KEYS.ENTER && handleClose()} - /> -
- - {isShareSupported && ( - - )} - - - - - event.preventDefault()} - onCloseAutoFocus={(event) => event.preventDefault()} - className="RoomDialog__popover" - side="top" - align="end" - sideOffset={5.5} - > - {tablerCheckIcon} copied - - -
-
-

- - {t("roomDialog.desc_privacy")} -

-

{t("roomDialog.desc_exitSession")}

-
- -
- { - trackEvent("share", "room closed"); - onRoomDestroy(); - }} - /> -
- - ); - } - - return ( - <> -
- -
-
- {t("labels.liveCollaboration")} -
- -
- {t("roomDialog.desc_intro")} - {t("roomDialog.desc_privacy")} -
- -
- { - trackEvent("share", "room creation", `ui (${getFrame()})`); - onRoomCreate(); - }} - /> -
- - ); -}; - -const RoomDialog = (props: RoomModalProps) => { - return ( - -
- -
-
- ); -}; - -export default RoomDialog; diff --git a/excalidraw-app/share/ShareDialog.scss b/excalidraw-app/share/ShareDialog.scss index 87fde849146e..436f411248af 100644 --- a/excalidraw-app/share/ShareDialog.scss +++ b/excalidraw-app/share/ShareDialog.scss @@ -58,8 +58,8 @@ font-size: 0.75rem; line-height: 110%; - background: var(--color-success-lighter); - color: var(--color-success); + background: var(--color-success); + color: var(--color-success-text); & > svg { width: 0.875rem; diff --git a/excalidraw-app/share/ShareDialog.tsx b/excalidraw-app/share/ShareDialog.tsx index 6511eec127e9..d0a078cd25b2 100644 --- a/excalidraw-app/share/ShareDialog.tsx +++ b/excalidraw-app/share/ShareDialog.tsx @@ -1,5 +1,4 @@ import { useEffect, useRef, useState } from "react"; -import * as Popover from "@radix-ui/react-popover"; import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard"; import { trackEvent } from "../../packages/excalidraw/analytics"; import { getFrame } from "../../packages/excalidraw/utils"; @@ -14,7 +13,6 @@ import { share, shareIOS, shareWindows, - tablerCheckIcon, } from "../../packages/excalidraw/components/icons"; import { TextField } from "../../packages/excalidraw/components/TextField"; import { FilledButton } from "../../packages/excalidraw/components/FilledButton"; @@ -24,6 +22,7 @@ import { atom, useAtom, useAtomValue } from "jotai"; import "./ShareDialog.scss"; import { useUIAppState } from "../../packages/excalidraw/context/ui-appState"; +import { useCopyStatus } from "../../packages/excalidraw/hooks/useCopiedIndicator"; type OnExportToBackend = () => void; type ShareDialogType = "share" | "collaborationOnly"; @@ -63,10 +62,11 @@ const ActiveRoomDialog = ({ handleClose: () => void; }) => { const { t } = useI18n(); - const [justCopied, setJustCopied] = useState(false); + const [, setJustCopied] = useState(false); const timerRef = useRef(0); const ref = useRef(null); const isShareSupported = "share" in navigator; + const { onCopy, copyStatus } = useCopyStatus(); const copyRoomLink = async () => { try { @@ -130,26 +130,16 @@ const ActiveRoomDialog = ({ onClick={shareRoomLink} /> )} - - - - - event.preventDefault()} - onCloseAutoFocus={(event) => event.preventDefault()} - className="ShareDialog__popover" - side="top" - align="end" - sideOffset={5.5} - > - {tablerCheckIcon} copied - - + { + copyRoomLink(); + onCopy(); + }} + />

diff --git a/packages/excalidraw/components/FilledButton.scss b/packages/excalidraw/components/FilledButton.scss index d23c9d104961..771f36403186 100644 --- a/packages/excalidraw/components/FilledButton.scss +++ b/packages/excalidraw/components/FilledButton.scss @@ -16,11 +16,19 @@ .Spinner { --spinner-color: var(--color-surface-lowest); - position: absolute; + } + + .ExcButton__statusIcon { visibility: visible; + position: absolute; + + width: 1rem; + height: 1rem; + font-size: 1rem; } - &[disabled] { + &.ExcButton--status-loading, + &.ExcButton--status-success { pointer-events: none; .ExcButton__contents { @@ -28,6 +36,10 @@ } } + &[disabled] { + pointer-events: none; + } + &, &__contents { display: flex; @@ -119,6 +131,46 @@ } } + &--color-success { + &.ExcButton--variant-filled { + --text-color: var(--color-success-text); + --back-color: var(--color-success); + + .Spinner { + --spinner-color: var(--color-success); + } + + &:hover { + --back-color: var(--color-success-darker); + } + + &:active { + --back-color: var(--color-success-darkest); + } + } + + &.ExcButton--variant-outlined, + &.ExcButton--variant-icon { + --text-color: var(--color-success-contrast); + --border-color: var(--color-success-contrast); + --back-color: transparent; + + .Spinner { + --spinner-color: var(--color-success-contrast); + } + + &:hover { + --text-color: var(--color-success-contrast-hover); + --border-color: var(--color-success-contrast-hover); + } + + &:active { + --text-color: var(--color-success-contrast-active); + --border-color: var(--color-success-contrast-active); + } + } + } + &--color-muted { &.ExcButton--variant-filled { --text-color: var(--island-bg-color); diff --git a/packages/excalidraw/components/FilledButton.tsx b/packages/excalidraw/components/FilledButton.tsx index ff17db623f7c..1360908484c6 100644 --- a/packages/excalidraw/components/FilledButton.tsx +++ b/packages/excalidraw/components/FilledButton.tsx @@ -5,9 +5,15 @@ import "./FilledButton.scss"; import { AbortError } from "../errors"; import Spinner from "./Spinner"; import { isPromiseLike } from "../utils"; +import { tablerCheckIcon } from "./icons"; export type ButtonVariant = "filled" | "outlined" | "icon"; -export type ButtonColor = "primary" | "danger" | "warning" | "muted"; +export type ButtonColor = + | "primary" + | "danger" + | "warning" + | "muted" + | "success"; export type ButtonSize = "medium" | "large"; export type FilledButtonProps = { @@ -15,6 +21,7 @@ export type FilledButtonProps = { children?: React.ReactNode; onClick?: (event: React.MouseEvent) => void; + status?: null | "loading" | "success"; variant?: ButtonVariant; color?: ButtonColor; @@ -37,6 +44,7 @@ export const FilledButton = forwardRef( size = "medium", fullWidth, className, + status, }, ref, ) => { @@ -46,8 +54,11 @@ export const FilledButton = forwardRef( const ret = onClick?.(event); if (isPromiseLike(ret)) { - try { + // delay loading state to prevent flicker in case of quick response + const timer = window.setTimeout(() => { setIsLoading(true); + }, 50); + try { await ret; } catch (error: any) { if (!(error instanceof AbortError)) { @@ -56,11 +67,15 @@ export const FilledButton = forwardRef( console.warn(error); } } finally { + clearTimeout(timer); setIsLoading(false); } } }; + const _status = isLoading ? "loading" : status; + color = _status === "success" ? "success" : color; + return ( + + + + + ); +}; + +interface DebugCanvasProps { + appState: AppState; + scale: number; +} + +const DebugCanvas = forwardRef( + ({ appState, scale }, ref) => { + const { width, height } = appState; + + const canvasRef = useRef(null); + useImperativeHandle( + ref, + () => canvasRef.current, + [canvasRef], + ); + + return ( + + Debug Canvas + + ); + }, +); + +export default DebugCanvas; diff --git a/packages/excalidraw/element/typeChecks.ts b/packages/excalidraw/element/typeChecks.ts index 9c1d6913dc0d..78f1a458a06a 100644 --- a/packages/excalidraw/element/typeChecks.ts +++ b/packages/excalidraw/element/typeChecks.ts @@ -1,7 +1,9 @@ +import type { LineSegment } from "../../utils"; import { ROUNDNESS } from "../constants"; -import type { ElementOrToolType } from "../types"; +import type { ElementOrToolType, Point } from "../types"; import type { MarkNonNullable } from "../utility-types"; import { assertNever } from "../utils"; +import type { Bounds } from "./bounds"; import type { ExcalidrawElement, ExcalidrawTextElement, @@ -322,3 +324,23 @@ export const isFixedPointBinding = ( ): binding is FixedPointBinding => { return binding.fixedPoint != null; }; + +// TODO: Move this to @excalidraw/math +export const isPoint = (point: unknown): point is Point => + Array.isArray(point) && point.length === 2; + +// TODO: Move this to @excalidraw/math +export const isBounds = (box: unknown): box is Bounds => + Array.isArray(box) && + box.length === 4 && + typeof box[0] === "number" && + typeof box[1] === "number" && + typeof box[2] === "number" && + typeof box[3] === "number"; + +// TODO: Move this to @excalidraw/math +export const isLineSegment = (segment: unknown): segment is LineSegment => + Array.isArray(segment) && + segment.length === 2 && + isPoint(segment[0]) && + isPoint(segment[0]); diff --git a/packages/excalidraw/visualdebug.ts b/packages/excalidraw/visualdebug.ts new file mode 100644 index 000000000000..f6ab8f7447b6 --- /dev/null +++ b/packages/excalidraw/visualdebug.ts @@ -0,0 +1,157 @@ +import type { LineSegment } from "../utils"; +import type { BoundingBox, Bounds } from "./element/bounds"; +import { isBounds, isLineSegment } from "./element/typeChecks"; +import type { Point } from "./types"; + +// The global data holder to collect the debug operations +declare global { + interface Window { + visualDebug?: { + data: DebugElement[][]; + currentFrame?: number; + }; + } +} + +export type DebugElement = { + color: string; + data: LineSegment; + permanent: boolean; +}; + +export const debugDrawLine = ( + segment: LineSegment | LineSegment[], + opts?: { + color?: string; + permanent?: boolean; + }, +) => { + (isLineSegment(segment) ? [segment] : segment).forEach((data) => + addToCurrentFrame({ + color: opts?.color ?? "red", + data, + permanent: !!opts?.permanent, + }), + ); +}; + +export const debugDrawPoint = ( + point: Point, + opts?: { + color?: string; + permanent?: boolean; + fuzzy?: boolean; + }, +) => { + const xOffset = opts?.fuzzy ? Math.random() * 3 : 0; + const yOffset = opts?.fuzzy ? Math.random() * 3 : 0; + + debugDrawLine( + [ + [point[0] + xOffset - 10, point[1] + yOffset - 10], + [point[0] + xOffset + 10, point[1] + yOffset + 10], + ], + { + color: opts?.color ?? "cyan", + permanent: opts?.permanent, + }, + ); + debugDrawLine( + [ + [point[0] + xOffset - 10, point[1] + yOffset + 10], + [point[0] + xOffset + 10, point[1] + yOffset - 10], + ], + { + color: opts?.color ?? "cyan", + permanent: opts?.permanent, + }, + ); +}; + +export const debugDrawBoundingBox = ( + box: BoundingBox | BoundingBox[], + opts?: { + color?: string; + permanent?: boolean; + }, +) => { + (Array.isArray(box) ? box : [box]).forEach((bbox) => + debugDrawLine( + [ + [ + [bbox.minX, bbox.minY], + [bbox.maxX, bbox.minY], + ], + [ + [bbox.maxX, bbox.minY], + [bbox.maxX, bbox.maxY], + ], + [ + [bbox.maxX, bbox.maxY], + [bbox.minX, bbox.maxY], + ], + [ + [bbox.minX, bbox.maxY], + [bbox.minX, bbox.minY], + ], + ], + { + color: opts?.color ?? "cyan", + permanent: opts?.permanent, + }, + ), + ); +}; + +export const debugDrawBounds = ( + box: Bounds | Bounds[], + opts?: { + color: string; + permanent: boolean; + }, +) => { + (isBounds(box) ? [box] : box).forEach((bbox) => + debugDrawLine( + [ + [ + [bbox[0], bbox[1]], + [bbox[2], bbox[1]], + ], + [ + [bbox[2], bbox[1]], + [bbox[2], bbox[3]], + ], + [ + [bbox[2], bbox[3]], + [bbox[0], bbox[3]], + ], + [ + [bbox[0], bbox[3]], + [bbox[0], bbox[1]], + ], + ], + { + color: opts?.color ?? "green", + permanent: opts?.permanent, + }, + ), + ); +}; + +export const debugCloseFrame = () => { + window.visualDebug?.data.push([]); +}; + +export const debugClear = () => { + if (window.visualDebug?.data) { + window.visualDebug.data = []; + } +}; + +const addToCurrentFrame = (element: DebugElement) => { + if (window.visualDebug?.data && window.visualDebug.data.length === 0) { + window.visualDebug.data[0] = []; + } + window.visualDebug?.data && + window.visualDebug.data[window.visualDebug.data.length - 1].push(element); +}; 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 3/6] 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 3673eafadcfd..b818d1a23f1e 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 b54bf06452d3..ef5d7bc2225b 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 27bbfb4e0458..9601807f6f1b 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 5093a2624c37..2281a0cc337e 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")! From 576bc0dbe52c705b5251a3599962d5d53749994d Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Thu, 29 Aug 2024 00:43:01 +0200 Subject: [PATCH 4/6] feat: tweak copy button success animation (#8441) --- .../excalidraw/components/FilledButton.scss | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/excalidraw/components/FilledButton.scss b/packages/excalidraw/components/FilledButton.scss index 771f36403186..1f689e96970a 100644 --- a/packages/excalidraw/components/FilledButton.scss +++ b/packages/excalidraw/components/FilledButton.scss @@ -1,5 +1,19 @@ @import "../css/variables.module.scss"; +@keyframes successStatusAnimation { + 0% { + transform: scale(0.35); + } + + 50% { + transform: scale(1.25); + } + + 100% { + transform: scale(1); + } +} + .excalidraw { .ExcButton { --text-color: transparent; @@ -22,9 +36,10 @@ visibility: visible; position: absolute; - width: 1rem; - height: 1rem; - font-size: 1rem; + width: 1.2rem; + height: 1.2rem; + + animation: successStatusAnimation 0.5s cubic-bezier(0.3, 1, 0.6, 1); } &.ExcButton--status-loading, From 16cae4fc07c5d114ff63efc686c5cffdfccf5bff Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:10:08 +0200 Subject: [PATCH 5/6] feat: reset copyStatus on export dialog settings change (#8443) --- .../excalidraw/components/ImageExportDialog.tsx | 16 +++++++++++++++- packages/excalidraw/hooks/useCopiedIndicator.ts | 7 ++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/excalidraw/components/ImageExportDialog.tsx b/packages/excalidraw/components/ImageExportDialog.tsx index 0f6a75703632..8c225ab54280 100644 --- a/packages/excalidraw/components/ImageExportDialog.tsx +++ b/packages/excalidraw/components/ImageExportDialog.tsx @@ -90,7 +90,20 @@ const ImageExportModal = ({ const previewRef = useRef(null); const [renderError, setRenderError] = useState(null); - const { onCopy, copyStatus } = useCopyStatus(); + const { onCopy, copyStatus, resetCopyStatus } = useCopyStatus(); + + useEffect(() => { + // if user changes setting right after export to clipboard, reset the status + // so they don't have to wait for the timeout to click the button again + resetCopyStatus(); + }, [ + projectName, + exportWithBackground, + exportDarkMode, + exportScale, + embedScene, + resetCopyStatus, + ]); const { exportedElements, exportingFrame } = prepareElementsForExport( elementsSnapshot, @@ -108,6 +121,7 @@ const ImageExportModal = ({ if (!maxWidth) { return; } + exportToCanvas({ elements: exportedElements, appState: { diff --git a/packages/excalidraw/hooks/useCopiedIndicator.ts b/packages/excalidraw/hooks/useCopiedIndicator.ts index 2204e6db482c..18ad79348665 100644 --- a/packages/excalidraw/hooks/useCopiedIndicator.ts +++ b/packages/excalidraw/hooks/useCopiedIndicator.ts @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useCallback, useRef, useState } from "react"; const TIMEOUT = 2000; @@ -15,8 +15,13 @@ export const useCopyStatus = () => { }, TIMEOUT); }; + const resetCopyStatus = useCallback(() => { + setCopyStatus(null); + }, []); + return { copyStatus, + resetCopyStatus, onCopy, }; }; From 54d57b5675e74baaa6b0050a65fe02979b3a1761 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Thu, 29 Aug 2024 22:27:01 +0200 Subject: [PATCH 6/6] comment --- packages/excalidraw/components/App.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index c0b3ae79a3c5..7dee13b3134e 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -6899,6 +6899,7 @@ class App extends React.Component { (event.button === POINTER_BUTTON.WHEEL || (event.button === POINTER_BUTTON.MAIN && isHoldingSpace) || isHandToolActive(this.state) || + //!isLastPointerActive added by zsviczian (but don't remember why....) (this.state.viewModeEnabled && !isLaserPointerActive(this.state))) ) ) {