1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2024-11-02 03:25:53 +01:00

Implement Save without re-prompt and Save as (#1709)

* Implement Save without re-prompt and Save as
Fixes #1668

* Add save-as icon

* Make .excalidraw the default extension

* Only show save as button on supporting browsers
This commit is contained in:
Thomas Steiner 2020-06-12 18:35:04 +02:00 committed by GitHub
parent 0ed6a96b6a
commit 5d3867d8ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 74 additions and 26 deletions

6
package-lock.json generated

@ -3306,9 +3306,9 @@
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
}, },
"browser-nativefs": { "browser-nativefs": {
"version": "0.8.1", "version": "0.8.2",
"resolved": "https://registry.npmjs.org/browser-nativefs/-/browser-nativefs-0.8.1.tgz", "resolved": "https://registry.npmjs.org/browser-nativefs/-/browser-nativefs-0.8.2.tgz",
"integrity": "sha512-5XQTR6eg+/hDBVoOKbCnCqUzhD7IP5RG6jCe+J+EaTHo8EnDxjEj3mod3BiEBc/4NfTLEMbrMzUPPY64KwnmNw==" "integrity": "sha512-x1dYA6lkpaLZcvvbQ1+/SSDR9H/fbzlcnKi3BDCvEe3fr3HzV5finUMX8fJspzCmPuP7fGLVO8S3UZ8RhQseFw=="
}, },
"browser-process-hrtime": { "browser-process-hrtime": {
"version": "1.0.0", "version": "1.0.0",

@ -28,7 +28,7 @@
"@types/react": "16.9.35", "@types/react": "16.9.35",
"@types/react-dom": "16.9.8", "@types/react-dom": "16.9.8",
"@types/socket.io-client": "1.4.33", "@types/socket.io-client": "1.4.33",
"browser-nativefs": "0.8.1", "browser-nativefs": "0.8.2",
"i18next-browser-languagedetector": "4.2.0", "i18next-browser-languagedetector": "4.2.0",
"lodash.throttle": "4.1.1", "lodash.throttle": "4.1.1",
"nanoid": "2.1.11", "nanoid": "2.1.11",

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { ProjectName } from "../components/ProjectName"; import { ProjectName } from "../components/ProjectName";
import { saveAsJSON, loadFromJSON } from "../data"; import { saveAsJSON, loadFromJSON } from "../data";
import { load, save } from "../components/icons"; import { load, save, saveAs } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import useIsMobile from "../is-mobile";
@ -65,11 +65,13 @@ export const actionChangeShouldAddWatermark = register({
export const actionSaveScene = register({ export const actionSaveScene = register({
name: "saveScene", name: "saveScene",
perform: (elements, appState, value) => { perform: (elements, appState, value) => {
saveAsJSON(elements, appState).catch((error) => console.error(error)); saveAsJSON(elements, appState, (window as any).handle).catch((error) =>
console.error(error),
);
return { commitToHistory: false }; return { commitToHistory: false };
}, },
keyTest: (event) => { keyTest: (event) => {
return event.key === "s" && event[KEYS.CTRL_OR_CMD]; return event.key === "s" && event[KEYS.CTRL_OR_CMD] && !event.shiftKey;
}, },
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
<ToolButton <ToolButton
@ -83,6 +85,28 @@ export const actionSaveScene = register({
), ),
}); });
export const actionSaveAsScene = register({
name: "saveAsScene",
perform: (elements, appState, value) => {
saveAsJSON(elements, appState, null).catch((error) => console.error(error));
return { commitToHistory: false };
},
keyTest: (event) => {
return event.key === "s" && event.shiftKey && event[KEYS.CTRL_OR_CMD];
},
PanelComponent: ({ updateData }) => (
<ToolButton
type="button"
icon={saveAs}
title={t("buttons.saveAs")}
aria-label={t("buttons.saveAs")}
showAriaLabel={useIsMobile()}
hidden={!("chooseFileSystemEntries" in window)}
onClick={() => updateData(null)}
/>
),
});
export const actionLoadScene = register({ export const actionLoadScene = register({
name: "loadScene", name: "loadScene",
perform: ( perform: (

@ -34,6 +34,7 @@ export {
actionChangeProjectName, actionChangeProjectName,
actionChangeExportBackground, actionChangeExportBackground,
actionSaveScene, actionSaveScene,
actionSaveAsScene,
actionLoadScene, actionLoadScene,
} from "./actionExport"; } from "./actionExport";

@ -43,6 +43,7 @@ export type ActionName =
| "changeExportBackground" | "changeExportBackground"
| "changeShouldAddWatermark" | "changeShouldAddWatermark"
| "saveScene" | "saveScene"
| "saveAsScene"
| "loadScene" | "loadScene"
| "duplicateSelection" | "duplicateSelection"
| "deleteSelectedElements" | "deleteSelectedElements"

@ -130,6 +130,7 @@ const LayerUI = ({
<Stack.Row gap={1} justifyContent="space-between"> <Stack.Row gap={1} justifyContent="space-between">
{actionManager.renderAction("loadScene")} {actionManager.renderAction("loadScene")}
{actionManager.renderAction("saveScene")} {actionManager.renderAction("saveScene")}
{actionManager.renderAction("saveAsScene")}
{renderExportDialog()} {renderExportDialog()}
{actionManager.renderAction("clearCanvas")} {actionManager.renderAction("clearCanvas")}
<RoomDialog <RoomDialog

@ -84,6 +84,7 @@ export const MobileMenu = ({
<Stack.Col gap={4}> <Stack.Col gap={4}>
{actionManager.renderAction("loadScene")} {actionManager.renderAction("loadScene")}
{actionManager.renderAction("saveScene")} {actionManager.renderAction("saveScene")}
{actionManager.renderAction("saveAsScene")}
{exportButton} {exportButton}
{actionManager.renderAction("clearCanvas")} {actionManager.renderAction("clearCanvas")}
<RoomDialog <RoomDialog

@ -16,6 +16,7 @@ type ToolButtonBaseProps = {
size?: ToolIconSize; size?: ToolIconSize;
keyBindingLabel?: string; keyBindingLabel?: string;
showAriaLabel?: boolean; showAriaLabel?: boolean;
hidden?: boolean;
visible?: boolean; visible?: boolean;
selected?: boolean; selected?: boolean;
className?: string; className?: string;
@ -44,13 +45,16 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
if (props.type === "button") { if (props.type === "button") {
return ( return (
<button <button
className={`ToolIcon_type_button ToolIcon ${sizeCn}${ className={`ToolIcon_type_button ${
props.selected ? " ToolIcon--selected" : "" !props.hidden ? "ToolIcon" : ""
} ${props.className} ${ } ${sizeCn}${props.selected ? " ToolIcon--selected" : ""} ${
props.visible props.className
} ${
props.visible && !props.hidden
? "ToolIcon_type_button--show" ? "ToolIcon_type_button--show"
: "ToolIcon_type_button--hide" : "ToolIcon_type_button--hide"
}`} }`}
hidden={props.hidden}
title={props.title} title={props.title}
aria-label={props["aria-label"]} aria-label={props["aria-label"]}
type="button" type="button"

@ -39,6 +39,11 @@ export const save = createIcon(
{ width: 448, height: 512 }, { width: 448, height: 512 },
); );
export const saveAs = createIcon(
"M252 54L203 8a28 27 0 00-20-8H28C12 0 0 12 0 27v195c0 15 12 26 28 26h204c15 0 28-11 28-26V73a28 27 0 00-8-19zM130 213c-21 0-37-16-37-36 0-19 16-35 37-35 20 0 37 16 37 35 0 20-17 36-37 36zm56-169v56c0 4-4 6-7 6H44c-4 0-7-2-7-6V42c0-4 3-7 7-7h133l4 2 3 2a7 7 0 012 5z M296 201l87 95-188 205-78 9c-10 1-19-8-18-20l9-84zm141-14l-41-44a31 31 0 00-46 0l-38 41 87 95 38-42c13-14 13-36 0-50z",
{ width: 448, height: 512 },
);
export const load = createIcon( export const load = createIcon(
"M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z", "M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z",
{ width: 576, height: 512, mirror: true }, { width: 576, height: 512, mirror: true },

@ -24,28 +24,38 @@ export const serializeAsJSON = (
export const saveAsJSON = async ( export const saveAsJSON = async (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
fileHandle: any,
) => { ) => {
const serialized = serializeAsJSON(elements, appState); const serialized = serializeAsJSON(elements, appState);
const blob = new Blob([serialized], {
const name = `${appState.name}.excalidraw`; type: "application/json",
await fileSave( });
new Blob([serialized], { // Either "Save as" or non-supporting browser
type: /\b(iPad|iPhone|iPod)\b/.test(navigator.userAgent) if (!fileHandle) {
? "application/json" const name = `${appState.name}.excalidraw`;
: "application/vnd.excalidraw+json", const handle = await fileSave(
}), blob,
{ {
fileName: name, fileName: name,
description: "Excalidraw file", description: "Excalidraw file",
}, extensions: ["excalidraw"],
(window as any).handle, },
); fileHandle,
);
(window as any).handle = handle;
return;
}
// "Save"
const writable = await fileHandle.createWritable();
await writable.write(blob);
await writable.close();
}; };
export const loadFromJSON = async () => { export const loadFromJSON = async () => {
const blob = await fileOpen({ const blob = await fileOpen({
description: "Excalidraw files", description: "Excalidraw files",
extensions: ["json", "excalidraw"], extensions: ["json", "excalidraw"],
mimeTypes: ["application/json", "application/vnd.excalidraw+json"], mimeTypes: ["application/json"],
}); });
return loadFromBlob(blob); return loadFromBlob(blob);
}; };

@ -71,6 +71,7 @@
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copy to clipboard",
"copyPngToClipboard": "Copy PNG to clipboard", "copyPngToClipboard": "Copy PNG to clipboard",
"save": "Save", "save": "Save",
"saveAs": "Save as",
"load": "Load", "load": "Load",
"getShareableLink": "Get shareable link", "getShareableLink": "Get shareable link",
"close": "Close", "close": "Close",