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:
parent
0ed6a96b6a
commit
5d3867d8ac
6
package-lock.json
generated
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",
|
||||||
|
Loading…
Reference in New Issue
Block a user