diff --git a/src/components/App.tsx b/src/components/App.tsx index 822500d6..a8d2442d 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -58,6 +58,8 @@ import { TAP_TWICE_TIMEOUT, TEXT_TO_CENTER_SNAP_THRESHOLD, TOUCH_CTX_MENU_TIMEOUT, + URL_HASH_KEYS, + URL_QUERY_KEYS, ZOOM_STEP, } from "../constants"; import { loadFromBlob } from "../data"; @@ -278,6 +280,7 @@ export type ExcalidrawImperativeAPI = { getSceneElements: InstanceType["getSceneElements"]; getAppState: () => InstanceType["state"]; setCanvasOffsets: InstanceType["setCanvasOffsets"]; + importLibrary: InstanceType["importLibraryFromUrl"]; readyPromise: ResolvablePromise; ready: true; }; @@ -338,6 +341,7 @@ class App extends React.Component { getSceneElements: this.getSceneElements, getAppState: () => this.state, setCanvasOffsets: this.setCanvasOffsets, + importLibrary: this.importLibraryFromUrl, } as const; if (typeof excalidrawRef === "function") { excalidrawRef(api); @@ -606,7 +610,16 @@ class App extends React.Component { }; private importLibraryFromUrl = async (url: string) => { - window.history.replaceState({}, APP_NAME, window.location.origin); + if (window.location.hash.includes(URL_HASH_KEYS.addLibrary)) { + const hash = new URLSearchParams(window.location.hash.slice(1)); + hash.delete(URL_HASH_KEYS.addLibrary); + window.history.replaceState({}, APP_NAME, `#${hash.toString()}`); + } else if (window.location.search.includes(URL_QUERY_KEYS.addLibrary)) { + const query = new URLSearchParams(window.location.search); + query.delete(URL_QUERY_KEYS.addLibrary); + window.history.replaceState({}, APP_NAME, `?${query.toString()}`); + } + try { const request = await fetch(decodeURIComponent(url)); const blob = await request.blob(); @@ -620,9 +633,11 @@ class App extends React.Component { ) ) { await Library.importLibrary(blob); - this.setState({ - isLibraryOpen: true, - }); + // hack to rerender the library items after import + if (this.state.isLibraryOpen) { + this.setState({ isLibraryOpen: false }); + } + this.setState({ isLibraryOpen: true }); } } catch (error) { window.alert(t("alerts.errorLoadingLibrary")); @@ -718,12 +733,18 @@ class App extends React.Component { commitToHistory: true, }); - const addToLibraryUrl = new URLSearchParams(window.location.search).get( - "addLibrary", - ); + const libraryUrl = + // current + new URLSearchParams(window.location.hash.slice(1)).get( + URL_HASH_KEYS.addLibrary, + ) || + // legacy, kept for compat reasons + new URLSearchParams(window.location.search).get( + URL_QUERY_KEYS.addLibrary, + ); - if (addToLibraryUrl) { - await this.importLibraryFromUrl(addToLibraryUrl); + if (libraryUrl) { + await this.importLibraryFromUrl(libraryUrl); } }; diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index d585f1fe..9e03f9bb 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -179,7 +179,7 @@ const LibraryMenuItems = ({ {t("labels.libraries")} diff --git a/src/constants.ts b/src/constants.ts index 5569d302..4abb3d26 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -116,3 +116,11 @@ export const MODES = { }; export const THEME_FILTER = cssVariables.themeFilter; + +export const URL_QUERY_KEYS = { + addLibrary: "addLibrary", +} as const; + +export const URL_HASH_KEYS = { + addLibrary: "addLibrary", +} as const; diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index be79990d..d0ddc5ae 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -12,7 +12,13 @@ import { getDefaultAppState } from "../appState"; import { ExcalidrawImperativeAPI } from "../components/App"; import { ErrorDialog } from "../components/ErrorDialog"; import { TopErrorBoundary } from "../components/TopErrorBoundary"; -import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants"; +import { + APP_NAME, + EVENT, + TITLE_TIMEOUT, + URL_HASH_KEYS, + VERSION_TIMEOUT, +} from "../constants"; import { loadFromBlob } from "../data/blob"; import { DataState, ImportedDataState } from "../data/types"; import { @@ -213,12 +219,25 @@ function ExcalidrawWrapper() { initialStatePromiseRef.current.promise.resolve(scene); }); - const onHashChange = (_: HashChangeEvent) => { - initializeScene({ collabAPI }).then((scene) => { - if (scene) { - excalidrawAPI.updateScene(scene); - } - }); + const onHashChange = (event: HashChangeEvent) => { + event.preventDefault(); + const libraryUrl = new URLSearchParams(window.location.hash.slice(1)).get( + URL_HASH_KEYS.addLibrary, + ); + if (libraryUrl) { + // If hash changed and it contains library url, import it and replace + // the url to its previous state (important in case of collaboration + // and similar). + // Using history API won't trigger another hashchange. + window.history.replaceState({}, "", event.oldURL); + excalidrawAPI.importLibrary(libraryUrl); + } else { + initializeScene({ collabAPI }).then((scene) => { + if (scene) { + excalidrawAPI.updateScene(scene); + } + }); + } }; const titleTimeout = setTimeout( diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 770285ce..3f89c14d 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -18,6 +18,8 @@ Please add the latest change on the top under the correct section. ### Features +- #### BREAKING CHANGE + Use `location.hash` when importing libraries to fix installation issues. This will require host apps to add a `hashchange` listener and call the newly exposed `excalidrawAPI.importLibrary(url)` API when applicable [#3320](https://github.com/excalidraw/excalidraw/pull/3320). - Append `location.pathname` to `libraryReturnUrl` default url [#3325](https://github.com/excalidraw/excalidraw/pull/3325). ## 0.5.0 (2021-03-21) diff --git a/src/packages/excalidraw/README.md b/src/packages/excalidraw/README.md index 7932020a..7dd4e132 100644 --- a/src/packages/excalidraw/README.md +++ b/src/packages/excalidraw/README.md @@ -473,6 +473,7 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the | history | `{ clear: () => void }` | This is the history API. `history.clear()` will clear the history | | setScrollToContent |
 (ExcalidrawElement[]) => void 
| Scroll to the nearest element to center | | setCanvasOffsets | `() => void` | Updates the offsets for the Excalidraw component so that the coordinates are computed correctly (for example the cursor position). You should call this API when your app changes the dimensions/position of the Excalidraw container, such as when toggling a sidebar. You don't have to call this when the position is changed on page scroll (we handled that ourselves). | +| importLibrary | `(url: string) => void` | Imports library from given URL. You should call this on `hashchange`, passing the `addLibrary` value if you detect it. | #### `readyPromise`