feat: Implement the Web Share Target API (#3230)

* Use the web share target API

* Make requested changes

* Remove line

* Add application/json back

* Add application/vnd.excalidraw+json

* Add 'POST' check back

* Make requested changes

* Update src/appState.ts

Co-authored-by: Thomas Steiner <tomac@google.com>

* Update test

* Override initializeScene

* Use Excalidraw MIME type

* Minor fixes

* More MIME type tweaks

* More permissive file open

* Be overpermissive in file open

Co-authored-by: Thomas Steiner <tomac@google.com>
Co-authored-by: tomayac <steiner.thomas@gmail.com>
This commit is contained in:
Arun 2021-03-14 03:12:54 +05:30 committed by GitHub
parent f1daff2437
commit b9e70ec666
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 11846 additions and 11397 deletions

View File

@ -27,7 +27,7 @@
"@types/react": "17.0.2",
"@types/react-dom": "17.0.1",
"@types/socket.io-client": "1.4.35",
"browser-fs-access": "0.14.1",
"browser-fs-access": "0.14.2",
"clsx": "1.1.1",
"firebase": "8.2.10",
"i18next-browser-languagedetector": "6.0.1",

View File

@ -26,5 +26,18 @@
}
}
],
"capture_links": "new_client"
"capture_links": "new_client",
"share_target": {
"action": "/web-share-target",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"files": [
{
"name": "file",
"accept": ["application/vnd.excalidraw+json", "application/json", ".excalidraw"]
}
]
}
}
}

View File

@ -737,11 +737,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.scene.addCallback(this.onSceneUpdated);
this.addEventListeners();
// optim to avoid extra render on init
if (
const searchParams = new URLSearchParams(window.location.search.slice(1));
if (searchParams.has("web-share-target")) {
// Obtain a file that was shared via the Web Share Target API.
this.restoreFileFromShare();
} else if (
typeof this.props.offsetLeft === "number" &&
typeof this.props.offsetTop === "number"
) {
// Optimization to avoid extra render on init.
this.initializeScene();
} else {
this.setState(this.getCanvasOffsets(this.props), () => {
@ -1278,6 +1283,22 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.setState({ toastMessage: null });
};
restoreFileFromShare = async () => {
try {
const webShareTargetCache = await caches.open("web-share-target");
const file = await webShareTargetCache.match("shared-file");
if (file) {
const blob = await file.blob();
this.loadFileToCanvas(blob);
await webShareTargetCache.delete("shared-file");
window.history.replaceState(null, APP_NAME, window.location.pathname);
}
} catch (error) {
this.setState({ errorMessage: error.message });
}
};
public updateScene = withBatchedUpdates((sceneData: SceneData) => {
if (sceneData.commitToHistory) {
history.resumeRecording();
@ -3576,20 +3597,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
console.warn(error.name, error.message);
}
}
loadFromBlob(file, this.state)
.then(({ elements, appState }) =>
this.syncActionResult({
elements,
appState: {
...(appState || this.state),
isLoading: false,
},
commitToHistory: true,
}),
)
.catch((error) => {
this.setState({ isLoading: false, errorMessage: error.message });
});
this.loadFileToCanvas(file);
} else if (
file?.type === MIME_TYPES.excalidrawlib ||
file?.name.endsWith(".excalidrawlib")
@ -3609,6 +3617,23 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
};
loadFileToCanvas = (file: Blob) => {
loadFromBlob(file, this.state)
.then(({ elements, appState }) =>
this.syncActionResult({
elements,
appState: {
...(appState || this.state),
isLoading: false,
},
commitToHistory: true,
}),
)
.catch((error) => {
this.setState({ isLoading: false, errorMessage: error.message });
});
};
private handleCanvasContextMenu = (
event: React.PointerEvent<HTMLCanvasElement>,
) => {

View File

@ -30,13 +30,13 @@ export const saveAsJSON = async (
) => {
const serialized = serializeAsJSON(elements, appState);
const blob = new Blob([serialized], {
type: "application/json",
type: MIME_TYPES.excalidraw,
});
const fileHandle = await fileSave(
blob,
{
fileName: appState.name,
fileName: `${appState.name}.excalidraw`,
description: "Excalidraw file",
extensions: [".excalidraw"],
},
@ -48,8 +48,17 @@ export const saveAsJSON = async (
export const loadFromJSON = async (localAppState: AppState) => {
const blob = await fileOpen({
description: "Excalidraw files",
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
/*
extensions: [".json", ".excalidraw", ".png", ".svg"],
mimeTypes: ["application/json", "image/png", "image/svg+xml"],
mimeTypes: [
MIME_TYPES.excalidraw,
"application/json",
"image/png",
"image/svg+xml",
],
*/
});
return loadFromBlob(blob, localAppState);
};
@ -101,8 +110,11 @@ export const saveLibraryAsJSON = async () => {
export const importLibraryFromJSON = async () => {
const blob = await fileOpen({
description: "Excalidraw library files",
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
/*
extensions: [".json", ".excalidrawlib"],
mimeTypes: ["application/json"],
*/
});
Library.importLibrary(blob);
};

View File

@ -47,3 +47,20 @@ workbox.routing.registerRoute(
plugins: [new workbox.expiration.Plugin({ maxEntries: 10 })],
}),
);
self.addEventListener("fetch", (event) => {
if (
event.request.method === "POST" &&
event.request.url.endsWith("/web-share-target")
) {
return event.respondWith(
(async () => {
const formData = await event.request.formData();
const file = formData.get("file");
const webShareTargetCache = await caches.open("web-share-target");
await webShareTargetCache.put("shared-file", new Response(file));
return Response.redirect("/?web-share-target", 303);
})(),
);
}
});

23132
yarn.lock

File diff suppressed because it is too large Load Diff