mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-02-18 13:29:36 +01:00
fix: fixed copy to clipboard button (#8426)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
afb68a6467
commit
26d2296578
@ -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<number>(0);
|
|
||||||
const ref = useRef<HTMLInputElement>(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 (
|
|
||||||
<>
|
|
||||||
<h3 className="RoomDialog__active__header">
|
|
||||||
{t("labels.liveCollaboration")}
|
|
||||||
</h3>
|
|
||||||
<TextField
|
|
||||||
value={username}
|
|
||||||
placeholder="Your name"
|
|
||||||
label="Your name"
|
|
||||||
onChange={onUsernameChange}
|
|
||||||
onKeyDown={(event) => event.key === KEYS.ENTER && handleClose()}
|
|
||||||
/>
|
|
||||||
<div className="RoomDialog__active__linkRow">
|
|
||||||
<TextField
|
|
||||||
ref={ref}
|
|
||||||
label="Link"
|
|
||||||
readonly
|
|
||||||
fullWidth
|
|
||||||
value={activeRoomLink}
|
|
||||||
/>
|
|
||||||
{isShareSupported && (
|
|
||||||
<FilledButton
|
|
||||||
size="large"
|
|
||||||
variant="icon"
|
|
||||||
label="Share"
|
|
||||||
icon={getShareIcon()}
|
|
||||||
className="RoomDialog__active__share"
|
|
||||||
onClick={shareRoomLink}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Popover.Root open={justCopied}>
|
|
||||||
<Popover.Trigger asChild>
|
|
||||||
<FilledButton
|
|
||||||
size="large"
|
|
||||||
label="Copy link"
|
|
||||||
icon={copyIcon}
|
|
||||||
onClick={copyRoomLink}
|
|
||||||
/>
|
|
||||||
</Popover.Trigger>
|
|
||||||
<Popover.Content
|
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
||||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
||||||
className="RoomDialog__popover"
|
|
||||||
side="top"
|
|
||||||
align="end"
|
|
||||||
sideOffset={5.5}
|
|
||||||
>
|
|
||||||
{tablerCheckIcon} copied
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Root>
|
|
||||||
</div>
|
|
||||||
<div className="RoomDialog__active__description">
|
|
||||||
<p>
|
|
||||||
<span
|
|
||||||
role="img"
|
|
||||||
aria-hidden="true"
|
|
||||||
className="RoomDialog__active__description__emoji"
|
|
||||||
>
|
|
||||||
🔒{" "}
|
|
||||||
</span>
|
|
||||||
{t("roomDialog.desc_privacy")}
|
|
||||||
</p>
|
|
||||||
<p>{t("roomDialog.desc_exitSession")}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="RoomDialog__active__actions">
|
|
||||||
<FilledButton
|
|
||||||
size="large"
|
|
||||||
variant="outlined"
|
|
||||||
color="danger"
|
|
||||||
label={t("roomDialog.button_stopSession")}
|
|
||||||
icon={playerStopFilledIcon}
|
|
||||||
onClick={() => {
|
|
||||||
trackEvent("share", "room closed");
|
|
||||||
onRoomDestroy();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="RoomDialog__inactive__illustration">
|
|
||||||
<CollabImage />
|
|
||||||
</div>
|
|
||||||
<div className="RoomDialog__inactive__header">
|
|
||||||
{t("labels.liveCollaboration")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="RoomDialog__inactive__description">
|
|
||||||
<strong>{t("roomDialog.desc_intro")}</strong>
|
|
||||||
{t("roomDialog.desc_privacy")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="RoomDialog__inactive__start_session">
|
|
||||||
<FilledButton
|
|
||||||
size="large"
|
|
||||||
label={t("roomDialog.button_startSession")}
|
|
||||||
icon={playerPlayIcon}
|
|
||||||
onClick={() => {
|
|
||||||
trackEvent("share", "room creation", `ui (${getFrame()})`);
|
|
||||||
onRoomCreate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RoomDialog = (props: RoomModalProps) => {
|
|
||||||
return (
|
|
||||||
<Dialog size="small" onCloseRequest={props.handleClose} title={false}>
|
|
||||||
<div className="RoomDialog">
|
|
||||||
<RoomModal {...props} />
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoomDialog;
|
|
@ -58,8 +58,8 @@
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
line-height: 110%;
|
line-height: 110%;
|
||||||
|
|
||||||
background: var(--color-success-lighter);
|
background: var(--color-success);
|
||||||
color: var(--color-success);
|
color: var(--color-success-text);
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
width: 0.875rem;
|
width: 0.875rem;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
|
||||||
import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard";
|
import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard";
|
||||||
import { trackEvent } from "../../packages/excalidraw/analytics";
|
import { trackEvent } from "../../packages/excalidraw/analytics";
|
||||||
import { getFrame } from "../../packages/excalidraw/utils";
|
import { getFrame } from "../../packages/excalidraw/utils";
|
||||||
@ -14,7 +13,6 @@ import {
|
|||||||
share,
|
share,
|
||||||
shareIOS,
|
shareIOS,
|
||||||
shareWindows,
|
shareWindows,
|
||||||
tablerCheckIcon,
|
|
||||||
} from "../../packages/excalidraw/components/icons";
|
} from "../../packages/excalidraw/components/icons";
|
||||||
import { TextField } from "../../packages/excalidraw/components/TextField";
|
import { TextField } from "../../packages/excalidraw/components/TextField";
|
||||||
import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
|
import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
|
||||||
@ -24,6 +22,7 @@ import { atom, useAtom, useAtomValue } from "jotai";
|
|||||||
|
|
||||||
import "./ShareDialog.scss";
|
import "./ShareDialog.scss";
|
||||||
import { useUIAppState } from "../../packages/excalidraw/context/ui-appState";
|
import { useUIAppState } from "../../packages/excalidraw/context/ui-appState";
|
||||||
|
import { useCopyStatus } from "../../packages/excalidraw/hooks/useCopiedIndicator";
|
||||||
|
|
||||||
type OnExportToBackend = () => void;
|
type OnExportToBackend = () => void;
|
||||||
type ShareDialogType = "share" | "collaborationOnly";
|
type ShareDialogType = "share" | "collaborationOnly";
|
||||||
@ -63,10 +62,11 @@ const ActiveRoomDialog = ({
|
|||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [justCopied, setJustCopied] = useState(false);
|
const [, setJustCopied] = useState(false);
|
||||||
const timerRef = useRef<number>(0);
|
const timerRef = useRef<number>(0);
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
const isShareSupported = "share" in navigator;
|
const isShareSupported = "share" in navigator;
|
||||||
|
const { onCopy, copyStatus } = useCopyStatus();
|
||||||
|
|
||||||
const copyRoomLink = async () => {
|
const copyRoomLink = async () => {
|
||||||
try {
|
try {
|
||||||
@ -130,26 +130,16 @@ const ActiveRoomDialog = ({
|
|||||||
onClick={shareRoomLink}
|
onClick={shareRoomLink}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Popover.Root open={justCopied}>
|
<FilledButton
|
||||||
<Popover.Trigger asChild>
|
size="large"
|
||||||
<FilledButton
|
label={t("buttons.copyLink")}
|
||||||
size="large"
|
icon={copyIcon}
|
||||||
label="Copy link"
|
status={copyStatus}
|
||||||
icon={copyIcon}
|
onClick={() => {
|
||||||
onClick={copyRoomLink}
|
copyRoomLink();
|
||||||
/>
|
onCopy();
|
||||||
</Popover.Trigger>
|
}}
|
||||||
<Popover.Content
|
/>
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
||||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
||||||
className="ShareDialog__popover"
|
|
||||||
side="top"
|
|
||||||
align="end"
|
|
||||||
sideOffset={5.5}
|
|
||||||
>
|
|
||||||
{tablerCheckIcon} copied
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Root>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ShareDialog__active__description">
|
<div className="ShareDialog__active__description">
|
||||||
<p>
|
<p>
|
||||||
|
@ -16,11 +16,19 @@
|
|||||||
|
|
||||||
.Spinner {
|
.Spinner {
|
||||||
--spinner-color: var(--color-surface-lowest);
|
--spinner-color: var(--color-surface-lowest);
|
||||||
position: absolute;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[disabled] {
|
.ExcButton__statusIcon {
|
||||||
|
visibility: visible;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ExcButton--status-loading,
|
||||||
|
&.ExcButton--status-success {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
.ExcButton__contents {
|
.ExcButton__contents {
|
||||||
@ -28,6 +36,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
&,
|
&,
|
||||||
&__contents {
|
&__contents {
|
||||||
display: flex;
|
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 {
|
&--color-muted {
|
||||||
&.ExcButton--variant-filled {
|
&.ExcButton--variant-filled {
|
||||||
--text-color: var(--island-bg-color);
|
--text-color: var(--island-bg-color);
|
||||||
|
@ -5,9 +5,15 @@ import "./FilledButton.scss";
|
|||||||
import { AbortError } from "../errors";
|
import { AbortError } from "../errors";
|
||||||
import Spinner from "./Spinner";
|
import Spinner from "./Spinner";
|
||||||
import { isPromiseLike } from "../utils";
|
import { isPromiseLike } from "../utils";
|
||||||
|
import { tablerCheckIcon } from "./icons";
|
||||||
|
|
||||||
export type ButtonVariant = "filled" | "outlined" | "icon";
|
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 ButtonSize = "medium" | "large";
|
||||||
|
|
||||||
export type FilledButtonProps = {
|
export type FilledButtonProps = {
|
||||||
@ -15,6 +21,7 @@ export type FilledButtonProps = {
|
|||||||
|
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onClick?: (event: React.MouseEvent) => void;
|
onClick?: (event: React.MouseEvent) => void;
|
||||||
|
status?: null | "loading" | "success";
|
||||||
|
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
color?: ButtonColor;
|
color?: ButtonColor;
|
||||||
@ -37,6 +44,7 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||||||
size = "medium",
|
size = "medium",
|
||||||
fullWidth,
|
fullWidth,
|
||||||
className,
|
className,
|
||||||
|
status,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
@ -46,8 +54,11 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||||||
const ret = onClick?.(event);
|
const ret = onClick?.(event);
|
||||||
|
|
||||||
if (isPromiseLike(ret)) {
|
if (isPromiseLike(ret)) {
|
||||||
try {
|
// delay loading state to prevent flicker in case of quick response
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
}, 50);
|
||||||
|
try {
|
||||||
await ret;
|
await ret;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (!(error instanceof AbortError)) {
|
if (!(error instanceof AbortError)) {
|
||||||
@ -56,11 +67,15 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||||||
console.warn(error);
|
console.warn(error);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _status = isLoading ? "loading" : status;
|
||||||
|
color = _status === "success" ? "success" : color;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -68,6 +83,7 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||||||
`ExcButton--color-${color}`,
|
`ExcButton--color-${color}`,
|
||||||
`ExcButton--variant-${variant}`,
|
`ExcButton--variant-${variant}`,
|
||||||
`ExcButton--size-${size}`,
|
`ExcButton--size-${size}`,
|
||||||
|
`ExcButton--status-${_status}`,
|
||||||
{ "ExcButton--fullWidth": fullWidth },
|
{ "ExcButton--fullWidth": fullWidth },
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@ -75,10 +91,16 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
disabled={isLoading}
|
disabled={_status === "loading" || _status === "success"}
|
||||||
>
|
>
|
||||||
<div className="ExcButton__contents">
|
<div className="ExcButton__contents">
|
||||||
{isLoading && <Spinner />}
|
{_status === "loading" ? (
|
||||||
|
<Spinner className="ExcButton__statusIcon" />
|
||||||
|
) : (
|
||||||
|
_status === "success" && (
|
||||||
|
<div className="ExcButton__statusIcon">{tablerCheckIcon}</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
{icon && (
|
{icon && (
|
||||||
<div className="ExcButton__icon" aria-hidden>
|
<div className="ExcButton__icon" aria-hidden>
|
||||||
{icon}
|
{icon}
|
||||||
|
@ -35,6 +35,7 @@ import "./ImageExportDialog.scss";
|
|||||||
import { FilledButton } from "./FilledButton";
|
import { FilledButton } from "./FilledButton";
|
||||||
import { cloneJSON } from "../utils";
|
import { cloneJSON } from "../utils";
|
||||||
import { prepareElementsForExport } from "../data";
|
import { prepareElementsForExport } from "../data";
|
||||||
|
import { useCopyStatus } from "../hooks/useCopiedIndicator";
|
||||||
|
|
||||||
const supportsContextFilters =
|
const supportsContextFilters =
|
||||||
"filter" in document.createElement("canvas").getContext("2d")!;
|
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||||
@ -89,6 +90,8 @@ const ImageExportModal = ({
|
|||||||
const previewRef = useRef<HTMLDivElement>(null);
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
const [renderError, setRenderError] = useState<Error | null>(null);
|
const [renderError, setRenderError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
const { onCopy, copyStatus } = useCopyStatus();
|
||||||
|
|
||||||
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
||||||
elementsSnapshot,
|
elementsSnapshot,
|
||||||
appStateSnapshot,
|
appStateSnapshot,
|
||||||
@ -294,11 +297,17 @@ const ImageExportModal = ({
|
|||||||
<FilledButton
|
<FilledButton
|
||||||
className="ImageExportModal__settings__buttons__button"
|
className="ImageExportModal__settings__buttons__button"
|
||||||
label={t("imageExportDialog.title.copyPngToClipboard")}
|
label={t("imageExportDialog.title.copyPngToClipboard")}
|
||||||
onClick={() =>
|
status={copyStatus}
|
||||||
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements, {
|
onClick={async () => {
|
||||||
exportingFrame,
|
await onExportImage(
|
||||||
})
|
EXPORT_IMAGE_TYPES.clipboard,
|
||||||
}
|
exportedElements,
|
||||||
|
{
|
||||||
|
exportingFrame,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
onCopy();
|
||||||
|
}}
|
||||||
icon={copyIcon}
|
icon={copyIcon}
|
||||||
>
|
>
|
||||||
{t("imageExportDialog.button.copyPngToClipboard")}
|
{t("imageExportDialog.button.copyPngToClipboard")}
|
||||||
|
@ -52,8 +52,8 @@
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
line-height: 110%;
|
line-height: 110%;
|
||||||
|
|
||||||
background: var(--color-success-lighter);
|
background: var(--color-success);
|
||||||
color: var(--color-success);
|
color: var(--color-success-text);
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
width: 0.875rem;
|
width: 0.875rem;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
|
||||||
|
|
||||||
import { copyTextToSystemClipboard } from "../clipboard";
|
import { copyTextToSystemClipboard } from "../clipboard";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
@ -7,7 +6,8 @@ import { useI18n } from "../i18n";
|
|||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import { TextField } from "./TextField";
|
import { TextField } from "./TextField";
|
||||||
import { FilledButton } from "./FilledButton";
|
import { FilledButton } from "./FilledButton";
|
||||||
import { copyIcon, tablerCheckIcon } from "./icons";
|
import { useCopyStatus } from "../hooks/useCopiedIndicator";
|
||||||
|
import { copyIcon } from "./icons";
|
||||||
|
|
||||||
import "./ShareableLinkDialog.scss";
|
import "./ShareableLinkDialog.scss";
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ export const ShareableLinkDialog = ({
|
|||||||
setErrorMessage,
|
setErrorMessage,
|
||||||
}: ShareableLinkDialogProps) => {
|
}: ShareableLinkDialogProps) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [justCopied, setJustCopied] = useState(false);
|
const [, setJustCopied] = useState(false);
|
||||||
const timerRef = useRef<number>(0);
|
const timerRef = useRef<number>(0);
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ export const ShareableLinkDialog = ({
|
|||||||
|
|
||||||
ref.current?.select();
|
ref.current?.select();
|
||||||
};
|
};
|
||||||
|
const { onCopy, copyStatus } = useCopyStatus();
|
||||||
return (
|
return (
|
||||||
<Dialog onCloseRequest={onCloseRequest} title={false} size="small">
|
<Dialog onCloseRequest={onCloseRequest} title={false} size="small">
|
||||||
<div className="ShareableLinkDialog">
|
<div className="ShareableLinkDialog">
|
||||||
@ -60,26 +60,16 @@ export const ShareableLinkDialog = ({
|
|||||||
value={link}
|
value={link}
|
||||||
selectOnRender
|
selectOnRender
|
||||||
/>
|
/>
|
||||||
<Popover.Root open={justCopied}>
|
<FilledButton
|
||||||
<Popover.Trigger asChild>
|
size="large"
|
||||||
<FilledButton
|
label={t("buttons.copyLink")}
|
||||||
size="large"
|
icon={copyIcon}
|
||||||
label="Copy link"
|
status={copyStatus}
|
||||||
icon={copyIcon}
|
onClick={() => {
|
||||||
onClick={copyRoomLink}
|
onCopy();
|
||||||
/>
|
copyRoomLink();
|
||||||
</Popover.Trigger>
|
}}
|
||||||
<Popover.Content
|
/>
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
||||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
||||||
className="ShareableLinkDialog__popover"
|
|
||||||
side="top"
|
|
||||||
align="end"
|
|
||||||
sideOffset={5.5}
|
|
||||||
>
|
|
||||||
{tablerCheckIcon} copied
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Root>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ShareableLinkDialog__description">
|
<div className="ShareableLinkDialog__description">
|
||||||
🔒 {t("alerts.uploadedSecurly")}
|
🔒 {t("alerts.uploadedSecurly")}
|
||||||
|
@ -6,16 +6,18 @@ const Spinner = ({
|
|||||||
size = "1em",
|
size = "1em",
|
||||||
circleWidth = 8,
|
circleWidth = 8,
|
||||||
synchronized = false,
|
synchronized = false,
|
||||||
|
className = "",
|
||||||
}: {
|
}: {
|
||||||
size?: string | number;
|
size?: string | number;
|
||||||
circleWidth?: number;
|
circleWidth?: number;
|
||||||
synchronized?: boolean;
|
synchronized?: boolean;
|
||||||
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const mountTime = React.useRef(Date.now());
|
const mountTime = React.useRef(Date.now());
|
||||||
const mountDelay = -(mountTime.current % 1600);
|
const mountDelay = -(mountTime.current % 1600);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Spinner">
|
<div className={`Spinner ${className}`}>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
style={{
|
style={{
|
||||||
|
@ -129,8 +129,14 @@
|
|||||||
--color-muted-background-darker: var(--color-gray-100);
|
--color-muted-background-darker: var(--color-gray-100);
|
||||||
|
|
||||||
--color-promo: var(--color-primary);
|
--color-promo: var(--color-primary);
|
||||||
--color-success: #268029;
|
|
||||||
--color-success-lighter: #cafccc;
|
--color-success: #cafccc;
|
||||||
|
--color-success-darker: #bafabc;
|
||||||
|
--color-success-darkest: #a5eba8;
|
||||||
|
--color-success-text: #268029;
|
||||||
|
--color-success-contrast: #65bb6a;
|
||||||
|
--color-success-contrast-hover: #6bcf70;
|
||||||
|
--color-success-contrast-active: #6edf74;
|
||||||
|
|
||||||
--color-logo-icon: var(--color-primary);
|
--color-logo-icon: var(--color-primary);
|
||||||
--color-logo-text: #190064;
|
--color-logo-text: #190064;
|
||||||
|
22
packages/excalidraw/hooks/useCopiedIndicator.ts
Normal file
22
packages/excalidraw/hooks/useCopiedIndicator.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
|
const TIMEOUT = 2000;
|
||||||
|
|
||||||
|
export const useCopyStatus = () => {
|
||||||
|
const [copyStatus, setCopyStatus] = useState<"success" | null>(null);
|
||||||
|
const timeoutRef = useRef<number>(0);
|
||||||
|
|
||||||
|
const onCopy = () => {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
setCopyStatus("success");
|
||||||
|
|
||||||
|
timeoutRef.current = window.setTimeout(() => {
|
||||||
|
setCopyStatus(null);
|
||||||
|
}, TIMEOUT);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
copyStatus,
|
||||||
|
onCopy,
|
||||||
|
};
|
||||||
|
};
|
@ -168,6 +168,7 @@
|
|||||||
"exportImage": "Export image...",
|
"exportImage": "Export image...",
|
||||||
"export": "Save to...",
|
"export": "Save to...",
|
||||||
"copyToClipboard": "Copy to clipboard",
|
"copyToClipboard": "Copy to clipboard",
|
||||||
|
"copyLink": "Copy link",
|
||||||
"save": "Save to current file",
|
"save": "Save to current file",
|
||||||
"saveAs": "Save as",
|
"saveAs": "Save as",
|
||||||
"load": "Open",
|
"load": "Open",
|
||||||
|
Loading…
Reference in New Issue
Block a user