mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 10:54:33 +01:00 
			
		
		
		
	
							
								
								
									
										23
									
								
								src/actions/actionAddToLibrary.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/actions/actionAddToLibrary.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import { register } from "./register"; | ||||
| import { getSelectedElements } from "../scene"; | ||||
| import { getNonDeletedElements } from "../element"; | ||||
| import { deepCopyElement } from "../element/newElement"; | ||||
| import { loadLibrary, saveLibrary } from "../data/localStorage"; | ||||
|  | ||||
| export const actionAddToLibrary = register({ | ||||
|   name: "addToLibrary", | ||||
|   perform: (elements, appState) => { | ||||
|     const selectedElements = getSelectedElements( | ||||
|       getNonDeletedElements(elements), | ||||
|       appState, | ||||
|     ); | ||||
|  | ||||
|     loadLibrary().then((items) => { | ||||
|       saveLibrary([...items, selectedElements.map(deepCopyElement)]); | ||||
|     }); | ||||
|  | ||||
|     return false; | ||||
|   }, | ||||
|   contextMenuOrder: 6, | ||||
|   contextItemLabel: "labels.addToLibrary", | ||||
| }); | ||||
| @@ -49,3 +49,5 @@ export { | ||||
| export { actionGroup, actionUngroup } from "./actionGroup"; | ||||
|  | ||||
| export { actionGoToCollaborator } from "./actionNavigate"; | ||||
|  | ||||
| export { actionAddToLibrary } from "./actionAddToLibrary"; | ||||
|   | ||||
| @@ -62,7 +62,8 @@ export type ActionName = | ||||
|   | "toggleShortcuts" | ||||
|   | "group" | ||||
|   | "ungroup" | ||||
|   | "goToCollaborator"; | ||||
|   | "goToCollaborator" | ||||
|   | "addToLibrary"; | ||||
|  | ||||
| export interface Action { | ||||
|   name: ActionName; | ||||
|   | ||||
| @@ -58,6 +58,7 @@ export const getDefaultAppState = (): AppState => { | ||||
|     selectedGroupIds: {}, | ||||
|     width: window.innerWidth, | ||||
|     height: window.innerHeight, | ||||
|     isLibraryOpen: false, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @@ -76,6 +77,7 @@ export const clearAppStateForLocalStorage = (appState: AppState) => { | ||||
|     errorMessage, | ||||
|     showShortcutsDialog, | ||||
|     editingLinearElement, | ||||
|     isLibraryOpen, | ||||
|     ...exportedState | ||||
|   } = appState; | ||||
|   return exportedState; | ||||
|   | ||||
| @@ -85,12 +85,21 @@ export const SelectedShapeActions = ({ | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const LIBRARY_ICON = ( | ||||
|   // fa-th-large | ||||
|   <svg viewBox="0 0 512 512"> | ||||
|     <path d="M296 32h192c13.255 0 24 10.745 24 24v160c0 13.255-10.745 24-24 24H296c-13.255 0-24-10.745-24-24V56c0-13.255 10.745-24 24-24zm-80 0H24C10.745 32 0 42.745 0 56v160c0 13.255 10.745 24 24 24h192c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24zM0 296v160c0 13.255 10.745 24 24 24h192c13.255 0 24-10.745 24-24V296c0-13.255-10.745-24-24-24H24c-13.255 0-24 10.745-24 24zm296 184h192c13.255 0 24-10.745 24-24V296c0-13.255-10.745-24-24-24H296c-13.255 0-24 10.745-24 24v160c0 13.255 10.745 24 24 24z" /> | ||||
|   </svg> | ||||
| ); | ||||
|  | ||||
| export const ShapesSwitcher = ({ | ||||
|   elementType, | ||||
|   setAppState, | ||||
|   isLibraryOpen, | ||||
| }: { | ||||
|   elementType: ExcalidrawElement["type"]; | ||||
|   setAppState: any; | ||||
|   setAppState: (appState: Partial<AppState>) => void; | ||||
|   isLibraryOpen: boolean; | ||||
| }) => ( | ||||
|   <> | ||||
|     {SHAPES.map(({ value, icon, key }, index) => { | ||||
| @@ -119,9 +128,21 @@ export const ShapesSwitcher = ({ | ||||
|             setCursorForShape(value); | ||||
|             setAppState({}); | ||||
|           }} | ||||
|         ></ToolButton> | ||||
|         /> | ||||
|       ); | ||||
|     })} | ||||
|     <ToolButton | ||||
|       type="button" | ||||
|       icon={LIBRARY_ICON} | ||||
|       name="editor-library" | ||||
|       keyBindingLabel="9" | ||||
|       aria-keyshortcuts="9" | ||||
|       title={`${capitalizeString(t("toolBar.library"))} — 9`} | ||||
|       aria-label={capitalizeString(t("toolBar.library"))} | ||||
|       onClick={() => { | ||||
|         setAppState({ isLibraryOpen: !isLibraryOpen }); | ||||
|       }} | ||||
|     /> | ||||
|   </> | ||||
| ); | ||||
|  | ||||
|   | ||||
| @@ -299,6 +299,9 @@ class App extends React.Component<ExcalidrawProps, AppState> { | ||||
|             }); | ||||
|           }} | ||||
|           onLockToggle={this.toggleLock} | ||||
|           onInsertShape={(elements) => | ||||
|             this.addElementsFromPasteOrLibrary(elements) | ||||
|           } | ||||
|           zenModeEnabled={zenModeEnabled} | ||||
|           toggleZenMode={this.toggleZenMode} | ||||
|           lng={getLanguage().lng} | ||||
| @@ -870,7 +873,7 @@ class App extends React.Component<ExcalidrawProps, AppState> { | ||||
|       if (data.error) { | ||||
|         alert(data.error); | ||||
|       } else if (data.elements) { | ||||
|         this.addElementsFromPaste(data.elements); | ||||
|         this.addElementsFromPasteOrLibrary(data.elements); | ||||
|       } else if (data.text) { | ||||
|         this.addTextFromPaste(data.text); | ||||
|       } | ||||
| @@ -879,8 +882,10 @@ class App extends React.Component<ExcalidrawProps, AppState> { | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   private addElementsFromPaste = ( | ||||
|   private addElementsFromPasteOrLibrary = ( | ||||
|     clipboardElements: readonly ExcalidrawElement[], | ||||
|     clientX = cursorX, | ||||
|     clientY = cursorY, | ||||
|   ) => { | ||||
|     const [minX, minY, maxX, maxY] = getCommonBounds(clipboardElements); | ||||
|  | ||||
| @@ -888,7 +893,7 @@ class App extends React.Component<ExcalidrawProps, AppState> { | ||||
|     const elementsCenterY = distance(minY, maxY) / 2; | ||||
|  | ||||
|     const { x, y } = viewportCoordsToSceneCoords( | ||||
|       { clientX: cursorX, clientY: cursorY }, | ||||
|       { clientX, clientY }, | ||||
|       this.state, | ||||
|       this.canvas, | ||||
|       window.devicePixelRatio, | ||||
| @@ -911,6 +916,7 @@ class App extends React.Component<ExcalidrawProps, AppState> { | ||||
|     ]); | ||||
|     history.resumeRecording(); | ||||
|     this.setState({ | ||||
|       isLibraryOpen: false, | ||||
|       selectedElementIds: newElements.reduce((map, element) => { | ||||
|         map[element.id] = true; | ||||
|         return map; | ||||
| @@ -1355,6 +1361,10 @@ class App extends React.Component<ExcalidrawProps, AppState> { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (event.code === "Digit9") { | ||||
|       this.setState({ isLibraryOpen: !this.state.isLibraryOpen }); | ||||
|     } | ||||
|  | ||||
|     const shape = findShapeByKey(event.key); | ||||
|  | ||||
|     if (isArrowKey(event.key)) { | ||||
| @@ -3135,6 +3145,18 @@ class App extends React.Component<ExcalidrawProps, AppState> { | ||||
|   }; | ||||
|  | ||||
|   private handleCanvasOnDrop = (event: React.DragEvent<HTMLCanvasElement>) => { | ||||
|     const libraryShapes = event.dataTransfer.getData( | ||||
|       "application/vnd.excalidraw.json", | ||||
|     ); | ||||
|     if (libraryShapes !== "") { | ||||
|       this.addElementsFromPasteOrLibrary( | ||||
|         JSON.parse(libraryShapes), | ||||
|         event.clientX, | ||||
|         event.clientY, | ||||
|       ); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const file = event.dataTransfer?.files[0]; | ||||
|     if ( | ||||
|       file?.type === "application/json" || | ||||
|   | ||||
| @@ -1,5 +1,22 @@ | ||||
| @import "open-color/open-color"; | ||||
|  | ||||
| .layer-ui__library { | ||||
|   margin: auto; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
| } | ||||
|  | ||||
| .layer-ui__library-message { | ||||
|   padding: 10px 20px; | ||||
|   max-width: 200px; | ||||
| } | ||||
|  | ||||
| .layer-ui__library-items { | ||||
|   max-height: 50vh; | ||||
|   overflow: auto; | ||||
| } | ||||
|  | ||||
| .layer-ui__wrapper { | ||||
|   .encrypted-icon { | ||||
|     position: relative; | ||||
|   | ||||
| @@ -1,10 +1,20 @@ | ||||
| import React from "react"; | ||||
| import React, { | ||||
|   useRef, | ||||
|   useState, | ||||
|   RefObject, | ||||
|   useEffect, | ||||
|   useCallback, | ||||
| } from "react"; | ||||
| import { showSelectedShapeActions } from "../element"; | ||||
| import { calculateScrollCenter } from "../scene"; | ||||
| import { calculateScrollCenter, getSelectedElements } from "../scene"; | ||||
| import { exportCanvas } from "../data"; | ||||
|  | ||||
| import { AppState } from "../types"; | ||||
| import { NonDeletedExcalidrawElement } from "../element/types"; | ||||
| import { AppState, LibraryItems } from "../types"; | ||||
| import { | ||||
|   NonDeletedExcalidrawElement, | ||||
|   ExcalidrawElement, | ||||
|   NonDeleted, | ||||
| } from "../element/types"; | ||||
|  | ||||
| import { ActionManager } from "../actions/manager"; | ||||
| import { Island } from "./Island"; | ||||
| @@ -32,6 +42,8 @@ import { GitHubCorner } from "./GitHubCorner"; | ||||
| import { Tooltip } from "./Tooltip"; | ||||
|  | ||||
| import "./LayerUI.scss"; | ||||
| import { LibraryUnit } from "./LibraryUnit"; | ||||
| import { loadLibrary, saveLibrary } from "../data/localStorage"; | ||||
|  | ||||
| interface LayerUIProps { | ||||
|   actionManager: ActionManager; | ||||
| @@ -43,11 +55,182 @@ interface LayerUIProps { | ||||
|   onUsernameChange: (username: string) => void; | ||||
|   onRoomDestroy: () => void; | ||||
|   onLockToggle: () => void; | ||||
|   onInsertShape: (elements: readonly NonDeleted<ExcalidrawElement>[]) => void; | ||||
|   zenModeEnabled: boolean; | ||||
|   toggleZenMode: () => void; | ||||
|   lng: string; | ||||
| } | ||||
|  | ||||
| function useOnClickOutside( | ||||
|   ref: RefObject<HTMLElement>, | ||||
|   cb: (event: MouseEvent) => void, | ||||
| ) { | ||||
|   useEffect(() => { | ||||
|     const listener = (event: MouseEvent) => { | ||||
|       if (!ref.current) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if ( | ||||
|         event.target instanceof Element && | ||||
|         (ref.current.contains(event.target) || | ||||
|           !document.body.contains(event.target)) | ||||
|       ) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       cb(event); | ||||
|     }; | ||||
|     document.addEventListener("pointerdown", listener, false); | ||||
|  | ||||
|     return () => { | ||||
|       document.removeEventListener("pointerdown", listener); | ||||
|     }; | ||||
|   }, [ref, cb]); | ||||
| } | ||||
|  | ||||
| const LibraryMenuItems = ({ | ||||
|   library, | ||||
|   onRemoveFromLibrary, | ||||
|   onAddToLibrary, | ||||
|   onInsertShape, | ||||
|   pendingElements, | ||||
| }: { | ||||
|   library: LibraryItems; | ||||
|   pendingElements: NonDeleted<ExcalidrawElement>[]; | ||||
|   onClickOutside: (event: MouseEvent) => void; | ||||
|   onRemoveFromLibrary: (index: number) => void; | ||||
|   onInsertShape: (elements: readonly NonDeleted<ExcalidrawElement>[]) => void; | ||||
|   onAddToLibrary: (elements: NonDeleted<ExcalidrawElement>[]) => void; | ||||
| }) => { | ||||
|   const numCells = library.length + (pendingElements.length > 0 ? 1 : 0); | ||||
|   const CELLS_PER_ROW = 3; | ||||
|   const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW)); | ||||
|   const rows = []; | ||||
|   let addedPendingElements = false; | ||||
|  | ||||
|   for (let row = 0; row < numRows; row++) { | ||||
|     const i = CELLS_PER_ROW * row; | ||||
|     const children = []; | ||||
|     for (let j = 0; j < 3; j++) { | ||||
|       const shouldAddPendingElements: boolean = | ||||
|         pendingElements.length > 0 && | ||||
|         !addedPendingElements && | ||||
|         i + j >= library.length; | ||||
|       addedPendingElements = addedPendingElements || shouldAddPendingElements; | ||||
|  | ||||
|       children.push( | ||||
|         <Stack.Col key={j}> | ||||
|           <LibraryUnit | ||||
|             elements={library[i + j]} | ||||
|             pendingElements={ | ||||
|               shouldAddPendingElements ? pendingElements : undefined | ||||
|             } | ||||
|             onRemoveFromLibrary={onRemoveFromLibrary.bind(null, i + j)} | ||||
|             onClick={ | ||||
|               shouldAddPendingElements | ||||
|                 ? onAddToLibrary.bind(null, pendingElements) | ||||
|                 : onInsertShape.bind(null, library[i + j]) | ||||
|             } | ||||
|           /> | ||||
|         </Stack.Col>, | ||||
|       ); | ||||
|     } | ||||
|     rows.push( | ||||
|       <Stack.Row align="center" gap={1} key={row}> | ||||
|         {children} | ||||
|       </Stack.Row>, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Stack.Col align="center" gap={1} className="layer-ui__library-items"> | ||||
|       {rows} | ||||
|     </Stack.Col> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const LibraryMenu = ({ | ||||
|   onClickOutside, | ||||
|   onInsertShape, | ||||
|   pendingElements, | ||||
|   onAddToLibrary, | ||||
| }: { | ||||
|   pendingElements: NonDeleted<ExcalidrawElement>[]; | ||||
|   onClickOutside: (event: MouseEvent) => void; | ||||
|   onInsertShape: (elements: readonly NonDeleted<ExcalidrawElement>[]) => void; | ||||
|   onAddToLibrary: () => void; | ||||
| }) => { | ||||
|   const ref = useRef<HTMLDivElement | null>(null); | ||||
|   useOnClickOutside(ref, onClickOutside); | ||||
|  | ||||
|   const [libraryItems, setLibraryItems] = useState<LibraryItems>([]); | ||||
|  | ||||
|   const [loadingState, setIsLoading] = useState< | ||||
|     "preloading" | "loading" | "ready" | ||||
|   >("preloading"); | ||||
|  | ||||
|   const loadingTimerRef = useRef<NodeJS.Timeout | null>(null); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     Promise.race([ | ||||
|       new Promise((resolve) => { | ||||
|         loadingTimerRef.current = setTimeout(() => { | ||||
|           resolve("loading"); | ||||
|         }, 100); | ||||
|       }), | ||||
|       loadLibrary().then((items) => { | ||||
|         setLibraryItems(items); | ||||
|         setIsLoading("ready"); | ||||
|       }), | ||||
|     ]).then((data) => { | ||||
|       if (data === "loading") { | ||||
|         setIsLoading("loading"); | ||||
|       } | ||||
|     }); | ||||
|     return () => { | ||||
|       clearTimeout(loadingTimerRef.current!); | ||||
|     }; | ||||
|   }, []); | ||||
|  | ||||
|   const removeFromLibrary = useCallback(async (indexToRemove) => { | ||||
|     const items = await loadLibrary(); | ||||
|     const nextItems = items.filter((_, index) => index !== indexToRemove); | ||||
|     saveLibrary(nextItems); | ||||
|     setLibraryItems(nextItems); | ||||
|   }, []); | ||||
|  | ||||
|   const addToLibrary = useCallback( | ||||
|     async (elements: NonDeleted<ExcalidrawElement>[]) => { | ||||
|       const items = await loadLibrary(); | ||||
|       const nextItems = [...items, elements]; | ||||
|       onAddToLibrary(); | ||||
|       saveLibrary(nextItems); | ||||
|       setLibraryItems(nextItems); | ||||
|     }, | ||||
|     [onAddToLibrary], | ||||
|   ); | ||||
|  | ||||
|   return loadingState === "preloading" ? null : ( | ||||
|     <Island padding={1} ref={ref} className="layer-ui__library"> | ||||
|       {loadingState === "loading" ? ( | ||||
|         <div className="layer-ui__library-message"> | ||||
|           {t("labels.libraryLoadingMessage")} | ||||
|         </div> | ||||
|       ) : ( | ||||
|         <LibraryMenuItems | ||||
|           library={libraryItems} | ||||
|           onClickOutside={onClickOutside} | ||||
|           onRemoveFromLibrary={removeFromLibrary} | ||||
|           onAddToLibrary={addToLibrary} | ||||
|           onInsertShape={onInsertShape} | ||||
|           pendingElements={pendingElements} | ||||
|         /> | ||||
|       )} | ||||
|     </Island> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const LayerUI = ({ | ||||
|   actionManager, | ||||
|   appState, | ||||
| @@ -58,6 +241,7 @@ const LayerUI = ({ | ||||
|   onUsernameChange, | ||||
|   onRoomDestroy, | ||||
|   onLockToggle, | ||||
|   onInsertShape, | ||||
|   zenModeEnabled, | ||||
|   toggleZenMode, | ||||
| }: LayerUIProps) => { | ||||
| @@ -167,11 +351,33 @@ const LayerUI = ({ | ||||
|     </Section> | ||||
|   ); | ||||
|  | ||||
|   const closeLibrary = useCallback( | ||||
|     (event) => { | ||||
|       setAppState({ isLibraryOpen: false }); | ||||
|     }, | ||||
|     [setAppState], | ||||
|   ); | ||||
|  | ||||
|   const deselectItems = useCallback(() => { | ||||
|     setAppState({ | ||||
|       selectedElementIds: {}, | ||||
|       selectedGroupIds: {}, | ||||
|     }); | ||||
|   }, [setAppState]); | ||||
|  | ||||
|   const renderFixedSideContainer = () => { | ||||
|     const shouldRenderSelectedShapeActions = showSelectedShapeActions( | ||||
|       appState, | ||||
|       elements, | ||||
|     ); | ||||
|     const libraryMenu = appState.isLibraryOpen ? ( | ||||
|       <LibraryMenu | ||||
|         pendingElements={getSelectedElements(elements, appState)} | ||||
|         onClickOutside={closeLibrary} | ||||
|         onInsertShape={onInsertShape} | ||||
|         onAddToLibrary={deselectItems} | ||||
|       /> | ||||
|     ) : null; | ||||
|     return ( | ||||
|       <FixedSideContainer side="top"> | ||||
|         <HintViewer appState={appState} elements={elements} /> | ||||
| @@ -193,6 +399,7 @@ const LayerUI = ({ | ||||
|                       <ShapesSwitcher | ||||
|                         elementType={appState.elementType} | ||||
|                         setAppState={setAppState} | ||||
|                         isLibraryOpen={appState.isLibraryOpen} | ||||
|                       /> | ||||
|                     </Stack.Row> | ||||
|                   </Island> | ||||
| @@ -203,6 +410,7 @@ const LayerUI = ({ | ||||
|                     title={t("toolBar.lock")} | ||||
|                   /> | ||||
|                 </Stack.Row> | ||||
|                 {libraryMenu} | ||||
|               </Stack.Col> | ||||
|             )} | ||||
|           </Section> | ||||
|   | ||||
							
								
								
									
										75
									
								
								src/components/LibraryUnit.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/components/LibraryUnit.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| .library-unit { | ||||
|   align-items: center; | ||||
|   border: 1px solid #ccc; | ||||
|   display: flex; | ||||
|   height: 126px; // match width | ||||
|   justify-content: center; | ||||
|   position: relative; | ||||
|   width: 126px; // exactly match the toolbar width when 3 are lined up + padding | ||||
| } | ||||
|  | ||||
| .library-unit__dragger { | ||||
|   display: flex; | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .library-unit__dragger > svg { | ||||
|   flex-grow: 1; | ||||
|   max-height: 100%; | ||||
|   max-width: 100%; | ||||
| } | ||||
|  | ||||
| .library-unit__removeFromLibrary, | ||||
| .library-unit__removeFromLibrary:hover, | ||||
| .library-unit__removeFromLibrary:active { | ||||
|   align-items: center; | ||||
|   background: none; | ||||
|   border: none; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   position: absolute; | ||||
|   right: 5px; | ||||
|   top: 5px; | ||||
| } | ||||
|  | ||||
| .library-unit__removeFromLibrary > svg { | ||||
|   height: 16px; | ||||
|   width: 16px; | ||||
| } | ||||
|  | ||||
| .library-unit__pulse { | ||||
|   transform: scale(1); | ||||
|   animation: library-unit__pulse-animation 1s ease-in infinite; | ||||
| } | ||||
|  | ||||
| .library-unit__adder { | ||||
|   position: absolute; | ||||
|   left: 50%; | ||||
|   top: 50%; | ||||
|   width: 20px; | ||||
|   height: 20px; | ||||
|   margin-left: -10px; | ||||
|   margin-top: -10px; | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| .library-unit__active { | ||||
|   cursor: pointer; | ||||
| } | ||||
|  | ||||
| @keyframes library-unit__pulse-animation { | ||||
|   0% { | ||||
|     transform: scale(0.95); | ||||
|   } | ||||
|  | ||||
|   50% { | ||||
|     transform: scale(1); | ||||
|   } | ||||
|  | ||||
|   100% { | ||||
|     transform: scale(0.95); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										93
									
								
								src/components/LibraryUnit.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/components/LibraryUnit.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| import React, { useRef, useEffect, useState } from "react"; | ||||
| import { exportToSvg } from "../scene/export"; | ||||
| import { ExcalidrawElement, NonDeleted } from "../element/types"; | ||||
| import { close } from "../components/icons"; | ||||
|  | ||||
| import "./LibraryUnit.scss"; | ||||
| import { t } from "../i18n"; | ||||
|  | ||||
| // fa-plus | ||||
| const PLUS_ICON = ( | ||||
|   <svg viewBox="0 0 1792 1792"> | ||||
|     <path d="M1600 736v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z" /> | ||||
|   </svg> | ||||
| ); | ||||
|  | ||||
| export const LibraryUnit = ({ | ||||
|   elements, | ||||
|   pendingElements, | ||||
|   onRemoveFromLibrary, | ||||
|   onClick, | ||||
| }: { | ||||
|   elements?: NonDeleted<ExcalidrawElement>[]; | ||||
|   pendingElements?: NonDeleted<ExcalidrawElement>[]; | ||||
|   onRemoveFromLibrary: () => void; | ||||
|   onClick: () => void; | ||||
| }) => { | ||||
|   const ref = useRef<HTMLDivElement | null>(null); | ||||
|   useEffect(() => { | ||||
|     const elementsToRender = elements || pendingElements; | ||||
|     if (!elementsToRender) { | ||||
|       return; | ||||
|     } | ||||
|     const svg = exportToSvg(elementsToRender, { | ||||
|       exportBackground: false, | ||||
|       viewBackgroundColor: "#fff", | ||||
|       shouldAddWatermark: false, | ||||
|     }); | ||||
|     for (const child of ref.current!.children) { | ||||
|       if (child.tagName !== "svg") { | ||||
|         continue; | ||||
|       } | ||||
|       ref.current!.removeChild(child); | ||||
|     } | ||||
|     ref.current!.appendChild(svg); | ||||
|  | ||||
|     const current = ref.current!; | ||||
|     return () => { | ||||
|       current.removeChild(svg); | ||||
|     }; | ||||
|   }, [elements, pendingElements]); | ||||
|  | ||||
|   const [isHovered, setIsHovered] = useState(false); | ||||
|  | ||||
|   const adder = isHovered && pendingElements && ( | ||||
|     <div className="library-unit__adder">{PLUS_ICON}</div> | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
|       className={`library-unit ${ | ||||
|         elements || pendingElements ? "library-unit__active" : "" | ||||
|       }`} | ||||
|       onMouseEnter={() => setIsHovered(true)} | ||||
|       onMouseLeave={() => setIsHovered(false)} | ||||
|     > | ||||
|       <div | ||||
|         className={`library-unit__dragger ${ | ||||
|           !!pendingElements ? "library-unit__pulse" : "" | ||||
|         }`} | ||||
|         ref={ref} | ||||
|         draggable={!!elements} | ||||
|         onClick={!!elements || !!pendingElements ? onClick : undefined} | ||||
|         onDragStart={(event) => { | ||||
|           setIsHovered(false); | ||||
|           event.dataTransfer.setData( | ||||
|             "application/vnd.excalidraw.json", | ||||
|             JSON.stringify(elements), | ||||
|           ); | ||||
|         }} | ||||
|       /> | ||||
|       {adder} | ||||
|       {elements && isHovered && ( | ||||
|         <button | ||||
|           className="library-unit__removeFromLibrary" | ||||
|           aria-label={t("labels.removeFromLibrary")} | ||||
|           onClick={onRemoveFromLibrary} | ||||
|         > | ||||
|           {close} | ||||
|         </button> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -56,6 +56,7 @@ export const MobileMenu = ({ | ||||
|                   <ShapesSwitcher | ||||
|                     elementType={appState.elementType} | ||||
|                     setAppState={setAppState} | ||||
|                     isLibraryOpen={appState.isLibraryOpen} | ||||
|                   /> | ||||
|                 </Stack.Row> | ||||
|               </Island> | ||||
|   | ||||
| @@ -63,6 +63,11 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => { | ||||
|       > | ||||
|         <div className="ToolIcon__icon" aria-hidden="true"> | ||||
|           {props.icon || props.label} | ||||
|           {props.keyBindingLabel && ( | ||||
|             <span className="ToolIcon__keybinding"> | ||||
|               {props.keyBindingLabel} | ||||
|             </span> | ||||
|           )} | ||||
|         </div> | ||||
|         {props.showAriaLabel && ( | ||||
|           <div className="ToolIcon__label">{props["aria-label"]}</div> | ||||
|   | ||||
| @@ -348,11 +348,12 @@ export const exportCanvas = async ( | ||||
|       window.alert(t("alerts.couldNotCopyToClipboard")); | ||||
|     } | ||||
|   } else if (type === "backend") { | ||||
|     const appState = getDefaultAppState(); | ||||
|     if (exportBackground) { | ||||
|       appState.viewBackgroundColor = viewBackgroundColor; | ||||
|     } | ||||
|     exportToBackend(elements, appState); | ||||
|     exportToBackend(elements, { | ||||
|       ...appState, | ||||
|       viewBackgroundColor: exportBackground | ||||
|         ? appState.viewBackgroundColor | ||||
|         : getDefaultAppState().viewBackgroundColor, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   // clean up the DOM | ||||
|   | ||||
| @@ -1,11 +1,54 @@ | ||||
| import { ExcalidrawElement } from "../element/types"; | ||||
| import { AppState } from "../types"; | ||||
| import { AppState, LibraryItems } from "../types"; | ||||
| import { clearAppStateForLocalStorage } from "../appState"; | ||||
| import { restore } from "./restore"; | ||||
|  | ||||
| const LOCAL_STORAGE_KEY = "excalidraw"; | ||||
| const LOCAL_STORAGE_KEY_STATE = "excalidraw-state"; | ||||
| const LOCAL_STORAGE_KEY_COLLAB = "excalidraw-collab"; | ||||
| const LOCAL_STORAGE_KEY_LIBRARY = "excalidraw-library"; | ||||
|  | ||||
| let _LATEST_LIBRARY_ITEMS: LibraryItems | null = null; | ||||
| export const loadLibrary = (): Promise<LibraryItems> => { | ||||
|   return new Promise(async (resolve) => { | ||||
|     if (_LATEST_LIBRARY_ITEMS) { | ||||
|       return resolve(JSON.parse(JSON.stringify(_LATEST_LIBRARY_ITEMS))); | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       const data = localStorage.getItem(LOCAL_STORAGE_KEY_LIBRARY); | ||||
|       if (!data) { | ||||
|         return resolve([]); | ||||
|       } | ||||
|  | ||||
|       const items = (JSON.parse(data) as ExcalidrawElement[][]).map( | ||||
|         (elements) => restore(elements, null).elements, | ||||
|       ) as Mutable<LibraryItems>; | ||||
|  | ||||
|       // clone to ensure we don't mutate the cached library elements in the app | ||||
|       _LATEST_LIBRARY_ITEMS = JSON.parse(JSON.stringify(items)); | ||||
|  | ||||
|       resolve(items); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       resolve([]); | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| export const saveLibrary = (items: LibraryItems) => { | ||||
|   const prevLibraryItems = _LATEST_LIBRARY_ITEMS; | ||||
|   try { | ||||
|     const serializedItems = JSON.stringify(items); | ||||
|     // cache optimistically so that consumers have access to the latest | ||||
|     //  immediately | ||||
|     _LATEST_LIBRARY_ITEMS = JSON.parse(serializedItems); | ||||
|     localStorage.setItem(LOCAL_STORAGE_KEY_LIBRARY, serializedItems); | ||||
|   } catch (e) { | ||||
|     _LATEST_LIBRARY_ITEMS = prevLibraryItems; | ||||
|     console.error(e); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const saveUsernameToLocalStorage = (username: string) => { | ||||
|   try { | ||||
|   | ||||
| @@ -65,7 +65,10 @@ | ||||
|     "group": "Group selection", | ||||
|     "ungroup": "Ungroup selection", | ||||
|     "collaborators": "Collaborators", | ||||
|     "toggleGridMode": "Toggle grid mode" | ||||
|     "toggleGridMode": "Toggle grid mode", | ||||
|     "addToLibrary": "Add to library", | ||||
|     "removeFromLibrary": "Remove from library", | ||||
|     "libraryLoadingMessage": "Loading library..." | ||||
|   }, | ||||
|   "buttons": { | ||||
|     "clearReset": "Reset the canvas", | ||||
| @@ -115,6 +118,7 @@ | ||||
|     "arrow": "Arrow", | ||||
|     "line": "Line", | ||||
|     "text": "Text", | ||||
|     "library": "Library", | ||||
|     "lock": "Keep selected tool active after drawing" | ||||
|   }, | ||||
|   "headings": { | ||||
|   | ||||
| @@ -27,6 +27,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -427,6 +428,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -636,6 +638,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -762,6 +765,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -1024,6 +1028,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -1188,6 +1193,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -1390,6 +1396,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -1598,6 +1605,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -1907,6 +1915,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -2302,6 +2311,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -4090,6 +4100,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -4216,6 +4227,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -4342,6 +4354,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -4468,6 +4481,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -4616,6 +4630,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -4764,6 +4779,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -4912,6 +4928,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -5060,6 +5077,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -5186,6 +5204,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -5312,6 +5331,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -5460,6 +5480,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -5586,6 +5607,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -5734,6 +5756,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -6374,6 +6397,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -6583,6 +6607,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -6650,6 +6675,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -6715,6 +6741,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -7537,6 +7564,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -7936,6 +7964,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -8252,6 +8281,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -8489,6 +8519,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -8651,6 +8682,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -9422,6 +9454,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -10094,6 +10127,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -10671,6 +10705,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -11157,6 +11192,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -11599,6 +11635,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -11956,6 +11993,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -12232,6 +12270,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -12431,6 +12470,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -13253,6 +13293,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -13974,6 +14015,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -14598,6 +14640,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -15129,6 +15172,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -15402,6 +15446,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -15613,6 +15658,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -15892,6 +15938,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -15957,6 +16004,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -16083,6 +16131,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -16148,6 +16197,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -16802,6 +16852,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -16869,6 +16920,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -17297,6 +17349,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
| @@ -17373,6 +17426,7 @@ Object { | ||||
|   "gridSize": null, | ||||
|   "height": 768, | ||||
|   "isCollaborating": false, | ||||
|   "isLibraryOpen": false, | ||||
|   "isLoading": false, | ||||
|   "isResizing": false, | ||||
|   "isRotating": false, | ||||
|   | ||||
| @@ -884,6 +884,7 @@ describe("regression tests", () => { | ||||
|       "Copy styles", | ||||
|       "Paste styles", | ||||
|       "Delete", | ||||
|       "Add to library", | ||||
|       "Send backward", | ||||
|       "Bring forward", | ||||
|       "Send to back", | ||||
| @@ -892,7 +893,7 @@ describe("regression tests", () => { | ||||
|     ]; | ||||
|  | ||||
|     expect(contextMenu).not.toBeNull(); | ||||
|     expect(contextMenu?.children.length).toBe(8); | ||||
|     expect(contextMenu?.children.length).toBe(9); | ||||
|     options?.forEach((opt, i) => { | ||||
|       expect(opt.textContent).toBe(expectedOptions[i]); | ||||
|     }); | ||||
| @@ -926,6 +927,7 @@ describe("regression tests", () => { | ||||
|       "Paste styles", | ||||
|       "Delete", | ||||
|       "Group selection", | ||||
|       "Add to library", | ||||
|       "Send backward", | ||||
|       "Bring forward", | ||||
|       "Send to back", | ||||
| @@ -934,7 +936,7 @@ describe("regression tests", () => { | ||||
|     ]; | ||||
|  | ||||
|     expect(contextMenu).not.toBeNull(); | ||||
|     expect(contextMenu?.children.length).toBe(9); | ||||
|     expect(contextMenu?.children.length).toBe(10); | ||||
|     options?.forEach((opt, i) => { | ||||
|       expect(opt.textContent).toBe(expectedOptions[i]); | ||||
|     }); | ||||
| @@ -973,6 +975,7 @@ describe("regression tests", () => { | ||||
|       "Delete", | ||||
|       "Group selection", | ||||
|       "Ungroup selection", | ||||
|       "Add to library", | ||||
|       "Send backward", | ||||
|       "Bring forward", | ||||
|       "Send to back", | ||||
| @@ -981,7 +984,7 @@ describe("regression tests", () => { | ||||
|     ]; | ||||
|  | ||||
|     expect(contextMenu).not.toBeNull(); | ||||
|     expect(contextMenu?.children.length).toBe(10); | ||||
|     expect(contextMenu?.children.length).toBe(11); | ||||
|     options?.forEach((opt, i) => { | ||||
|       expect(opt.textContent).toBe(expectedOptions[i]); | ||||
|     }); | ||||
|   | ||||
| @@ -81,6 +81,8 @@ export type AppState = { | ||||
|   editingGroupId: GroupId | null; | ||||
|   width: number; | ||||
|   height: number; | ||||
|  | ||||
|   isLibraryOpen: boolean; | ||||
| }; | ||||
|  | ||||
| export type PointerCoords = Readonly<{ | ||||
| @@ -103,3 +105,5 @@ export declare class GestureEvent extends UIEvent { | ||||
| export type SocketUpdateData = SocketUpdateDataSource[keyof SocketUpdateDataSource] & { | ||||
|   _brand: "socketUpdateData"; | ||||
| }; | ||||
|  | ||||
| export type LibraryItems = readonly NonDeleted<ExcalidrawElement>[][]; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Pete Hunt
					Pete Hunt