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": "17.0.2",
"@types/react-dom": "17.0.1", "@types/react-dom": "17.0.1",
"@types/socket.io-client": "1.4.35", "@types/socket.io-client": "1.4.35",
"browser-fs-access": "0.14.1", "browser-fs-access": "0.14.2",
"clsx": "1.1.1", "clsx": "1.1.1",
"firebase": "8.2.10", "firebase": "8.2.10",
"i18next-browser-languagedetector": "6.0.1", "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.scene.addCallback(this.onSceneUpdated);
this.addEventListeners(); this.addEventListeners();
// optim to avoid extra render on init const searchParams = new URLSearchParams(window.location.search.slice(1));
if (
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.offsetLeft === "number" &&
typeof this.props.offsetTop === "number" typeof this.props.offsetTop === "number"
) { ) {
// Optimization to avoid extra render on init.
this.initializeScene(); this.initializeScene();
} else { } else {
this.setState(this.getCanvasOffsets(this.props), () => { this.setState(this.getCanvasOffsets(this.props), () => {
@ -1278,6 +1283,22 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.setState({ toastMessage: null }); 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) => { public updateScene = withBatchedUpdates((sceneData: SceneData) => {
if (sceneData.commitToHistory) { if (sceneData.commitToHistory) {
history.resumeRecording(); history.resumeRecording();
@ -3576,20 +3597,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
console.warn(error.name, error.message); console.warn(error.name, error.message);
} }
} }
loadFromBlob(file, this.state) this.loadFileToCanvas(file);
.then(({ elements, appState }) =>
this.syncActionResult({
elements,
appState: {
...(appState || this.state),
isLoading: false,
},
commitToHistory: true,
}),
)
.catch((error) => {
this.setState({ isLoading: false, errorMessage: error.message });
});
} else if ( } else if (
file?.type === MIME_TYPES.excalidrawlib || file?.type === MIME_TYPES.excalidrawlib ||
file?.name.endsWith(".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 = ( private handleCanvasContextMenu = (
event: React.PointerEvent<HTMLCanvasElement>, event: React.PointerEvent<HTMLCanvasElement>,
) => { ) => {

View File

@ -30,13 +30,13 @@ export const saveAsJSON = async (
) => { ) => {
const serialized = serializeAsJSON(elements, appState); const serialized = serializeAsJSON(elements, appState);
const blob = new Blob([serialized], { const blob = new Blob([serialized], {
type: "application/json", type: MIME_TYPES.excalidraw,
}); });
const fileHandle = await fileSave( const fileHandle = await fileSave(
blob, blob,
{ {
fileName: appState.name, fileName: `${appState.name}.excalidraw`,
description: "Excalidraw file", description: "Excalidraw file",
extensions: [".excalidraw"], extensions: [".excalidraw"],
}, },
@ -48,8 +48,17 @@ export const saveAsJSON = async (
export const loadFromJSON = async (localAppState: AppState) => { export const loadFromJSON = async (localAppState: AppState) => {
const blob = await fileOpen({ const blob = await fileOpen({
description: "Excalidraw files", 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"], 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); return loadFromBlob(blob, localAppState);
}; };
@ -101,8 +110,11 @@ export const saveLibraryAsJSON = async () => {
export const importLibraryFromJSON = async () => { export const importLibraryFromJSON = async () => {
const blob = await fileOpen({ const blob = await fileOpen({
description: "Excalidraw library files", 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"], extensions: [".json", ".excalidrawlib"],
mimeTypes: ["application/json"], */
}); });
Library.importLibrary(blob); Library.importLibrary(blob);
}; };

View File

@ -47,3 +47,20 @@ workbox.routing.registerRoute(
plugins: [new workbox.expiration.Plugin({ maxEntries: 10 })], 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