System clipboard (#2117)
This commit is contained in:
parent
950ec66907
commit
47dba05c91
@ -4,19 +4,23 @@ import { AppState } from "./types";
|
|||||||
import { t } from "./i18n";
|
import { t } from "./i18n";
|
||||||
import { DEFAULT_VERTICAL_ALIGN } from "./constants";
|
import { DEFAULT_VERTICAL_ALIGN } from "./constants";
|
||||||
|
|
||||||
interface Spreadsheet {
|
export interface Spreadsheet {
|
||||||
yAxisLabel: string | null;
|
yAxisLabel: string | null;
|
||||||
labels: string[] | null;
|
labels: string[] | null;
|
||||||
values: number[];
|
values: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
|
||||||
|
export const MALFORMED_SPREADSHEET = "MALFORMED_SPREADSHEET";
|
||||||
|
export const VALID_SPREADSHEET = "VALID_SPREADSHEET";
|
||||||
|
|
||||||
type ParseSpreadsheetResult =
|
type ParseSpreadsheetResult =
|
||||||
| {
|
| {
|
||||||
type: "not a spreadsheet";
|
type: typeof NOT_SPREADSHEET;
|
||||||
}
|
}
|
||||||
| { type: "spreadsheet"; spreadsheet: Spreadsheet }
|
| { type: typeof VALID_SPREADSHEET; spreadsheet: Spreadsheet }
|
||||||
| {
|
| {
|
||||||
type: "malformed spreadsheet";
|
type: typeof MALFORMED_SPREADSHEET;
|
||||||
error: string;
|
error: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,12 +42,12 @@ function tryParseCells(cells: string[][]): ParseSpreadsheetResult {
|
|||||||
const numCols = cells[0].length;
|
const numCols = cells[0].length;
|
||||||
|
|
||||||
if (numCols > 2) {
|
if (numCols > 2) {
|
||||||
return { type: "malformed spreadsheet", error: t("charts.tooManyColumns") };
|
return { type: MALFORMED_SPREADSHEET, error: t("charts.tooManyColumns") };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (numCols === 1) {
|
if (numCols === 1) {
|
||||||
if (!isNumericColumn(cells, 0)) {
|
if (!isNumericColumn(cells, 0)) {
|
||||||
return { type: "not a spreadsheet" };
|
return { type: NOT_SPREADSHEET };
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasHeader = tryParseNumber(cells[0][0]) === null;
|
const hasHeader = tryParseNumber(cells[0][0]) === null;
|
||||||
@ -52,11 +56,11 @@ function tryParseCells(cells: string[][]): ParseSpreadsheetResult {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (values.length < 2) {
|
if (values.length < 2) {
|
||||||
return { type: "not a spreadsheet" };
|
return { type: NOT_SPREADSHEET };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "spreadsheet",
|
type: VALID_SPREADSHEET,
|
||||||
spreadsheet: {
|
spreadsheet: {
|
||||||
yAxisLabel: hasHeader ? cells[0][0] : null,
|
yAxisLabel: hasHeader ? cells[0][0] : null,
|
||||||
labels: null,
|
labels: null,
|
||||||
@ -69,7 +73,7 @@ function tryParseCells(cells: string[][]): ParseSpreadsheetResult {
|
|||||||
|
|
||||||
if (!isNumericColumn(cells, valueColumnIndex)) {
|
if (!isNumericColumn(cells, valueColumnIndex)) {
|
||||||
return {
|
return {
|
||||||
type: "malformed spreadsheet",
|
type: MALFORMED_SPREADSHEET,
|
||||||
error: t("charts.noNumericColumn"),
|
error: t("charts.noNumericColumn"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -79,11 +83,11 @@ function tryParseCells(cells: string[][]): ParseSpreadsheetResult {
|
|||||||
const rows = hasHeader ? cells.slice(1) : cells;
|
const rows = hasHeader ? cells.slice(1) : cells;
|
||||||
|
|
||||||
if (rows.length < 2) {
|
if (rows.length < 2) {
|
||||||
return { type: "not a spreadsheet" };
|
return { type: NOT_SPREADSHEET };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "spreadsheet",
|
type: VALID_SPREADSHEET,
|
||||||
spreadsheet: {
|
spreadsheet: {
|
||||||
yAxisLabel: hasHeader ? cells[0][valueColumnIndex] : null,
|
yAxisLabel: hasHeader ? cells[0][valueColumnIndex] : null,
|
||||||
labels: rows.map((row) => row[labelColumnIndex]),
|
labels: rows.map((row) => row[labelColumnIndex]),
|
||||||
@ -114,7 +118,7 @@ export function tryParseSpreadsheet(text: string): ParseSpreadsheetResult {
|
|||||||
.map((line) => line.trim().split("\t"));
|
.map((line) => line.trim().split("\t"));
|
||||||
|
|
||||||
if (lines.length === 0) {
|
if (lines.length === 0) {
|
||||||
return { type: "not a spreadsheet" };
|
return { type: NOT_SPREADSHEET };
|
||||||
}
|
}
|
||||||
|
|
||||||
const numColsFirstLine = lines[0].length;
|
const numColsFirstLine = lines[0].length;
|
||||||
@ -123,13 +127,13 @@ export function tryParseSpreadsheet(text: string): ParseSpreadsheetResult {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!isASpreadsheet) {
|
if (!isASpreadsheet) {
|
||||||
return { type: "not a spreadsheet" };
|
return { type: NOT_SPREADSHEET };
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = tryParseCells(lines);
|
const result = tryParseCells(lines);
|
||||||
if (result.type !== "spreadsheet") {
|
if (result.type !== VALID_SPREADSHEET) {
|
||||||
const transposedResults = tryParseCells(transposeCells(lines));
|
const transposedResults = tryParseCells(transposeCells(lines));
|
||||||
if (transposedResults.type === "spreadsheet") {
|
if (transposedResults.type === VALID_SPREADSHEET) {
|
||||||
return transposedResults;
|
return transposedResults;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
165
src/clipboard.ts
165
src/clipboard.ts
@ -5,7 +5,20 @@ import {
|
|||||||
import { getSelectedElements } from "./scene";
|
import { getSelectedElements } from "./scene";
|
||||||
import { AppState } from "./types";
|
import { AppState } from "./types";
|
||||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||||
import { tryParseSpreadsheet, renderSpreadsheet } from "./charts";
|
import {
|
||||||
|
tryParseSpreadsheet,
|
||||||
|
Spreadsheet,
|
||||||
|
VALID_SPREADSHEET,
|
||||||
|
MALFORMED_SPREADSHEET,
|
||||||
|
} from "./charts";
|
||||||
|
|
||||||
|
const TYPE_ELEMENTS = "excalidraw/elements";
|
||||||
|
|
||||||
|
type ElementsClipboard = {
|
||||||
|
type: typeof TYPE_ELEMENTS;
|
||||||
|
created: number;
|
||||||
|
elements: ExcalidrawElement[];
|
||||||
|
};
|
||||||
|
|
||||||
let CLIPBOARD = "";
|
let CLIPBOARD = "";
|
||||||
let PREFER_APP_CLIPBOARD = false;
|
let PREFER_APP_CLIPBOARD = false;
|
||||||
@ -22,86 +35,126 @@ export const probablySupportsClipboardBlob =
|
|||||||
"ClipboardItem" in window &&
|
"ClipboardItem" in window &&
|
||||||
"toBlob" in HTMLCanvasElement.prototype;
|
"toBlob" in HTMLCanvasElement.prototype;
|
||||||
|
|
||||||
export const copyToAppClipboard = async (
|
const isElementsClipboard = (contents: any): contents is ElementsClipboard => {
|
||||||
|
if (contents?.type === TYPE_ELEMENTS) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const copyToClipboard = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
CLIPBOARD = JSON.stringify(getSelectedElements(elements, appState));
|
const contents: ElementsClipboard = {
|
||||||
|
type: TYPE_ELEMENTS,
|
||||||
|
created: Date.now(),
|
||||||
|
elements: getSelectedElements(elements, appState),
|
||||||
|
};
|
||||||
|
const json = JSON.stringify(contents);
|
||||||
|
CLIPBOARD = json;
|
||||||
try {
|
try {
|
||||||
// when copying to in-app clipboard, clear system clipboard so that if
|
|
||||||
// system clip contains text on paste we know it was copied *after* user
|
|
||||||
// copied elements, and thus we should prefer the text content.
|
|
||||||
await copyTextToSystemClipboard(null);
|
|
||||||
PREFER_APP_CLIPBOARD = false;
|
PREFER_APP_CLIPBOARD = false;
|
||||||
} catch {
|
await copyTextToSystemClipboard(json);
|
||||||
// if clearing system clipboard didn't work, we should prefer in-app
|
} catch (err) {
|
||||||
// clipboard even if there's text in system clipboard on paste, because
|
|
||||||
// we can't be sure of the order of copy operations
|
|
||||||
PREFER_APP_CLIPBOARD = true;
|
PREFER_APP_CLIPBOARD = true;
|
||||||
|
console.error(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAppClipboard = (): {
|
const getAppClipboard = (): Partial<ElementsClipboard> => {
|
||||||
elements?: readonly ExcalidrawElement[];
|
|
||||||
} => {
|
|
||||||
if (!CLIPBOARD) {
|
if (!CLIPBOARD) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const clipboardElements = JSON.parse(CLIPBOARD);
|
return JSON.parse(CLIPBOARD);
|
||||||
|
|
||||||
if (
|
|
||||||
Array.isArray(clipboardElements) &&
|
|
||||||
clipboardElements.length > 0 &&
|
|
||||||
clipboardElements[0].type // need to implement a better check here...
|
|
||||||
) {
|
|
||||||
return { elements: clipboardElements };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getClipboardContent = async (
|
const parsePotentialSpreadsheet = (
|
||||||
appState: AppState,
|
text: string,
|
||||||
cursorX: number,
|
): { spreadsheet: Spreadsheet } | { errorMessage: string } | null => {
|
||||||
cursorY: number,
|
const result = tryParseSpreadsheet(text);
|
||||||
|
if (result.type === VALID_SPREADSHEET) {
|
||||||
|
return { spreadsheet: result.spreadsheet };
|
||||||
|
} else if (result.type === MALFORMED_SPREADSHEET) {
|
||||||
|
return { errorMessage: result.error };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves content from system clipboard (either from ClipboardEvent or
|
||||||
|
* via async clipboard API if supported)
|
||||||
|
*/
|
||||||
|
const getSystemClipboard = async (
|
||||||
event: ClipboardEvent | null,
|
event: ClipboardEvent | null,
|
||||||
): Promise<{
|
): Promise<string> => {
|
||||||
text?: string;
|
|
||||||
elements?: readonly ExcalidrawElement[];
|
|
||||||
error?: string;
|
|
||||||
}> => {
|
|
||||||
try {
|
try {
|
||||||
const text = event
|
const text = event
|
||||||
? event.clipboardData?.getData("text/plain").trim()
|
? event.clipboardData?.getData("text/plain").trim()
|
||||||
: probablySupportsClipboardReadText &&
|
: probablySupportsClipboardReadText &&
|
||||||
(await navigator.clipboard.readText());
|
(await navigator.clipboard.readText());
|
||||||
|
|
||||||
if (text && !PREFER_APP_CLIPBOARD && !text.includes(SVG_EXPORT_TAG)) {
|
return text || "";
|
||||||
const result = tryParseSpreadsheet(text);
|
} catch {
|
||||||
if (result.type === "spreadsheet") {
|
return "";
|
||||||
return {
|
|
||||||
elements: renderSpreadsheet(
|
|
||||||
appState,
|
|
||||||
result.spreadsheet,
|
|
||||||
cursorX,
|
|
||||||
cursorY,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
} else if (result.type === "malformed spreadsheet") {
|
|
||||||
return { error: result.error };
|
|
||||||
}
|
}
|
||||||
return { text };
|
};
|
||||||
}
|
|
||||||
} catch (error) {
|
/**
|
||||||
console.error(error);
|
* Attemps to parse clipboard. Prefers system clipboard.
|
||||||
|
*/
|
||||||
|
export const parseClipboard = async (
|
||||||
|
event: ClipboardEvent | null,
|
||||||
|
): Promise<{
|
||||||
|
spreadsheet?: Spreadsheet;
|
||||||
|
elements?: readonly ExcalidrawElement[];
|
||||||
|
text?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
}> => {
|
||||||
|
const systemClipboard = await getSystemClipboard(event);
|
||||||
|
|
||||||
|
// if system clipboard empty, couldn't be resolved, or contains previously
|
||||||
|
// copied excalidraw scene as SVG, fall back to previously copied excalidraw
|
||||||
|
// elements
|
||||||
|
if (!systemClipboard || systemClipboard.includes(SVG_EXPORT_TAG)) {
|
||||||
|
return getAppClipboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
return getAppClipboard();
|
// if system clipboard contains spreadsheet, use it even though it's
|
||||||
|
// technically possible it's staler than in-app clipboard
|
||||||
|
const spreadsheetResult = parsePotentialSpreadsheet(systemClipboard);
|
||||||
|
if (spreadsheetResult) {
|
||||||
|
return spreadsheetResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appClipboardData = getAppClipboard();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const systemClipboardData = JSON.parse(systemClipboard);
|
||||||
|
// system clipboard elements are newer than in-app clipboard
|
||||||
|
if (
|
||||||
|
isElementsClipboard(systemClipboardData) &&
|
||||||
|
(!appClipboardData?.created ||
|
||||||
|
appClipboardData.created < systemClipboardData.created)
|
||||||
|
) {
|
||||||
|
return { elements: systemClipboardData.elements };
|
||||||
|
}
|
||||||
|
// in-app clipboard is newer than system clipboard
|
||||||
|
return appClipboardData;
|
||||||
|
} catch {
|
||||||
|
// system clipboard doesn't contain excalidraw elements → return plaintext
|
||||||
|
// unless we set a flag to prefer in-app clipboard because browser didn't
|
||||||
|
// support storing to system clipboard on copy
|
||||||
|
return PREFER_APP_CLIPBOARD && appClipboardData.elements
|
||||||
|
? appClipboardData
|
||||||
|
: { text: systemClipboard };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) =>
|
export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) =>
|
||||||
@ -122,14 +175,6 @@ export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const copyCanvasToClipboardAsSvg = async (svgroot: SVGSVGElement) => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(svgroot.outerHTML);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const copyTextToSystemClipboard = async (text: string | null) => {
|
export const copyTextToSystemClipboard = async (text: string | null) => {
|
||||||
let copied = false;
|
let copied = false;
|
||||||
if (probablySupportsClipboardWriteText) {
|
if (probablySupportsClipboardWriteText) {
|
||||||
|
@ -100,8 +100,8 @@ import { getDefaultAppState } from "../appState";
|
|||||||
import { t, getLanguage } from "../i18n";
|
import { t, getLanguage } from "../i18n";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
copyToAppClipboard,
|
copyToClipboard,
|
||||||
getClipboardContent,
|
parseClipboard,
|
||||||
probablySupportsClipboardBlob,
|
probablySupportsClipboardBlob,
|
||||||
probablySupportsClipboardWriteText,
|
probablySupportsClipboardWriteText,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
@ -174,6 +174,7 @@ import {
|
|||||||
shouldEnableBindingForPointerEvent,
|
shouldEnableBindingForPointerEvent,
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { MaybeTransformHandleType } from "../element/transformHandles";
|
import { MaybeTransformHandleType } from "../element/transformHandles";
|
||||||
|
import { renderSpreadsheet } from "../charts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param func handler taking at most single parameter (event).
|
* @param func handler taking at most single parameter (event).
|
||||||
@ -872,7 +873,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
private copyAll = () => {
|
private copyAll = () => {
|
||||||
copyToAppClipboard(this.scene.getElements(), this.state);
|
copyToClipboard(this.scene.getElements(), this.state);
|
||||||
};
|
};
|
||||||
|
|
||||||
private copyToClipboardAsPng = () => {
|
private copyToClipboardAsPng = () => {
|
||||||
@ -960,14 +961,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await getClipboardContent(
|
const data = await parseClipboard(event);
|
||||||
this.state,
|
if (data.errorMessage) {
|
||||||
cursorX,
|
this.setState({ errorMessage: data.errorMessage });
|
||||||
cursorY,
|
} else if (data.spreadsheet) {
|
||||||
event,
|
this.addElementsFromPasteOrLibrary(
|
||||||
|
renderSpreadsheet(this.state, data.spreadsheet, cursorX, cursorY),
|
||||||
);
|
);
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else if (data.elements) {
|
} else if (data.elements) {
|
||||||
this.addElementsFromPasteOrLibrary(data.elements);
|
this.addElementsFromPasteOrLibrary(data.elements);
|
||||||
} else if (data.text) {
|
} else if (data.text) {
|
||||||
|
@ -371,6 +371,9 @@ const LayerUI = ({
|
|||||||
onUsernameChange={onUsernameChange}
|
onUsernameChange={onUsernameChange}
|
||||||
onRoomCreate={onRoomCreate}
|
onRoomCreate={onRoomCreate}
|
||||||
onRoomDestroy={onRoomDestroy}
|
onRoomDestroy={onRoomDestroy}
|
||||||
|
setErrorMessage={(message: string) =>
|
||||||
|
setAppState({ errorMessage: message })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
<BackgroundPickerAndDarkModeToggle
|
<BackgroundPickerAndDarkModeToggle
|
||||||
|
@ -100,6 +100,9 @@ export const MobileMenu = ({
|
|||||||
onUsernameChange={onUsernameChange}
|
onUsernameChange={onUsernameChange}
|
||||||
onRoomCreate={onRoomCreate}
|
onRoomCreate={onRoomCreate}
|
||||||
onRoomDestroy={onRoomDestroy}
|
onRoomDestroy={onRoomDestroy}
|
||||||
|
setErrorMessage={(message: string) =>
|
||||||
|
setAppState({ errorMessage: message })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<BackgroundPickerAndDarkModeToggle
|
<BackgroundPickerAndDarkModeToggle
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
|
@ -16,6 +16,7 @@ const RoomModal = ({
|
|||||||
onRoomCreate,
|
onRoomCreate,
|
||||||
onRoomDestroy,
|
onRoomDestroy,
|
||||||
onPressingEnter,
|
onPressingEnter,
|
||||||
|
setErrorMessage,
|
||||||
}: {
|
}: {
|
||||||
activeRoomLink: string;
|
activeRoomLink: string;
|
||||||
username: string;
|
username: string;
|
||||||
@ -23,11 +24,16 @@ const RoomModal = ({
|
|||||||
onRoomCreate: () => void;
|
onRoomCreate: () => void;
|
||||||
onRoomDestroy: () => void;
|
onRoomDestroy: () => void;
|
||||||
onPressingEnter: () => void;
|
onPressingEnter: () => void;
|
||||||
|
setErrorMessage: (message: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const roomLinkInput = useRef<HTMLInputElement>(null);
|
const roomLinkInput = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const copyRoomLink = () => {
|
const copyRoomLink = async () => {
|
||||||
copyTextToSystemClipboard(activeRoomLink);
|
try {
|
||||||
|
await copyTextToSystemClipboard(activeRoomLink);
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMessage(error.message);
|
||||||
|
}
|
||||||
if (roomLinkInput.current) {
|
if (roomLinkInput.current) {
|
||||||
roomLinkInput.current.select();
|
roomLinkInput.current.select();
|
||||||
}
|
}
|
||||||
@ -127,6 +133,7 @@ export const RoomDialog = ({
|
|||||||
onUsernameChange,
|
onUsernameChange,
|
||||||
onRoomCreate,
|
onRoomCreate,
|
||||||
onRoomDestroy,
|
onRoomDestroy,
|
||||||
|
setErrorMessage,
|
||||||
}: {
|
}: {
|
||||||
isCollaborating: AppState["isCollaborating"];
|
isCollaborating: AppState["isCollaborating"];
|
||||||
collaboratorCount: number;
|
collaboratorCount: number;
|
||||||
@ -134,6 +141,7 @@ export const RoomDialog = ({
|
|||||||
onUsernameChange: (username: string) => void;
|
onUsernameChange: (username: string) => void;
|
||||||
onRoomCreate: () => void;
|
onRoomCreate: () => void;
|
||||||
onRoomDestroy: () => void;
|
onRoomDestroy: () => void;
|
||||||
|
setErrorMessage: (message: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [modalIsShown, setModalIsShown] = useState(false);
|
const [modalIsShown, setModalIsShown] = useState(false);
|
||||||
const [activeRoomLink, setActiveRoomLink] = useState("");
|
const [activeRoomLink, setActiveRoomLink] = useState("");
|
||||||
@ -182,6 +190,7 @@ export const RoomDialog = ({
|
|||||||
onRoomCreate={onRoomCreate}
|
onRoomCreate={onRoomCreate}
|
||||||
onRoomDestroy={onRoomDestroy}
|
onRoomDestroy={onRoomDestroy}
|
||||||
onPressingEnter={handleClose}
|
onPressingEnter={handleClose}
|
||||||
|
setErrorMessage={setErrorMessage}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
|
@ -12,7 +12,7 @@ import { fileSave } from "browser-nativefs";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import {
|
import {
|
||||||
copyCanvasToClipboardAsPng,
|
copyCanvasToClipboardAsPng,
|
||||||
copyCanvasToClipboardAsSvg,
|
copyTextToSystemClipboard,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import { serializeAsJSON } from "./json";
|
import { serializeAsJSON } from "./json";
|
||||||
|
|
||||||
@ -317,7 +317,7 @@ export const exportCanvas = async (
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} else if (type === "clipboard-svg") {
|
} else if (type === "clipboard-svg") {
|
||||||
copyCanvasToClipboardAsSvg(tempSvg);
|
copyTextToSystemClipboard(tempSvg.outerHTML);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user