diff --git a/package.json b/package.json index 9d0d3178e..9f218af9d 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@types/react": "17.0.39", "@types/react-dom": "17.0.11", "@types/socket.io-client": "1.4.36", - "browser-fs-access": "0.24.1", + "browser-fs-access": "0.29.1", "clsx": "1.1.1", "fake-indexeddb": "3.1.7", "firebase": "8.3.3", @@ -38,7 +38,7 @@ "image-blob-reduce": "3.0.1", "jotai": "1.6.4", "lodash.throttle": "4.1.1", - "nanoid": "3.1.32", + "nanoid": "3.3.3", "open-color": "1.9.1", "pako": "1.0.11", "perfect-freehand": "1.0.16", diff --git a/src/actions/actionAddToLibrary.ts b/src/actions/actionAddToLibrary.ts index 7d2f9877c..41a701786 100644 --- a/src/actions/actionAddToLibrary.ts +++ b/src/actions/actionAddToLibrary.ts @@ -25,9 +25,9 @@ export const actionAddToLibrary = register({ } return app.library - .loadLibrary() + .getLatestLibrary() .then((items) => { - return app.library.saveLibrary([ + return app.library.setLibrary([ { id: randomId(), status: "unpublished", diff --git a/src/actions/actionClipboard.tsx b/src/actions/actionClipboard.tsx index 5d92290e2..8a25e1810 100644 --- a/src/actions/actionClipboard.tsx +++ b/src/actions/actionClipboard.tsx @@ -15,7 +15,9 @@ export const actionCopy = register({ name: "copy", trackEvent: { category: "element" }, perform: (elements, appState, _, app) => { - copyToClipboard(getNonDeletedElements(elements), appState, app.files); + const selectedElements = getSelectedElements(elements, appState, true); + + copyToClipboard(selectedElements, appState, app.files); return { commitToHistory: false, diff --git a/src/actions/actionNavigate.tsx b/src/actions/actionNavigate.tsx index 453fd5271..6a13ef3ad 100644 --- a/src/actions/actionNavigate.tsx +++ b/src/actions/actionNavigate.tsx @@ -1,4 +1,4 @@ -import { getClientColors, getClientInitials } from "../clients"; +import { getClientColors } from "../clients"; import { Avatar } from "../components/Avatar"; import { centerScrollOn } from "../scene/scroll"; import { Collaborator } from "../types"; @@ -43,16 +43,15 @@ export const actionGoToCollaborator = register({ } const { background, stroke } = getClientColors(clientId, appState); - const shortName = getClientInitials(collaborator.username); return ( updateData(collaborator.pointer)} - > - {shortName} - + name={collaborator.username || ""} + src={collaborator.src} + /> ); }, }); diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index 53801a220..49efbe45f 100644 --- a/src/actions/actionProperties.tsx +++ b/src/actions/actionProperties.tsx @@ -503,20 +503,6 @@ export const actionChangeOpacity = register({ max="100" step="10" onChange={(event) => updateData(+event.target.value)} - onWheel={(event) => { - event.stopPropagation(); - const target = event.target as HTMLInputElement; - const STEP = 10; - const MAX = 100; - const MIN = 0; - const value = +target.value; - - if (event.deltaY < 0 && value < MAX) { - updateData(value + STEP); - } else if (event.deltaY > 0 && value > MIN) { - updateData(value - STEP); - } - }} value={ getFormValue( elements, diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index 434a38490..7481d56a3 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -117,6 +117,7 @@ export class ActionManager { trackAction(action, "keyboard", appState, elements, this.app, null); event.preventDefault(); + event.stopPropagation(); this.updater(data[0].perform(elements, appState, value, this.app)); return true; } diff --git a/src/clipboard.ts b/src/clipboard.ts index 5b61dae25..bc4d3d3b4 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -2,7 +2,6 @@ import { ExcalidrawElement, NonDeletedExcalidrawElement, } from "./element/types"; -import { getSelectedElements } from "./scene"; import { AppState, BinaryFiles } from "./types"; import { SVG_EXPORT_TAG } from "./scene/export"; import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts"; @@ -12,7 +11,7 @@ import { isPromiseLike } from "./utils"; type ElementsClipboard = { type: typeof EXPORT_DATA_TYPES.excalidrawClipboard; - elements: ExcalidrawElement[]; + elements: readonly NonDeletedExcalidrawElement[]; files: BinaryFiles | undefined; }; @@ -57,19 +56,20 @@ const clipboardContainsElements = ( export const copyToClipboard = async ( elements: readonly NonDeletedExcalidrawElement[], appState: AppState, - files: BinaryFiles, + files: BinaryFiles | null, ) => { // select binded text elements when copying - const selectedElements = getSelectedElements(elements, appState, true); const contents: ElementsClipboard = { type: EXPORT_DATA_TYPES.excalidrawClipboard, - elements: selectedElements, - files: selectedElements.reduce((acc, element) => { - if (isInitializedImageElement(element) && files[element.fileId]) { - acc[element.fileId] = files[element.fileId]; - } - return acc; - }, {} as BinaryFiles), + elements, + files: files + ? elements.reduce((acc, element) => { + if (isInitializedImageElement(element) && files[element.fileId]) { + acc[element.fileId] = files[element.fileId]; + } + return acc; + }, {} as BinaryFiles) + : undefined, }; const json = JSON.stringify(contents); CLIPBOARD = json; diff --git a/src/components/App.tsx b/src/components/App.tsx index 51651e046..d6640f343 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -263,6 +263,7 @@ import { isPointHittingLinkIcon, isLocalLink, } from "../element/Hyperlink"; +import { AbortError } from "../errors"; const defaultDeviceTypeContext: DeviceType = { isMobile: false, @@ -769,21 +770,35 @@ class App extends React.Component { window.history.replaceState({}, APP_NAME, `?${query.toString()}`); } + const defaultStatus = "published"; + + this.setState({ isLibraryOpen: true }); + try { - const request = await fetch(decodeURIComponent(url)); - const blob = await request.blob(); - const defaultStatus = "published"; - const libraryItems = await loadLibraryFromBlob(blob, defaultStatus); - if ( - token === this.id || - window.confirm( - t("alerts.confirmAddLibrary", { - numShapes: libraryItems.length, - }), - ) - ) { - await this.library.importLibrary(libraryItems, defaultStatus); - } + await this.library.importLibrary( + new Promise(async (resolve, reject) => { + try { + const request = await fetch(decodeURIComponent(url)); + const blob = await request.blob(); + const libraryItems = await loadLibraryFromBlob(blob, defaultStatus); + + if ( + token === this.id || + window.confirm( + t("alerts.confirmAddLibrary", { + numShapes: libraryItems.length, + }), + ) + ) { + resolve(libraryItems); + } else { + reject(new AbortError()); + } + } catch (error: any) { + reject(error); + } + }), + ); } catch (error: any) { console.error(error); this.setState({ errorMessage: t("errors.importLibraryError") }); @@ -1339,6 +1354,7 @@ class App extends React.Component { } this.cutAll(); event.preventDefault(); + event.stopPropagation(); }); private onCopy = withBatchedUpdates((event: ClipboardEvent) => { @@ -1350,6 +1366,7 @@ class App extends React.Component { } this.copyAll(); event.preventDefault(); + event.stopPropagation(); }); private cutAll = () => { @@ -1743,6 +1760,11 @@ class App extends React.Component { collaborators?: SceneData["collaborators"]; commitToHistory?: SceneData["commitToHistory"]; libraryItems?: + | (( + currentLibraryItems: LibraryItems, + ) => + | Required["libraryItems"] + | Promise["libraryItems"]>) | Required["libraryItems"] | Promise["libraryItems"]>; }) => { @@ -1763,20 +1785,20 @@ class App extends React.Component { } if (sceneData.libraryItems) { - this.library.saveLibrary( - new Promise(async (resolve, reject) => { + this.library.setLibrary((currentLibraryItems) => { + const nextItems = + typeof sceneData.libraryItems === "function" + ? sceneData.libraryItems(currentLibraryItems) + : sceneData.libraryItems; + + return new Promise(async (resolve, reject) => { try { - resolve( - restoreLibraryItems( - await sceneData.libraryItems, - "unpublished", - ), - ); - } catch { - reject(new Error(t("errors.importLibraryError"))); + resolve(restoreLibraryItems(await nextItems, "unpublished")); + } catch (error: any) { + reject(error); } - }), - ); + }); + }); } }, ); @@ -1945,8 +1967,10 @@ class App extends React.Component { ); } this.setActiveTool({ ...this.state.activeTool, type: shape }); + event.stopPropagation(); } else if (event.key === KEYS.Q) { this.toggleLock("keyboard"); + event.stopPropagation(); } } if (event.key === KEYS.SPACE && gesture.pointers.size === 0) { @@ -1955,7 +1979,11 @@ class App extends React.Component { event.preventDefault(); } - if (event.key === KEYS.G || event.key === KEYS.S) { + if ( + (event.key === KEYS.G || event.key === KEYS.S) && + !event.altKey && + !event[KEYS.CTRL_OR_CMD] + ) { const selectedElements = getSelectedElements( this.scene.getElements(), this.state, @@ -1973,9 +2001,11 @@ class App extends React.Component { selectedElements.some((element) => hasBackground(element.type))) ) { this.setState({ openPopup: "backgroundColorPicker" }); + event.stopPropagation(); } if (event.key === KEYS.S) { this.setState({ openPopup: "strokeColorPicker" }); + event.stopPropagation(); } } }, @@ -2300,8 +2330,7 @@ class App extends React.Component { if (isTextElement(selectedElements[0])) { existingTextElement = selectedElements[0]; } else if (isTextBindableContainer(selectedElements[0], false)) { - container = selectedElements[0]; - existingTextElement = getBoundTextElement(container); + existingTextElement = getBoundTextElement(selectedElements[0]); } } @@ -5374,11 +5403,14 @@ class App extends React.Component { file?.type === MIME_TYPES.excalidrawlib || file?.name?.endsWith(".excalidrawlib") ) { - this.library - .importLibrary(file) - .catch((error) => - this.setState({ isLoading: false, errorMessage: error.message }), - ); + this.setState({ isLibraryOpen: true }); + this.library.importLibrary(file).catch((error) => { + console.error(error); + this.setState({ + isLoading: false, + errorMessage: t("errors.importLibraryError"), + }); + }); // default: assume an Excalidraw file regardless of extension/MimeType } else if (file) { this.setState({ isLoading: true }); diff --git a/src/components/Avatar.scss b/src/components/Avatar.scss index d077d916b..d3f8c8bd3 100644 --- a/src/components/Avatar.scss +++ b/src/components/Avatar.scss @@ -12,5 +12,11 @@ cursor: pointer; font-size: 0.8rem; font-weight: 500; + + &-img { + width: 100%; + height: 100%; + border-radius: 100%; + } } } diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 2b8513707..9b2c8b71d 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,20 +1,28 @@ import "./Avatar.scss"; import React from "react"; +import { getClientInitials } from "../clients"; type AvatarProps = { - children: string; onClick: (e: React.MouseEvent) => void; color: string; border: string; + name: string; + src?: string; }; -export const Avatar = ({ children, color, border, onClick }: AvatarProps) => ( -
- {children} -
-); +export const Avatar = ({ color, border, onClick, name, src }: AvatarProps) => { + const shortName = getClientInitials(name); + const style = src + ? undefined + : { background: color, border: `1px solid ${border}` }; + return ( +
+ {src ? ( + {shortName} + ) : ( + shortName + )} +
+ ); +}; diff --git a/src/components/LibraryMenu.scss b/src/components/LibraryMenu.scss index 803c18480..7e0f43af5 100644 --- a/src/components/LibraryMenu.scss +++ b/src/components/LibraryMenu.scss @@ -13,6 +13,10 @@ width: 100%; margin: 2px 0; + .Spinner { + margin-right: 1rem; + } + button { // 2px from the left to account for focus border of left-most button margin: 0 2px; diff --git a/src/components/LibraryMenu.tsx b/src/components/LibraryMenu.tsx index 8ecced2cf..e2389836d 100644 --- a/src/components/LibraryMenu.tsx +++ b/src/components/LibraryMenu.tsx @@ -139,7 +139,7 @@ export const LibraryMenu = ({ const nextItems = libraryItems.filter( (item) => !selectedItems.includes(item.id), ); - library.saveLibrary(nextItems).catch(() => { + library.setLibrary(nextItems).catch(() => { setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") }); }); setSelectedItems([]); @@ -170,7 +170,7 @@ export const LibraryMenu = ({ ...libraryItems, ]; onAddToLibrary(); - library.saveLibrary(nextItems).catch(() => { + library.setLibrary(nextItems).catch(() => { setAppState({ errorMessage: t("alerts.errorAddingToLibrary") }); }); }, @@ -220,7 +220,7 @@ export const LibraryMenu = ({ libItem.status = "published"; } }); - library.saveLibrary(nextLibItems); + library.setLibrary(nextLibItems); }, [setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library], ); @@ -229,7 +229,10 @@ export const LibraryMenu = ({ LibraryItem["id"] | null >(null); - if (libraryItemsData.status === "loading") { + if ( + libraryItemsData.status === "loading" && + !libraryItemsData.isInitialized + ) { return (
@@ -255,7 +258,7 @@ export const LibraryMenu = ({ } onError={(error) => window.alert(error)} updateItemsInStorage={() => - library.saveLibrary(libraryItemsData.libraryItems) + library.setLibrary(libraryItemsData.libraryItems) } onRemove={(id: string) => setSelectedItems(selectedItems.filter((_id) => _id !== id)) @@ -264,6 +267,7 @@ export const LibraryMenu = ({ )} {publishLibSuccess && renderPublishSuccess()} removeFromLibrary(libraryItemsData.libraryItems) diff --git a/src/components/LibraryMenuItems.tsx b/src/components/LibraryMenuItems.tsx index b236e1023..11aff36cb 100644 --- a/src/components/LibraryMenuItems.tsx +++ b/src/components/LibraryMenuItems.tsx @@ -22,8 +22,10 @@ import { Tooltip } from "./Tooltip"; import "./LibraryMenuItems.scss"; import { VERSIONS } from "../constants"; +import Spinner from "./Spinner"; const LibraryMenuItems = ({ + isLoading, libraryItems, onRemoveFromLibrary, onAddToLibrary, @@ -40,6 +42,7 @@ const LibraryMenuItems = ({ onPublish, resetLibrary, }: { + isLoading: boolean; libraryItems: LibraryItems; pendingElements: LibraryItem["elements"]; onRemoveFromLibrary: () => void; @@ -108,7 +111,8 @@ const LibraryMenuItems = ({ importLibraryFromJSON(library) .catch(muteFSAbortError) .catch((error) => { - setAppState({ errorMessage: error.message }); + console.error(error); + setAppState({ errorMessage: t("errors.importLibraryError") }); }); }} className="library-actions--load" @@ -125,7 +129,7 @@ const LibraryMenuItems = ({ onClick={async () => { const libraryItems = itemsSelected ? items - : await library.loadLibrary(); + : await library.getLatestLibrary(); saveLibraryAsJSON(libraryItems) .catch(muteFSAbortError) .catch((error) => { @@ -284,16 +288,20 @@ const LibraryMenuItems = ({ {showRemoveLibAlert && renderRemoveLibAlert()}
{renderLibraryActions()} - - {t("labels.libraries")} - + {isLoading ? ( + + ) : ( + + {t("labels.libraries")} + + )}
} - | { status: "loaded"; libraryItems: LibraryItems } ->({ status: "loaded", libraryItems: [] }); +export const libraryItemsAtom = atom<{ + status: "loading" | "loaded"; + isInitialized: boolean; + libraryItems: LibraryItems; +}>({ status: "loaded", isInitialized: true, libraryItems: [] }); const cloneLibraryItems = (libraryItems: LibraryItems): LibraryItems => JSON.parse(JSON.stringify(libraryItems)); @@ -40,12 +39,28 @@ const isUniqueItem = ( }); }; +/** Merges otherItems into localItems. Unique items in otherItems array are + sorted first. */ +export const mergeLibraryItems = ( + localItems: LibraryItems, + otherItems: LibraryItems, +): LibraryItems => { + const newItems = []; + for (const item of otherItems) { + if (isUniqueItem(localItems, item)) { + newItems.push(item); + } + } + + return [...newItems, ...localItems]; +}; + class Library { - /** cache for currently active promise when initializing/updating libaries - asynchronously */ - private libraryItemsPromise: Promise | null = null; - /** last resolved libraryItems */ + /** latest libraryItems */ private lastLibraryItems: LibraryItems = []; + /** indicates whether library is initialized with library items (has gone + * though at least one update) */ + private isInitialized = false; private app: App; @@ -53,95 +68,138 @@ class Library { this.app = app; } - resetLibrary = async () => { - this.saveLibrary([]); + private updateQueue: Promise[] = []; + + private getLastUpdateTask = (): Promise | undefined => { + return this.updateQueue[this.updateQueue.length - 1]; }; - /** imports library (currently merges, removing duplicates) */ - async importLibrary( + private notifyListeners = () => { + if (this.updateQueue.length > 0) { + jotaiStore.set(libraryItemsAtom, { + status: "loading", + libraryItems: this.lastLibraryItems, + isInitialized: this.isInitialized, + }); + } else { + this.isInitialized = true; + jotaiStore.set(libraryItemsAtom, { + status: "loaded", + libraryItems: this.lastLibraryItems, + isInitialized: this.isInitialized, + }); + try { + this.app.props.onLibraryChange?.( + cloneLibraryItems(this.lastLibraryItems), + ); + } catch (error) { + console.error(error); + } + } + }; + + resetLibrary = () => { + return this.setLibrary([]); + }; + + /** + * imports library (from blob or libraryItems), merging with current library + * (attempting to remove duplicates) + */ + importLibrary( library: | Blob | Required["libraryItems"] | Promise["libraryItems"]>, defaultStatus: LibraryItem["status"] = "unpublished", - ) { - return this.saveLibrary( - new Promise(async (resolve, reject) => { - try { - let libraryItems: LibraryItems; - if (library instanceof Blob) { - libraryItems = await loadLibraryFromBlob(library, defaultStatus); - } else { - libraryItems = restoreLibraryItems(await library, defaultStatus); - } - - const existingLibraryItems = this.lastLibraryItems; - - const filteredItems = []; - for (const item of libraryItems) { - if (isUniqueItem(existingLibraryItems, item)) { - filteredItems.push(item); + ): Promise { + return this.setLibrary( + () => + new Promise(async (resolve, reject) => { + try { + let libraryItems: LibraryItems; + if (library instanceof Blob) { + libraryItems = await loadLibraryFromBlob(library, defaultStatus); + } else { + libraryItems = restoreLibraryItems(await library, defaultStatus); } - } - resolve([...filteredItems, ...existingLibraryItems]); - } catch (error) { - reject(new Error(t("errors.importLibraryError"))); - } - }), + resolve(mergeLibraryItems(this.lastLibraryItems, libraryItems)); + } catch (error) { + reject(error); + } + }), ); } - loadLibrary = (): Promise => { + /** + * @returns latest cloned libraryItems. Awaits all in-progress updates first. + */ + getLatestLibrary = (): Promise => { return new Promise(async (resolve) => { try { - resolve( - cloneLibraryItems( - await (this.libraryItemsPromise || this.lastLibraryItems), - ), - ); + const libraryItems = await (this.getLastUpdateTask() || + this.lastLibraryItems); + if (this.updateQueue.length > 0) { + resolve(this.getLatestLibrary()); + } else { + resolve(cloneLibraryItems(libraryItems)); + } } catch (error) { return resolve(this.lastLibraryItems); } }); }; - saveLibrary = async (items: LibraryItems | Promise) => { - const prevLibraryItems = this.lastLibraryItems; - try { - let nextLibraryItems; - if (isPromiseLike(items)) { - const promise = items.then((items) => cloneLibraryItems(items)); - this.libraryItemsPromise = promise; - jotaiStore.set(libraryItemsAtom, { - status: "loading", - promise, - libraryItems: null, - }); - nextLibraryItems = await promise; - } else { - nextLibraryItems = cloneLibraryItems(items); + setLibrary = ( + /** + * LibraryItems that will replace current items. Can be a function which + * will be invoked after all previous tasks are resolved + * (this is the prefered way to update the library to avoid race conditions, + * but you'll want to manually merge the library items in the callback + * - which is what we're doing in Library.importLibrary()). + * + * If supplied promise is rejected with AbortError, we swallow it and + * do not update the library. + */ + libraryItems: + | LibraryItems + | Promise + | (( + latestLibraryItems: LibraryItems, + ) => LibraryItems | Promise), + ): Promise => { + const task = new Promise(async (resolve, reject) => { + try { + await this.getLastUpdateTask(); + + if (typeof libraryItems === "function") { + libraryItems = libraryItems(this.lastLibraryItems); + } + + this.lastLibraryItems = cloneLibraryItems(await libraryItems); + + resolve(this.lastLibraryItems); + } catch (error: any) { + reject(error); } - - this.lastLibraryItems = nextLibraryItems; - this.libraryItemsPromise = null; - - jotaiStore.set(libraryItemsAtom, { - status: "loaded", - libraryItems: nextLibraryItems, + }) + .catch((error) => { + if (error.name === "AbortError") { + console.warn("Library update aborted by user"); + return this.lastLibraryItems; + } + throw error; + }) + .finally(() => { + this.updateQueue = this.updateQueue.filter((_task) => _task !== task); + this.notifyListeners(); }); - await this.app.props.onLibraryChange?.( - cloneLibraryItems(nextLibraryItems), - ); - } catch (error: any) { - this.lastLibraryItems = prevLibraryItems; - this.libraryItemsPromise = null; - jotaiStore.set(libraryItemsAtom, { - status: "loaded", - libraryItems: prevLibraryItems, - }); - throw error; - } + + this.updateQueue.push(task); + this.notifyListeners(); + + return task; }; } diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index d9753c89b..ae7f1341c 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -544,6 +544,29 @@ describe("textWysiwyg", () => { expect((h.elements[1] as ExcalidrawTextElement).containerId).toBe(null); }); + it("should'nt bind text to container when not double clicked on center", async () => { + expect(h.elements.length).toBe(1); + expect(h.elements[0].id).toBe(rectangle.id); + + // clicking somewhere on top left + mouse.doubleClickAt(rectangle.x + 20, rectangle.y + 20); + expect(h.elements.length).toBe(2); + + const text = h.elements[1] as ExcalidrawTextElementWithContainer; + expect(text.type).toBe("text"); + expect(text.containerId).toBe(null); + mouse.down(); + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + + fireEvent.change(editor, { target: { value: "Hello World!" } }); + + await new Promise((r) => setTimeout(r, 0)); + editor.blur(); + expect(rectangle.boundElements).toBe(null); + }); + it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => { expect(h.elements.length).toBe(1); diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index 4edea15db..1bd940deb 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -283,7 +283,14 @@ export const textWysiwyg = ({ // using scrollHeight here since we need to calculate // number of lines so cannot use editable.style.height // as that gets updated below - const lines = editable.scrollHeight / getApproxLineHeight(font); + // Rounding here so that the lines calculated is more accurate in all browsers. + // The scrollHeight and approxLineHeight differs in diff browsers + // eg it gives 1.05 in firefox for handewritten small font due to which + // height gets updated as lines > 1 and leads to jumping text for first line in bound container + // hence rounding here to avoid that + const lines = Math.round( + editable.scrollHeight / getApproxLineHeight(font), + ); // auto increase height only when lines > 1 so its // measured correctly and vertically aligns for // first line as well as setting height to "auto" @@ -298,7 +305,6 @@ export const textWysiwyg = ({ font, container!.width, ).split("\n").length; - // This is browser behaviour when setting height to "auto" // It sets the height needed for 2 lines even if actual // line count is 1 as mentioned above as well @@ -316,8 +322,6 @@ export const textWysiwyg = ({ } editable.onkeydown = (event) => { - event.stopPropagation(); - if (!event.shiftKey && actionZoomIn.keyTest(event)) { event.preventDefault(); app.actionManager.executeAction(actionZoomIn); diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 5451a9c82..75f5ae149 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -19,7 +19,8 @@ import { } from "../element/types"; import { useCallbackRefState } from "../hooks/useCallbackRefState"; import { Language, t } from "../i18n"; -import Excalidraw, { +import { + Excalidraw, defaultLang, languages, } from "../packages/excalidraw/index"; diff --git a/src/global.d.ts b/src/global.d.ts index 4d607b9a7..dbaf3922e 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -13,6 +13,7 @@ interface Window { ClipboardItem: any; __EXCALIDRAW_SHA__: string | undefined; EXCALIDRAW_ASSET_PATH: string | undefined; + EXCALIDRAW_EXPORT_SOURCE: string; gtag: Function; } diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index e3aa056d8..402cfd79c 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -17,11 +17,18 @@ Please add the latest change on the top under the correct section. #### Features +- Export [`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L92) supported by Excalidraw [#5135](https://github.com/excalidraw/excalidraw/pull/5135). +- Support [`src`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L50) for collaborators. Now onwards host can pass `src` to render the customized avatar for collaborators [#5114](https://github.com/excalidraw/excalidraw/pull/5114). +- Support `libraryItems` argument in `initialData.libraryItems` and `updateScene({ libraryItems })` to be a Promise resolving to `LibraryItems`, and support functional update of `libraryItems` in [`updateScene({ libraryItems })`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene). [#5101](https://github.com/excalidraw/excalidraw/pull/5101). +- Expose util [`mergeLibraryItems`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#mergeLibraryItems) [#5101](https://github.com/excalidraw/excalidraw/pull/5101). +- Expose util [`exportToClipboard`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToClipboard) which allows to copy the scene contents to clipboard as `svg`, `png` or `json` [#5103](https://github.com/excalidraw/excalidraw/pull/5103). +- Expose `window.EXCALIDRAW_EXPORT_SOURCE` which you can use to overwrite the `source` field in exported data [#5095](https://github.com/excalidraw/excalidraw/pull/5095). - The `exportToBlob` utility now supports the `exportEmbedScene` option when generating a png image [#5047](https://github.com/excalidraw/excalidraw/pull/5047). - Exported [`restoreLibraryItems`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreLibraryItems) API [#4995](https://github.com/excalidraw/excalidraw/pull/4995). #### Fixes +- Use `window.EXCALIDRAW_ASSET_PATH` for fonts when exporting to svg [#5065](https://github.com/excalidraw/excalidraw/pull/5065). - Library menu now properly rerenders if open when library is updated using `updateScene({ libraryItems })` [#4995](https://github.com/excalidraw/excalidraw/pull/4995). #### Refactor diff --git a/src/packages/excalidraw/README_NEXT.md b/src/packages/excalidraw/README_NEXT.md index c20b29151..ca8262bd9 100644 --- a/src/packages/excalidraw/README_NEXT.md +++ b/src/packages/excalidraw/README_NEXT.md @@ -436,7 +436,7 @@ This helps to load Excalidraw with `initialData`. It must be an object or a [pro | `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | The elements with which Excalidraw should be mounted. | | `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) | The App state with which Excalidraw should be mounted. | | `scrollToContent` | boolean | This attribute implies whether to scroll to the nearest element to center once Excalidraw is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained | -| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L151) | This library items with which Excalidraw should be mounted. | +| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | This library items with which Excalidraw should be mounted. | | `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | The files added to the scene. | ```json @@ -512,9 +512,9 @@ You can use this function to update the scene with the sceneData. It accepts the | --- | --- | --- | | `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L17) | The `elements` to be updated in the scene | | `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L18) | The `appState` to be updated in the scene. | -| `collaborators` |
MapCollaborator>
| The list of collaborators to be updated in the scene. | +| `collaborators` |
MapCollaborator>
| The list of collaborators to be updated in the scene. | | `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. | -| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L258) | The `libraryItems` to be update in the scene. | +| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | ((currentItems: [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) => [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) | The `libraryItems` to be update in the scene. | ### `addFiles` @@ -857,7 +857,7 @@ This function returns the canvas with the exported elements, appState and dimens
 exportToBlob(
-  opts: ExportOpts & {
+  opts: ExportOpts & {
   mimeType?: string,
   quality?: number;
 })
@@ -900,6 +900,34 @@ exportToSvg({
 
 This function returns a promise which resolves to svg of the exported drawing.
 
+#### `exportToClipboard`
+
+**_Signature_**
+
+
+exportToClipboard(
+  opts: ExportOpts & {
+  mimeType?: string,
+  quality?: number;
+  type: 'png' | 'svg' |'json'
+})
+
+ +| Name | Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| opts | | | This param is same as the params passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exportToCanvas). | +| mimeType | string | "image/png" | Indicates the image format, this will be used when exporting as `png`. | +| quality | number | 0.92 | A value between 0 and 1 indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. This will be used when exporting as `png`. | +| type | 'png' | 'svg' | 'json' | | This determines the format to which the scene data should be exported. | + +**How to use** + +```js +import { exportToClipboard } from "@excalidraw/excalidraw-next"; +``` + +Copies the scene data in the specified format (determined by `type`) to clipboard. + ##### Additional attributes of appState for `export\*` APIs | Name | Type | Default | Description | @@ -924,17 +952,21 @@ serializeAsJSON({ Takes the scene elements and state and returns a JSON string. Deleted `elements`as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L16) source for details). +If you want to overwrite the source field in the JSON string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value. + #### `serializeLibraryAsJSON` **_Signature_**
 serializeLibraryAsJSON({
-  libraryItems: LibraryItems[],
+  libraryItems: LibraryItems[],
 
Takes the library items and returns a JSON string. +If you want to overwrite the source field in the JSON string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value. + #### `getSceneVersion` **How to use** @@ -1040,6 +1072,20 @@ getNonDeletedElements(elements: +mergeLibraryItems(localItems: LibraryItems, otherItems: LibraryItems) => LibraryItems +
+ +This function merges two `LibraryItems` arrays, where unique items from `otherItems` are sorted first in the returned array. + ### Exported constants #### `FONT_FAMILY` @@ -1077,6 +1123,16 @@ import { THEME } from "@excalidraw/excalidraw-next"; Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme` +### `MIME_TYPES` + +**How to use ** + +```js +import { MIME_TYPES } from "@excalidraw/excalidraw-next"; +``` + +[`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L92) contains all the mime types supported by `Excalidraw`. + ## Need help? Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). diff --git a/src/packages/excalidraw/entry.js b/src/packages/excalidraw/entry.js index 8915a1255..02c14fdb1 100644 --- a/src/packages/excalidraw/entry.js +++ b/src/packages/excalidraw/entry.js @@ -1,6 +1,5 @@ -import Excalidraw from "./index"; +import "./publicPath"; import "../../../public/fonts.css"; -export { Excalidraw }; export * from "./index"; diff --git a/src/packages/excalidraw/env.js b/src/packages/excalidraw/env.js index 7ca6283e2..6c5fd56e5 100644 --- a/src/packages/excalidraw/env.js +++ b/src/packages/excalidraw/env.js @@ -1,14 +1,18 @@ const dotenv = require("dotenv"); const { readFileSync } = require("fs"); - +const pkg = require("./package.json"); const parseEnvVariables = (filepath) => { - return Object.entries(dotenv.parse(readFileSync(filepath))).reduce( + const envVars = Object.entries(dotenv.parse(readFileSync(filepath))).reduce( (env, [key, value]) => { env[key] = JSON.stringify(value); return env; }, {}, ); + envVars.PKG_NAME = JSON.stringify(pkg.name); + envVars.PKG_VERSION = JSON.stringify(pkg.version); + envVars.IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true); + return envVars; }; module.exports = { parseEnvVariables }; diff --git a/src/packages/excalidraw/example/App.js b/src/packages/excalidraw/example/App.js index 70915224b..307ff288a 100644 --- a/src/packages/excalidraw/example/App.js +++ b/src/packages/excalidraw/example/App.js @@ -5,15 +5,16 @@ import Sidebar from "./sidebar/Sidebar"; import "./App.scss"; import initialData from "./initialData"; -import { MIME_TYPES } from "../../../constants"; // This is so that we use the bundled excalidraw.development.js file instead // of the actual source code const { - Excalidraw, exportToCanvas, exportToSvg, exportToBlob, + exportToClipboard, + Excalidraw, + MIME_TYPES, sceneCoordsToViewportCoords, } = window.ExcalidrawLib; @@ -49,7 +50,10 @@ const resolvablePromise = () => { const renderTopRightUI = () => { return ( - @@ -76,6 +80,7 @@ export default function App() { const [exportWithDarkMode, setExportWithDarkMode] = useState(false); const [exportEmbedScene, setExportEmbedScene] = useState(false); const [theme, setTheme] = useState("light"); + const [isCollaborating, setIsCollaborating] = useState(false); const initialStatePromiseRef = useRef({ promise: null }); if (!initialStatePromiseRef.current.promise) { @@ -293,6 +298,16 @@ export default function App() { addTextArea(element); } }; + const onCopy = async (type) => { + await exportToClipboard({ + elements: excalidrawRef.current.getSceneElements(), + appState: excalidrawRef.current.getAppState(), + files: excalidrawRef.current.getFiles(), + type, + }); + window.alert(`Copied to clipboard as ${type} sucessfully`); + }; + return (

Excalidraw Example

@@ -327,6 +342,7 @@ export default function App() { > Update Library + + +
+ + + +
{ +const ExcalidrawBase = (props: ExcalidrawProps) => { const { onChange, initialData, @@ -186,8 +185,10 @@ const areEqual = ( const forwardedRefComp = forwardRef< ExcalidrawAPIRefValue, PublicExcalidrawProps ->((props, ref) => ); -export default React.memo(forwardedRefComp, areEqual); +>((props, ref) => ); + +export const Excalidraw = React.memo(forwardedRefComp, areEqual); + export { getSceneVersion, isInvisiblySmallElement, @@ -209,10 +210,12 @@ export { loadLibraryFromBlob, loadFromBlob, getFreeDrawSvgPath, + exportToClipboard, + mergeLibraryItems, } from "../../packages/utils"; export { isLinearElement } from "../../element/typeChecks"; -export { FONT_FAMILY, THEME } from "../../constants"; +export { FONT_FAMILY, THEME, MIME_TYPES } from "../../constants"; export { mutateElement, diff --git a/src/packages/excalidraw/package.json b/src/packages/excalidraw/package.json index f4b9aa779..5ae76ebe0 100644 --- a/src/packages/excalidraw/package.json +++ b/src/packages/excalidraw/package.json @@ -47,12 +47,12 @@ "@babel/core": "7.17.0", "@babel/plugin-transform-arrow-functions": "7.16.7", "@babel/plugin-transform-async-to-generator": "7.16.0", - "@babel/plugin-transform-runtime": "7.16.8", + "@babel/plugin-transform-runtime": "7.17.10", "@babel/plugin-transform-typescript": "7.16.1", "@babel/preset-env": "7.16.7", "@babel/preset-react": "7.16.7", "@babel/preset-typescript": "7.16.7", - "autoprefixer": "10.4.2", + "autoprefixer": "10.4.5", "babel-loader": "8.2.3", "babel-plugin-transform-class-properties": "6.24.1", "cross-env": "7.0.3", @@ -63,7 +63,7 @@ "terser-webpack-plugin": "5.3.1", "ts-loader": "9.2.8", "typescript": "4.5.4", - "webpack": "5.65.0", + "webpack": "5.72.0", "webpack-bundle-analyzer": "4.5.0", "webpack-cli": "4.9.2", "webpack-dev-server": "4.7.4", diff --git a/src/packages/excalidraw/publicPath.js b/src/packages/excalidraw/publicPath.js index 93660ee66..0e1f8c3db 100644 --- a/src/packages/excalidraw/publicPath.js +++ b/src/packages/excalidraw/publicPath.js @@ -1,9 +1,8 @@ import { ENV } from "../../constants"; -import pkg from "./package.json"; if (process.env.NODE_ENV !== ENV.TEST) { /* eslint-disable */ /* global __webpack_public_path__:writable */ __webpack_public_path__ = window.EXCALIDRAW_ASSET_PATH || - `https://unpkg.com/${pkg.name}@${pkg.version}/dist/`; + `https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}/dist/`; } diff --git a/src/packages/excalidraw/yarn.lock b/src/packages/excalidraw/yarn.lock index 3bbda4369..97784140d 100644 --- a/src/packages/excalidraw/yarn.lock +++ b/src/packages/excalidraw/yarn.lock @@ -862,10 +862,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" -"@babel/plugin-transform-runtime@7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.8.tgz#3339368701103edae708f0fba9e4bfb70a3e5872" - integrity sha512-6Kg2XHPFnIarNweZxmzbgYnnWsXxkx9WQUVk2sksBRL80lBC1RAQV3wQagWxdCHiYHqPN+oenwNIuttlYgIbQQ== +"@babel/plugin-transform-runtime@7.17.10": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.10.tgz#b89d821c55d61b5e3d3c3d1d636d8d5a81040ae1" + integrity sha512-6jrMilUAJhktTr56kACL8LnWC5hx3Lf27BS0R0DSyW/OoJfb/iTHeE96V3b1dgKG3FSFdd/0culnYWMkjcKCig== dependencies: "@babel/helper-module-imports" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" @@ -1174,10 +1174,10 @@ dependencies: "@types/node" "*" -"@types/eslint-scope@^3.7.0": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.0.tgz#4792816e31119ebd506902a482caec4951fabd86" - integrity sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw== +"@types/eslint-scope@^3.7.3": + version "3.7.3" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" + integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g== dependencies: "@types/eslint" "*" "@types/estree" "*" @@ -1190,10 +1190,10 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@^0.0.50": - version "0.0.50" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" - integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== +"@types/estree@*", "@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": version "4.17.27" @@ -1571,20 +1571,20 @@ array-union@^2.1.0: integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== async@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== dependencies: lodash "^4.17.14" -autoprefixer@10.4.2: - version "10.4.2" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b" - integrity sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ== +autoprefixer@10.4.5: + version "10.4.5" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.5.tgz#662193c744094b53d3637f39be477e07bd904998" + integrity sha512-Fvd8yCoA7lNX/OUllvS+aS1I7WRBclGXsepbvT8ZaPgrH24rgXpZzF0/6Hh3ZEkwg+0AES/Osd196VZmYoEFtw== dependencies: - browserslist "^4.19.1" - caniuse-lite "^1.0.30001297" - fraction.js "^4.1.2" + browserslist "^4.20.2" + caniuse-lite "^1.0.30001332" + fraction.js "^4.2.0" normalize-range "^0.1.2" picocolors "^1.0.0" postcss-value-parser "^4.2.0" @@ -1800,15 +1800,15 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.14.5, browserslist@^4.17.5, browserslist@^4.17.6, browserslist@^4.19.1: - version "4.19.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3" - integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A== +browserslist@^4.14.5, browserslist@^4.17.5, browserslist@^4.17.6, browserslist@^4.19.1, browserslist@^4.20.2: + version "4.20.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88" + integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA== dependencies: - caniuse-lite "^1.0.30001286" - electron-to-chromium "^1.4.17" + caniuse-lite "^1.0.30001317" + electron-to-chromium "^1.4.84" escalade "^3.1.1" - node-releases "^2.0.1" + node-releases "^2.0.2" picocolors "^1.0.0" buffer-from@^1.0.0: @@ -1844,10 +1844,10 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297: - version "1.0.30001298" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001298.tgz#0e690039f62e91c3ea581673d716890512e7ec52" - integrity sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ== +caniuse-lite@^1.0.30001317, caniuse-lite@^1.0.30001332: + version "1.0.30001332" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd" + integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw== chalk@^1.1.3: version "1.1.3" @@ -2211,10 +2211,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.4.17: - version "1.4.38" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.38.tgz#10ea58d73d36b13e78d5024f3b74a352d3958d01" - integrity sha512-WhHt3sZazKj0KK/UpgsbGQnUUoFeAHVishzHFExMxagpZgjiGYSC9S0ZlbhCfSH2L2i+2A1yyqOIliTctMx7KQ== +electron-to-chromium@^1.4.84: + version "1.4.118" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.118.tgz#2d917c71712dac9652cc01af46c7d0bd51552974" + integrity sha512-maZIKjnYDvF7Fs35nvVcyr44UcKNwybr93Oba2n3HkKDFAtk0svERkLN/HyczJDS3Fo4wU9th9fUQd09ZLtj1w== emojis-list@^3.0.0: version "3.0.0" @@ -2226,10 +2226,10 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -enhanced-resolve@^5.0.0, enhanced-resolve@^5.8.3: - version "5.8.3" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0" - integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA== +enhanced-resolve@^5.0.0, enhanced-resolve@^5.9.2: + version "5.9.2" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz#0224dcd6a43389ebfb2d55efee517e5466772dd9" + integrity sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -2449,10 +2449,10 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -fraction.js@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.2.tgz#13e420a92422b6cf244dff8690ed89401029fbe8" - integrity sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA== +fraction.js@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" + integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== fresh@0.5.2: version "0.5.2" @@ -2544,10 +2544,10 @@ globby@^11.0.1: merge2 "^1.3.0" slash "^3.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== +graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== gzip-size@^6.0.0: version "6.0.0" @@ -3044,24 +3044,12 @@ micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4: braces "^3.0.1" picomatch "^2.2.3" -mime-db@1.45.0: - version "1.45.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== - mime-db@1.51.0, "mime-db@>= 1.43.0 < 2": version "1.51.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== -mime-types@^2.1.27: - version "2.1.28" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" - integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== - dependencies: - mime-db "1.45.0" - -mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24: +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24: version "2.1.34" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== @@ -3103,9 +3091,9 @@ minimatch@^3.0.4: brace-expansion "^1.1.7" minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== mkdirp@^0.5.5: version "0.5.5" @@ -3158,14 +3146,14 @@ neo-async@^2.6.2: integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== node-forge@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c" - integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w== + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== -node-releases@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" - integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +node-releases@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96" + integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -4180,18 +4168,18 @@ webpack-merge@5.8.0, webpack-merge@^5.7.3: clone-deep "^4.0.1" wildcard "^2.0.0" -webpack-sources@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.2.tgz#d88e3741833efec57c4c789b6010db9977545260" - integrity sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw== +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@5.65.0: - version "5.65.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.65.0.tgz#ed2891d9145ba1f0d318e4ea4f89c3fa18e6f9be" - integrity sha512-Q5or2o6EKs7+oKmJo7LaqZaMOlDWQse9Tm5l1WAfU/ujLGN5Pb0SqGeVkN/4bpPmEqEP5RnVhiqsOtWtUVwGRw== +webpack@5.72.0: + version "5.72.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.72.0.tgz#f8bc40d9c6bb489a4b7a8a685101d6022b8b6e28" + integrity sha512-qmSmbspI0Qo5ld49htys8GY9XhS9CGqFoHTsOVAnjBdg0Zn79y135R+k4IR4rKK6+eKaabMhJwiVB7xw0SJu5w== dependencies: - "@types/eslint-scope" "^3.7.0" - "@types/estree" "^0.0.50" + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" "@webassemblyjs/ast" "1.11.1" "@webassemblyjs/wasm-edit" "1.11.1" "@webassemblyjs/wasm-parser" "1.11.1" @@ -4199,12 +4187,12 @@ webpack@5.65.0: acorn-import-assertions "^1.7.6" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.8.3" + enhanced-resolve "^5.9.2" es-module-lexer "^0.9.0" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" json-parse-better-errors "^1.0.2" loader-runner "^4.2.0" mime-types "^2.1.27" @@ -4213,7 +4201,7 @@ webpack@5.65.0: tapable "^2.1.1" terser-webpack-plugin "^5.1.3" watchpack "^2.3.1" - webpack-sources "^3.2.2" + webpack-sources "^3.2.3" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" diff --git a/src/packages/utils.ts b/src/packages/utils.ts index a0a4cc599..03dcd3d0a 100644 --- a/src/packages/utils.ts +++ b/src/packages/utils.ts @@ -10,6 +10,11 @@ import { restore } from "../data/restore"; import { MIME_TYPES } from "../constants"; import { encodePngMetadata } from "../data/image"; import { serializeAsJSON } from "../data/json"; +import { + copyBlobToClipboardAsPng, + copyTextToSystemClipboard, + copyToClipboard, +} from "../clipboard"; type ExportOpts = { elements: readonly NonDeleted[]; @@ -81,7 +86,7 @@ export const exportToBlob = async ( mimeType?: string; quality?: number; }, -): Promise => { +): Promise => { let { mimeType = MIME_TYPES.png, quality } = opts; if (mimeType === MIME_TYPES.png && typeof quality === "number") { @@ -107,9 +112,12 @@ export const exportToBlob = async ( quality = quality ? quality : /image\/jpe?g/.test(mimeType) ? 0.92 : 0.8; - return new Promise((resolve) => { + return new Promise((resolve, reject) => { canvas.toBlob( - async (blob: Blob | null) => { + async (blob) => { + if (!blob) { + return reject(new Error("couldn't export to blob")); + } if ( blob && mimeType === MIME_TYPES.png && @@ -156,6 +164,34 @@ export const exportToSvg = async ({ ); }; +export const exportToClipboard = async ( + opts: ExportOpts & { + mimeType?: string; + quality?: number; + type: "png" | "svg" | "json"; + }, +) => { + if (opts.type === "svg") { + const svg = await exportToSvg(opts); + await copyTextToSystemClipboard(svg.outerHTML); + } else if (opts.type === "png") { + await copyBlobToClipboardAsPng(exportToBlob(opts)); + } else if (opts.type === "json") { + const appState = { + offsetTop: 0, + offsetLeft: 0, + width: 0, + height: 0, + ...getDefaultAppState(), + ...opts.appState, + }; + await copyToClipboard(opts.elements, appState, opts.files); + } else { + throw new Error("Invalid export type"); + } +}; + export { serializeAsJSON, serializeLibraryAsJSON } from "../data/json"; export { loadFromBlob, loadLibraryFromBlob } from "../data/blob"; export { getFreeDrawSvgPath } from "../renderer/renderElement"; +export { mergeLibraryItems } from "../data/library"; diff --git a/src/packages/utils/package.json b/src/packages/utils/package.json index bf06ed494..26c82fb71 100644 --- a/src/packages/utils/package.json +++ b/src/packages/utils/package.json @@ -41,7 +41,7 @@ "@babel/plugin-transform-typescript": "7.16.1", "@babel/preset-env": "7.16.7", "@babel/preset-typescript": "7.16.7", - "babel-loader": "8.2.3", + "babel-loader": "8.2.5", "babel-plugin-transform-class-properties": "6.24.1", "cross-env": "7.0.3", "css-loader": "6.7.1", diff --git a/src/packages/utils/yarn.lock b/src/packages/utils/yarn.lock index ebe9c89d8..0efdd1b70 100644 --- a/src/packages/utils/yarn.lock +++ b/src/packages/utils/yarn.lock @@ -1320,13 +1320,13 @@ babel-helper-get-function-arity@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" -babel-loader@8.2.3: - version "8.2.3" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.3.tgz#8986b40f1a64cacfcb4b8429320085ef68b1342d" - integrity sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw== +babel-loader@8.2.5: + version "8.2.5" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.5.tgz#d45f585e654d5a5d90f5350a779d7647c5ed512e" + integrity sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ== dependencies: find-cache-dir "^3.3.1" - loader-utils "^1.4.0" + loader-utils "^2.0.0" make-dir "^3.1.0" schema-utils "^2.6.5" @@ -2002,13 +2002,6 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - json5@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" @@ -2031,15 +2024,6 @@ loader-runner@^4.2.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== -loader-utils@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - loader-utils@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" @@ -2122,10 +2106,10 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== ms@2.0.0: version "2.0.0" diff --git a/src/scene/export.ts b/src/scene/export.ts index d404e822b..9dacc755b 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -115,6 +115,19 @@ export const exportToSvg = async ( svgRoot.setAttribute("filter", THEME_FILTER); } + let assetPath = "https://excalidraw.com/"; + + // Asset path needs to be determined only when using package + if (process.env.IS_EXCALIDRAW_NPM_PACKAGE) { + assetPath = + window.EXCALIDRAW_ASSET_PATH || + `https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}`; + + if (assetPath?.startsWith("/")) { + assetPath = assetPath.replace("/", `${window.location.origin}/`); + } + assetPath = `${assetPath}/dist/excalidraw-assets/`; + } svgRoot.innerHTML = ` ${SVG_EXPORT_TAG} ${metadata} @@ -122,16 +135,15 @@ export const exportToSvg = async ( `; - // render background rect if (appState.exportBackground && viewBackgroundColor) { const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect"); diff --git a/src/tests/library.test.tsx b/src/tests/library.test.tsx index 5b8bf2641..3d433940e 100644 --- a/src/tests/library.test.tsx +++ b/src/tests/library.test.tsx @@ -14,12 +14,12 @@ describe("library", () => { }); it("import library via drag&drop", async () => { - expect(await h.app.library.loadLibrary()).toEqual([]); + expect(await h.app.library.getLatestLibrary()).toEqual([]); await API.drop( await API.loadFile("./fixtures/fixture_library.excalidrawlib"), ); await waitFor(async () => { - expect(await h.app.library.loadLibrary()).toEqual([ + expect(await h.app.library.getLatestLibrary()).toEqual([ { status: "unpublished", elements: [expect.objectContaining({ id: "A" })], diff --git a/src/tests/packages/excalidraw.test.tsx b/src/tests/packages/excalidraw.test.tsx index d0e22c6dc..1007fd88b 100644 --- a/src/tests/packages/excalidraw.test.tsx +++ b/src/tests/packages/excalidraw.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, GlobalTestState, render } from "../test-utils"; -import Excalidraw from "../../packages/excalidraw/index"; +import { Excalidraw } from "../../packages/excalidraw/index"; import { queryByText, queryByTestId } from "@testing-library/react"; import { GRID_SIZE, THEME } from "../../constants"; import { t } from "../../i18n"; diff --git a/src/tests/scene/export.test.ts b/src/tests/scene/export.test.ts index 1b6f72113..bab87a15c 100644 --- a/src/tests/scene/export.test.ts +++ b/src/tests/scene/export.test.ts @@ -7,6 +7,7 @@ import { } from "../fixtures/elementFixture"; describe("exportToSvg", () => { + window.EXCALIDRAW_ASSET_PATH = "/"; const ELEMENT_HEIGHT = 100; const ELEMENT_WIDTH = 100; const ELEMENTS = [ diff --git a/src/tests/scroll.test.tsx b/src/tests/scroll.test.tsx index 70a115a50..1105352d0 100644 --- a/src/tests/scroll.test.tsx +++ b/src/tests/scroll.test.tsx @@ -4,7 +4,7 @@ import { restoreOriginalGetBoundingClientRect, waitFor, } from "./test-utils"; -import Excalidraw from "../packages/excalidraw/index"; +import { Excalidraw } from "../packages/excalidraw/index"; import { API } from "./helpers/api"; const { h } = window; diff --git a/src/types.ts b/src/types.ts index 877f3a4d4..f397af32a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,6 +45,9 @@ export type Collaborator = { background: string; stroke: string; }; + // The url of the collaborator's avatar, defaults to username intials + // if not present + src?: string; }; export type DataURL = string & { _brand: "DataURL" }; diff --git a/src/utils.ts b/src/utils.ts index 3c457e2bd..e6a189ac7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -575,6 +575,9 @@ export const arrayToMap = ( export const isTestEnv = () => typeof process !== "undefined" && process.env?.NODE_ENV === "test"; +export const isProdEnv = () => + typeof process !== "undefined" && process.env?.NODE_ENV === "production"; + export const wrapEvent = (name: EVENT, nativeEvent: T) => { return new CustomEvent(name, { detail: { diff --git a/yarn.lock b/yarn.lock index c34301cc9..1cb204eb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2971,9 +2971,9 @@ async-limiter@~1.0.0: integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== async@^2.6.2: - version "2.6.3" - resolved "https://registry.npmjs.org/async/-/async-2.6.3.tgz" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== dependencies: lodash "^4.17.14" @@ -3394,10 +3394,10 @@ brorand@^1.0.1, brorand@^1.1.0: resolved "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= -browser-fs-access@0.24.1: - version "0.24.1" - resolved "https://registry.yarnpkg.com/browser-fs-access/-/browser-fs-access-0.24.1.tgz#29b915fdcd2ef0972ebabc2b7685c6893d288a72" - integrity sha512-fRcwhfNej0h2Jy+Uodxjbc5PQvNkZyG9fXu3S6Mcv0kigKath5sL54GXfMtOay/A/ULMa956eQZ9lMWVPiLtMA== +browser-fs-access@0.29.1: + version "0.29.1" + resolved "https://registry.yarnpkg.com/browser-fs-access/-/browser-fs-access-0.29.1.tgz#8a9794c73cf86b9aec74201829999c597128379c" + integrity sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw== browser-process-hrtime@^1.0.0: version "1.0.0" @@ -8369,9 +8369,9 @@ minimatch@3.0.4, minimatch@^3.0.4: brace-expansion "^1.1.7" minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== minipass-collect@^1.0.2: version "1.0.2" @@ -8510,10 +8510,10 @@ nan@^2.12.1: resolved "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== -nanoid@3.1.32, nanoid@^3.1.20: - version "3.1.32" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.32.tgz#8f96069e6239cc0a9ae8c0d3b41a3b4933a88c0a" - integrity sha512-F8mf7R3iT9bvThBoW4tGXhXFHCctyCiUUPrWF8WaTqa3h96d9QybkSeba43XVOOE3oiLfkVDe4bT8MeGmkrTxw== +nanoid@3.3.3, nanoid@^3.1.20: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== nanomatch@^1.2.9: version "1.2.13"