feat: show copy-as-png export button on firefox and show steps how to enable it (#6125)
* feat: hide copy-as-png shortcut from help dialog if not supported * fix: support firefox if clipboard.write supported * show shrotcut in firefox and instead show error message how to enable the flag support * widen to TypeError because minification * show copy-as-png on firefox even if it will throw
This commit is contained in:
parent
0f1720be61
commit
d2b698093c
@ -5,10 +5,11 @@ import { t } from "../i18n";
|
|||||||
import History, { HistoryEntry } from "../history";
|
import History, { HistoryEntry } from "../history";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isWindows, KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { fixBindingsAfterDeletion } from "../element/binding";
|
import { fixBindingsAfterDeletion } from "../element/binding";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
|
import { isWindows } from "../constants";
|
||||||
|
|
||||||
const writeData = (
|
const writeData = (
|
||||||
prevElements: readonly ExcalidrawElement[],
|
prevElements: readonly ExcalidrawElement[],
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
moveAllLeft,
|
moveAllLeft,
|
||||||
moveAllRight,
|
moveAllRight,
|
||||||
} from "../zindex";
|
} from "../zindex";
|
||||||
import { KEYS, isDarwin, CODES } from "../keys";
|
import { KEYS, CODES } from "../keys";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
SendBackwardIcon,
|
SendBackwardIcon,
|
||||||
SendToBackIcon,
|
SendToBackIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
|
import { isDarwin } from "../constants";
|
||||||
|
|
||||||
export const actionSendBackward = register({
|
export const actionSendBackward = register({
|
||||||
name: "sendBackward",
|
name: "sendBackward",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
import { isDarwin } from "../constants";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isDarwin } from "../keys";
|
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { ActionName } from "./types";
|
import { ActionName } from "./types";
|
||||||
|
|
||||||
|
@ -180,16 +180,16 @@ export const parseClipboard = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||||
let promise;
|
|
||||||
try {
|
try {
|
||||||
// in Safari so far we need to construct the ClipboardItem synchronously
|
// in Safari so far we need to construct the ClipboardItem synchronously
|
||||||
// (i.e. in the same tick) otherwise browser will complain for lack of
|
// (i.e. in the same tick) otherwise browser will complain for lack of
|
||||||
// user intent. Using a Promise ClipboardItem constructor solves this.
|
// user intent. Using a Promise ClipboardItem constructor solves this.
|
||||||
// https://bugs.webkit.org/show_bug.cgi?id=222262
|
// https://bugs.webkit.org/show_bug.cgi?id=222262
|
||||||
//
|
//
|
||||||
// not await so that we can detect whether the thrown error likely relates
|
// Note that Firefox (and potentially others) seems to support Promise
|
||||||
// to a lack of support for the Promise ClipboardItem constructor
|
// ClipboardItem constructor, but throws on an unrelated MIME type error.
|
||||||
promise = navigator.clipboard.write([
|
// So we need to await this and fallback to awaiting the blob if applicable.
|
||||||
|
await navigator.clipboard.write([
|
||||||
new window.ClipboardItem({
|
new window.ClipboardItem({
|
||||||
[MIME_TYPES.png]: blob,
|
[MIME_TYPES.png]: blob,
|
||||||
}),
|
}),
|
||||||
@ -207,7 +207,6 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await promise;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyTextToSystemClipboard = async (text: string | null) => {
|
export const copyTextToSystemClipboard = async (text: string | null) => {
|
||||||
|
@ -57,6 +57,7 @@ import {
|
|||||||
EVENT,
|
EVENT,
|
||||||
GRID_SIZE,
|
GRID_SIZE,
|
||||||
IMAGE_RENDER_TIMEOUT,
|
IMAGE_RENDER_TIMEOUT,
|
||||||
|
isAndroid,
|
||||||
LINE_CONFIRM_THRESHOLD,
|
LINE_CONFIRM_THRESHOLD,
|
||||||
MAX_ALLOWED_FILE_BYTES,
|
MAX_ALLOWED_FILE_BYTES,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
@ -166,7 +167,6 @@ import {
|
|||||||
shouldRotateWithDiscreteAngle,
|
shouldRotateWithDiscreteAngle,
|
||||||
isArrowKey,
|
isArrowKey,
|
||||||
KEYS,
|
KEYS,
|
||||||
isAndroid,
|
|
||||||
} from "../keys";
|
} from "../keys";
|
||||||
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
||||||
import { renderScene } from "../renderer/renderScene";
|
import { renderScene } from "../renderer/renderScene";
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isDarwin, isWindows, KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import "./HelpDialog.scss";
|
import "./HelpDialog.scss";
|
||||||
import { ExternalLinkIcon } from "./icons";
|
import { ExternalLinkIcon } from "./icons";
|
||||||
|
import { probablySupportsClipboardBlob } from "../clipboard";
|
||||||
|
import { isDarwin, isFirefox, isWindows } from "../constants";
|
||||||
|
|
||||||
const Header = () => (
|
const Header = () => (
|
||||||
<div className="HelpDialog__header">
|
<div className="HelpDialog__header">
|
||||||
@ -304,10 +306,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.pasteAsPlaintext")}
|
label={t("labels.pasteAsPlaintext")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
{/* firefox supports clipboard API under a flag, so we'll
|
||||||
label={t("labels.copyAsPng")}
|
show users what they can do in the error message */}
|
||||||
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||||
/>
|
<Shortcut
|
||||||
|
label={t("labels.copyAsPng")}
|
||||||
|
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.copyStyles")}
|
label={t("labels.copyStyles")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
|
||||||
|
@ -12,7 +12,7 @@ import Stack from "./Stack";
|
|||||||
import "./ExportDialog.scss";
|
import "./ExportDialog.scss";
|
||||||
import OpenColor from "open-color";
|
import OpenColor from "open-color";
|
||||||
import { CheckboxItem } from "./CheckboxItem";
|
import { CheckboxItem } from "./CheckboxItem";
|
||||||
import { DEFAULT_EXPORT_PADDING } from "../constants";
|
import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
|
||||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
|
|
||||||
@ -190,7 +190,9 @@ const ImageExportModal = ({
|
|||||||
>
|
>
|
||||||
SVG
|
SVG
|
||||||
</ExportButton>
|
</ExportButton>
|
||||||
{probablySupportsClipboardBlob && (
|
{/* firefox supports clipboard API under a flag,
|
||||||
|
so let's throw and tell people what they can do */}
|
||||||
|
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||||
<ExportButton
|
<ExportButton
|
||||||
title={t("buttons.copyPngToClipboard")}
|
title={t("buttons.copyPngToClipboard")}
|
||||||
onClick={() => onExportToClipboard(exportedElements)}
|
onClick={() => onExportToClipboard(exportedElements)}
|
||||||
|
@ -2,6 +2,14 @@ import cssVariables from "./css/variables.module.scss";
|
|||||||
import { AppProps } from "./types";
|
import { AppProps } from "./types";
|
||||||
import { FontFamilyValues } from "./element/types";
|
import { FontFamilyValues } from "./element/types";
|
||||||
|
|
||||||
|
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||||
|
export const isWindows = /^Win/.test(navigator.platform);
|
||||||
|
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
||||||
|
export const isFirefox =
|
||||||
|
"netscape" in window &&
|
||||||
|
navigator.userAgent.indexOf("rv:") > 1 &&
|
||||||
|
navigator.userAgent.indexOf("Gecko") > 1;
|
||||||
|
|
||||||
export const APP_NAME = "Excalidraw";
|
export const APP_NAME = "Excalidraw";
|
||||||
|
|
||||||
export const DRAGGING_THRESHOLD = 10; // px
|
export const DRAGGING_THRESHOLD = 10; // px
|
||||||
|
@ -2,7 +2,7 @@ import {
|
|||||||
copyBlobToClipboardAsPng,
|
copyBlobToClipboardAsPng,
|
||||||
copyTextToSystemClipboard,
|
copyTextToSystemClipboard,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import { DEFAULT_EXPORT_PADDING, MIME_TYPES } from "../constants";
|
import { DEFAULT_EXPORT_PADDING, isFirefox, MIME_TYPES } from "../constants";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { exportToCanvas, exportToSvg } from "../scene/export";
|
import { exportToCanvas, exportToSvg } from "../scene/export";
|
||||||
@ -97,10 +97,21 @@ export const exportCanvas = async (
|
|||||||
const blob = canvasToBlob(tempCanvas);
|
const blob = canvasToBlob(tempCanvas);
|
||||||
await copyBlobToClipboardAsPng(blob);
|
await copyBlobToClipboardAsPng(blob);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.warn(error);
|
||||||
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
|
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
// TypeError *probably* suggests ClipboardItem not defined, which
|
||||||
|
// people on Firefox can enable through a flag, so let's tell them.
|
||||||
|
if (isFirefox && error.name === "TypeError") {
|
||||||
|
throw new Error(
|
||||||
|
`${t("alerts.couldNotCopyToClipboard")}\n\n${t(
|
||||||
|
"hints.firefox_clipboard_write",
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
tempCanvas.remove();
|
tempCanvas.remove();
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
|
import { isDarwin } from "./constants";
|
||||||
export const isWindows = /^Win/.test(window.navigator.platform);
|
|
||||||
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
|
||||||
|
|
||||||
export const CODES = {
|
export const CODES = {
|
||||||
EQUAL: "Equal",
|
EQUAL: "Equal",
|
||||||
|
@ -246,7 +246,8 @@
|
|||||||
"publishLibrary": "Publish your own library",
|
"publishLibrary": "Publish your own library",
|
||||||
"bindTextToElement": "Press enter to add text",
|
"bindTextToElement": "Press enter to add text",
|
||||||
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
|
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
|
||||||
"eraserRevert": "Hold Alt to revert the elements marked for deletion"
|
"eraserRevert": "Hold Alt to revert the elements marked for deletion",
|
||||||
|
"firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Cannot show preview",
|
"cannotShowPreview": "Cannot show preview",
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
DEFAULT_VERSION,
|
DEFAULT_VERSION,
|
||||||
EVENT,
|
EVENT,
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
|
isDarwin,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
THEME,
|
THEME,
|
||||||
WINDOWS_EMOJI_FALLBACK_FONT,
|
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||||
@ -13,7 +14,6 @@ import {
|
|||||||
import { FontFamilyValues, FontString } from "./element/types";
|
import { FontFamilyValues, FontString } from "./element/types";
|
||||||
import { AppState, DataURL, LastActiveToolBeforeEraser, Zoom } from "./types";
|
import { AppState, DataURL, LastActiveToolBeforeEraser, Zoom } from "./types";
|
||||||
import { unstable_batchedUpdates } from "react-dom";
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
import { isDarwin } from "./keys";
|
|
||||||
import { SHAPES } from "./shapes";
|
import { SHAPES } from "./shapes";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user