From d2b698093cace836fd50273874b179b872c40b0e Mon Sep 17 00:00:00 2001 From: David Luzar Date: Sun, 22 Jan 2023 12:33:15 +0100 Subject: [PATCH] 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 --- src/actions/actionHistory.tsx | 3 ++- src/actions/actionZindex.tsx | 3 ++- src/actions/shortcuts.ts | 2 +- src/clipboard.ts | 9 ++++----- src/components/App.tsx | 2 +- src/components/HelpDialog.tsx | 16 +++++++++++----- src/components/ImageExportDialog.tsx | 6 ++++-- src/constants.ts | 8 ++++++++ src/data/index.ts | 15 +++++++++++++-- src/keys.ts | 4 +--- src/locales/en.json | 3 ++- src/utils.ts | 2 +- 12 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/actions/actionHistory.tsx b/src/actions/actionHistory.tsx index 76aa9d78..2e0f4c09 100644 --- a/src/actions/actionHistory.tsx +++ b/src/actions/actionHistory.tsx @@ -5,10 +5,11 @@ import { t } from "../i18n"; import History, { HistoryEntry } from "../history"; import { ExcalidrawElement } from "../element/types"; import { AppState } from "../types"; -import { isWindows, KEYS } from "../keys"; +import { KEYS } from "../keys"; import { newElementWith } from "../element/mutateElement"; import { fixBindingsAfterDeletion } from "../element/binding"; import { arrayToMap } from "../utils"; +import { isWindows } from "../constants"; const writeData = ( prevElements: readonly ExcalidrawElement[], diff --git a/src/actions/actionZindex.tsx b/src/actions/actionZindex.tsx index 07da2621..17ecde1a 100644 --- a/src/actions/actionZindex.tsx +++ b/src/actions/actionZindex.tsx @@ -5,7 +5,7 @@ import { moveAllLeft, moveAllRight, } from "../zindex"; -import { KEYS, isDarwin, CODES } from "../keys"; +import { KEYS, CODES } from "../keys"; import { t } from "../i18n"; import { getShortcutKey } from "../utils"; import { register } from "./register"; @@ -15,6 +15,7 @@ import { SendBackwardIcon, SendToBackIcon, } from "../components/icons"; +import { isDarwin } from "../constants"; export const actionSendBackward = register({ name: "sendBackward", diff --git a/src/actions/shortcuts.ts b/src/actions/shortcuts.ts index 41686e52..4138ae08 100644 --- a/src/actions/shortcuts.ts +++ b/src/actions/shortcuts.ts @@ -1,5 +1,5 @@ +import { isDarwin } from "../constants"; import { t } from "../i18n"; -import { isDarwin } from "../keys"; import { getShortcutKey } from "../utils"; import { ActionName } from "./types"; diff --git a/src/clipboard.ts b/src/clipboard.ts index bf90a4b1..5f7950c5 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -180,16 +180,16 @@ export const parseClipboard = async ( }; export const copyBlobToClipboardAsPng = async (blob: Blob | Promise) => { - let promise; try { // in Safari so far we need to construct the ClipboardItem synchronously // (i.e. in the same tick) otherwise browser will complain for lack of // user intent. Using a Promise ClipboardItem constructor solves this. // https://bugs.webkit.org/show_bug.cgi?id=222262 // - // not await so that we can detect whether the thrown error likely relates - // to a lack of support for the Promise ClipboardItem constructor - promise = navigator.clipboard.write([ + // Note that Firefox (and potentially others) seems to support Promise + // ClipboardItem constructor, but throws on an unrelated MIME type error. + // So we need to await this and fallback to awaiting the blob if applicable. + await navigator.clipboard.write([ new window.ClipboardItem({ [MIME_TYPES.png]: blob, }), @@ -207,7 +207,6 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise) => { throw error; } } - await promise; }; export const copyTextToSystemClipboard = async (text: string | null) => { diff --git a/src/components/App.tsx b/src/components/App.tsx index e0ce0ee9..0f4b6ce0 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -57,6 +57,7 @@ import { EVENT, GRID_SIZE, IMAGE_RENDER_TIMEOUT, + isAndroid, LINE_CONFIRM_THRESHOLD, MAX_ALLOWED_FILE_BYTES, MIME_TYPES, @@ -166,7 +167,6 @@ import { shouldRotateWithDiscreteAngle, isArrowKey, KEYS, - isAndroid, } from "../keys"; import { distance2d, getGridPoint, isPathALoop } from "../math"; import { renderScene } from "../renderer/renderScene"; diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index 065d0f77..4eea5d69 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -1,10 +1,12 @@ import React from "react"; import { t } from "../i18n"; -import { isDarwin, isWindows, KEYS } from "../keys"; +import { KEYS } from "../keys"; import { Dialog } from "./Dialog"; import { getShortcutKey } from "../utils"; import "./HelpDialog.scss"; import { ExternalLinkIcon } from "./icons"; +import { probablySupportsClipboardBlob } from "../clipboard"; +import { isDarwin, isFirefox, isWindows } from "../constants"; const Header = () => (
@@ -304,10 +306,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => { label={t("labels.pasteAsPlaintext")} shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]} /> - + {/* firefox supports clipboard API under a flag, so we'll + show users what they can do in the error message */} + {(probablySupportsClipboardBlob || isFirefox) && ( + + )} SVG - {probablySupportsClipboardBlob && ( + {/* firefox supports clipboard API under a flag, + so let's throw and tell people what they can do */} + {(probablySupportsClipboardBlob || isFirefox) && ( onExportToClipboard(exportedElements)} diff --git a/src/constants.ts b/src/constants.ts index d6b744bb..20b3778e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,6 +2,14 @@ import cssVariables from "./css/variables.module.scss"; import { AppProps } from "./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 DRAGGING_THRESHOLD = 10; // px diff --git a/src/data/index.ts b/src/data/index.ts index 8ea90adf..89877aab 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -2,7 +2,7 @@ import { copyBlobToClipboardAsPng, copyTextToSystemClipboard, } 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 { t } from "../i18n"; import { exportToCanvas, exportToSvg } from "../scene/export"; @@ -97,10 +97,21 @@ export const exportCanvas = async ( const blob = canvasToBlob(tempCanvas); await copyBlobToClipboardAsPng(blob); } catch (error: any) { + console.warn(error); if (error.name === "CANVAS_POSSIBLY_TOO_BIG") { 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 { tempCanvas.remove(); } diff --git a/src/keys.ts b/src/keys.ts index 0cb2f384..ce0e803c 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -1,6 +1,4 @@ -export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform); -export const isWindows = /^Win/.test(window.navigator.platform); -export const isAndroid = /\b(android)\b/i.test(navigator.userAgent); +import { isDarwin } from "./constants"; export const CODES = { EQUAL: "Equal", diff --git a/src/locales/en.json b/src/locales/en.json index 2fe9e7c8..b0e5a2c5 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -246,7 +246,8 @@ "publishLibrary": "Publish your own library", "bindTextToElement": "Press enter to add text", "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": { "cannotShowPreview": "Cannot show preview", diff --git a/src/utils.ts b/src/utils.ts index d3d80560..a332e921 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,6 +6,7 @@ import { DEFAULT_VERSION, EVENT, FONT_FAMILY, + isDarwin, MIME_TYPES, THEME, WINDOWS_EMOJI_FALLBACK_FONT, @@ -13,7 +14,6 @@ import { import { FontFamilyValues, FontString } from "./element/types"; import { AppState, DataURL, LastActiveToolBeforeEraser, Zoom } from "./types"; import { unstable_batchedUpdates } from "react-dom"; -import { isDarwin } from "./keys"; import { SHAPES } from "./shapes"; import React from "react";