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="
|
||||
},
|
||||
"browser-nativefs": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/browser-nativefs/-/browser-nativefs-0.8.1.tgz",
|
||||
"integrity": "sha512-5XQTR6eg+/hDBVoOKbCnCqUzhD7IP5RG6jCe+J+EaTHo8EnDxjEj3mod3BiEBc/4NfTLEMbrMzUPPY64KwnmNw=="
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/browser-nativefs/-/browser-nativefs-0.8.2.tgz",
|
||||
"integrity": "sha512-x1dYA6lkpaLZcvvbQ1+/SSDR9H/fbzlcnKi3BDCvEe3fr3HzV5finUMX8fJspzCmPuP7fGLVO8S3UZ8RhQseFw=="
|
||||
},
|
||||
"browser-process-hrtime": {
|
||||
"version": "1.0.0",
|
||||
|
@ -28,7 +28,7 @@
|
||||
"@types/react": "16.9.35",
|
||||
"@types/react-dom": "16.9.8",
|
||||
"@types/socket.io-client": "1.4.33",
|
||||
"browser-nativefs": "0.8.1",
|
||||
"browser-nativefs": "0.8.2",
|
||||
"i18next-browser-languagedetector": "4.2.0",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"nanoid": "2.1.11",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { ProjectName } from "../components/ProjectName";
|
||||
import { saveAsJSON, loadFromJSON } from "../data";
|
||||
import { load, save } from "../components/icons";
|
||||
import { load, save, saveAs } from "../components/icons";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
@ -65,11 +65,13 @@ export const actionChangeShouldAddWatermark = register({
|
||||
export const actionSaveScene = register({
|
||||
name: "saveScene",
|
||||
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 };
|
||||
},
|
||||
keyTest: (event) => {
|
||||
return event.key === "s" && event[KEYS.CTRL_OR_CMD];
|
||||
return event.key === "s" && event[KEYS.CTRL_OR_CMD] && !event.shiftKey;
|
||||
},
|
||||
PanelComponent: ({ updateData }) => (
|
||||
<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({
|
||||
name: "loadScene",
|
||||
perform: (
|
||||
|
@ -34,6 +34,7 @@ export {
|
||||
actionChangeProjectName,
|
||||
actionChangeExportBackground,
|
||||
actionSaveScene,
|
||||
actionSaveAsScene,
|
||||
actionLoadScene,
|
||||
} from "./actionExport";
|
||||
|
||||
|
@ -43,6 +43,7 @@ export type ActionName =
|
||||
| "changeExportBackground"
|
||||
| "changeShouldAddWatermark"
|
||||
| "saveScene"
|
||||
| "saveAsScene"
|
||||
| "loadScene"
|
||||
| "duplicateSelection"
|
||||
| "deleteSelectedElements"
|
||||
|
@ -130,6 +130,7 @@ const LayerUI = ({
|
||||
<Stack.Row gap={1} justifyContent="space-between">
|
||||
{actionManager.renderAction("loadScene")}
|
||||
{actionManager.renderAction("saveScene")}
|
||||
{actionManager.renderAction("saveAsScene")}
|
||||
{renderExportDialog()}
|
||||
{actionManager.renderAction("clearCanvas")}
|
||||
<RoomDialog
|
||||
|
@ -84,6 +84,7 @@ export const MobileMenu = ({
|
||||
<Stack.Col gap={4}>
|
||||
{actionManager.renderAction("loadScene")}
|
||||
{actionManager.renderAction("saveScene")}
|
||||
{actionManager.renderAction("saveAsScene")}
|
||||
{exportButton}
|
||||
{actionManager.renderAction("clearCanvas")}
|
||||
<RoomDialog
|
||||
|
@ -16,6 +16,7 @@ type ToolButtonBaseProps = {
|
||||
size?: ToolIconSize;
|
||||
keyBindingLabel?: string;
|
||||
showAriaLabel?: boolean;
|
||||
hidden?: boolean;
|
||||
visible?: boolean;
|
||||
selected?: boolean;
|
||||
className?: string;
|
||||
@ -44,13 +45,16 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
||||
if (props.type === "button") {
|
||||
return (
|
||||
<button
|
||||
className={`ToolIcon_type_button ToolIcon ${sizeCn}${
|
||||
props.selected ? " ToolIcon--selected" : ""
|
||||
} ${props.className} ${
|
||||
props.visible
|
||||
className={`ToolIcon_type_button ${
|
||||
!props.hidden ? "ToolIcon" : ""
|
||||
} ${sizeCn}${props.selected ? " ToolIcon--selected" : ""} ${
|
||||
props.className
|
||||
} ${
|
||||
props.visible && !props.hidden
|
||||
? "ToolIcon_type_button--show"
|
||||
: "ToolIcon_type_button--hide"
|
||||
}`}
|
||||
hidden={props.hidden}
|
||||
title={props.title}
|
||||
aria-label={props["aria-label"]}
|
||||
type="button"
|
||||
|
@ -39,6 +39,11 @@ export const save = createIcon(
|
||||
{ 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(
|
||||
"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 },
|
||||
|
@ -24,28 +24,38 @@ export const serializeAsJSON = (
|
||||
export const saveAsJSON = async (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
fileHandle: any,
|
||||
) => {
|
||||
const serialized = serializeAsJSON(elements, appState);
|
||||
|
||||
const blob = new Blob([serialized], {
|
||||
type: "application/json",
|
||||
});
|
||||
// Either "Save as" or non-supporting browser
|
||||
if (!fileHandle) {
|
||||
const name = `${appState.name}.excalidraw`;
|
||||
await fileSave(
|
||||
new Blob([serialized], {
|
||||
type: /\b(iPad|iPhone|iPod)\b/.test(navigator.userAgent)
|
||||
? "application/json"
|
||||
: "application/vnd.excalidraw+json",
|
||||
}),
|
||||
const handle = await fileSave(
|
||||
blob,
|
||||
{
|
||||
fileName: name,
|
||||
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 () => {
|
||||
const blob = await fileOpen({
|
||||
description: "Excalidraw files",
|
||||
extensions: ["json", "excalidraw"],
|
||||
mimeTypes: ["application/json", "application/vnd.excalidraw+json"],
|
||||
mimeTypes: ["application/json"],
|
||||
});
|
||||
return loadFromBlob(blob);
|
||||
};
|
||||
|
@ -71,6 +71,7 @@
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"copyPngToClipboard": "Copy PNG to clipboard",
|
||||
"save": "Save",
|
||||
"saveAs": "Save as",
|
||||
"load": "Load",
|
||||
"getShareableLink": "Get shareable link",
|
||||
"close": "Close",
|
||||
|
Loading…
Reference in New Issue
Block a user