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:
David Luzar 2023-01-22 12:33:15 +01:00 committed by GitHub
parent 0f1720be61
commit d2b698093c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 50 additions and 23 deletions

View File

@ -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[],

View File

@ -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",

View File

@ -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";

View File

@ -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) => {

View File

@ -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";

View File

@ -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")]}

View File

@ -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)}

View File

@ -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

View File

@ -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();
} }

View File

@ -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",

View File

@ -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",

View File

@ -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";