Selaa lähdekoodia

feat: Introduce ExcalidrawElements and ExcalidrawAppState provider (#5463)

* feat: Introduce ExcalidrawData provider so that app state and elements need not be passed to children

* fix

* fix zen mode

* Separate providers for data and elements

* pass appState and elements to layerUI

* pass appState and elements to selectedShapeActions

* pass appState and elements to MobileMenu

* pass appState to librarymenu

* rename

* rename to ExcalidrawAppState
Aakansha Doshi 2 vuotta sitten
vanhempi
commit
ec350ba8b2

+ 10 - 12
src/components/Actions.tsx

@@ -31,12 +31,10 @@ export const SelectedShapeActions = ({
   appState,
   elements,
   renderAction,
-  activeTool,
 }: {
   appState: AppState;
   elements: readonly ExcalidrawElement[];
   renderAction: ActionManager["renderAction"];
-  activeTool: AppState["activeTool"]["type"];
 }) => {
   const targetElements = getTargetElements(
     getNonDeletedElements(elements),
@@ -56,13 +54,13 @@ export const SelectedShapeActions = ({
   const isRTL = document.documentElement.getAttribute("dir") === "rtl";
 
   const showFillIcons =
-    hasBackground(activeTool) ||
+    hasBackground(appState.activeTool.type) ||
     targetElements.some(
       (element) =>
         hasBackground(element.type) && !isTransparent(element.backgroundColor),
     );
   const showChangeBackgroundIcons =
-    hasBackground(activeTool) ||
+    hasBackground(appState.activeTool.type) ||
     targetElements.some((element) => hasBackground(element.type));
 
   const showLinkIcon =
@@ -79,23 +77,23 @@ export const SelectedShapeActions = ({
 
   return (
     <div className="panelColumn">
-      {((hasStrokeColor(activeTool) &&
-        activeTool !== "image" &&
+      {((hasStrokeColor(appState.activeTool.type) &&
+        appState.activeTool.type !== "image" &&
         commonSelectedType !== "image") ||
         targetElements.some((element) => hasStrokeColor(element.type))) &&
         renderAction("changeStrokeColor")}
       {showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
       {showFillIcons && renderAction("changeFillStyle")}
 
-      {(hasStrokeWidth(activeTool) ||
+      {(hasStrokeWidth(appState.activeTool.type) ||
         targetElements.some((element) => hasStrokeWidth(element.type))) &&
         renderAction("changeStrokeWidth")}
 
-      {(activeTool === "freedraw" ||
+      {(appState.activeTool.type === "freedraw" ||
         targetElements.some((element) => element.type === "freedraw")) &&
         renderAction("changeStrokeShape")}
 
-      {(hasStrokeStyle(activeTool) ||
+      {(hasStrokeStyle(appState.activeTool.type) ||
         targetElements.some((element) => hasStrokeStyle(element.type))) && (
         <>
           {renderAction("changeStrokeStyle")}
@@ -103,12 +101,12 @@ export const SelectedShapeActions = ({
         </>
       )}
 
-      {(canChangeSharpness(activeTool) ||
+      {(canChangeSharpness(appState.activeTool.type) ||
         targetElements.some((element) => canChangeSharpness(element.type))) && (
         <>{renderAction("changeSharpness")}</>
       )}
 
-      {(hasText(activeTool) ||
+      {(hasText(appState.activeTool.type) ||
         targetElements.some((element) => hasText(element.type))) && (
         <>
           {renderAction("changeFontSize")}
@@ -123,7 +121,7 @@ export const SelectedShapeActions = ({
         (element) =>
           hasBoundTextElement(element) || isBoundToContainer(element),
       ) && renderAction("changeVerticalAlign")}
-      {(canHaveArrowheads(activeTool) ||
+      {(canHaveArrowheads(appState.activeTool.type) ||
         targetElements.some((element) => canHaveArrowheads(element.type))) && (
         <>{renderAction("changeArrowhead")}</>
       )}

+ 80 - 57
src/components/App.tsx

@@ -272,6 +272,7 @@ const deviceContextInitialValue = {
 };
 const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
 export const useDevice = () => useContext<Device>(DeviceContext);
+
 const ExcalidrawContainerContext = React.createContext<{
   container: HTMLDivElement | null;
   id: string | null;
@@ -279,6 +280,22 @@ const ExcalidrawContainerContext = React.createContext<{
 export const useExcalidrawContainer = () =>
   useContext(ExcalidrawContainerContext);
 
+const ExcalidrawElementsContext = React.createContext<
+  readonly NonDeletedExcalidrawElement[]
+>([]);
+
+const ExcalidrawAppStateContext = React.createContext<AppState>({
+  ...getDefaultAppState(),
+  width: 0,
+  height: 0,
+  offsetLeft: 0,
+  offsetTop: 0,
+});
+export const useExcalidrawElements = () =>
+  useContext(ExcalidrawElementsContext);
+export const useExcalidrawAppState = () =>
+  useContext(ExcalidrawAppStateContext);
+
 let didTapTwice: boolean = false;
 let tappedTwiceTimer = 0;
 let cursorX = 0;
@@ -505,63 +522,69 @@ class App extends React.Component<AppProps, AppState> {
           value={this.excalidrawContainerValue}
         >
           <DeviceContext.Provider value={this.device}>
-            <LayerUI
-              canvas={this.canvas}
-              appState={this.state}
-              files={this.files}
-              setAppState={this.setAppState}
-              actionManager={this.actionManager}
-              elements={this.scene.getNonDeletedElements()}
-              onCollabButtonClick={onCollabButtonClick}
-              onLockToggle={this.toggleLock}
-              onPenModeToggle={this.togglePenMode}
-              onInsertElements={(elements) =>
-                this.addElementsFromPasteOrLibrary({
-                  elements,
-                  position: "center",
-                  files: null,
-                })
-              }
-              langCode={getLanguage().code}
-              isCollaborating={this.props.isCollaborating}
-              renderTopRightUI={renderTopRightUI}
-              renderCustomFooter={renderFooter}
-              renderCustomStats={renderCustomStats}
-              showExitZenModeBtn={
-                typeof this.props?.zenModeEnabled === "undefined" &&
-                this.state.zenModeEnabled
-              }
-              showThemeBtn={
-                typeof this.props?.theme === "undefined" &&
-                this.props.UIOptions.canvasActions.theme
-              }
-              libraryReturnUrl={this.props.libraryReturnUrl}
-              UIOptions={this.props.UIOptions}
-              focusContainer={this.focusContainer}
-              library={this.library}
-              id={this.id}
-              onImageAction={this.onImageAction}
-            />
-            <div className="excalidraw-textEditorContainer" />
-            <div className="excalidraw-contextMenuContainer" />
-            {selectedElement.length === 1 && this.state.showHyperlinkPopup && (
-              <Hyperlink
-                key={selectedElement[0].id}
-                element={selectedElement[0]}
-                appState={this.state}
-                setAppState={this.setAppState}
-                onLinkOpen={this.props.onLinkOpen}
-              />
-            )}
-            {this.state.toast !== null && (
-              <Toast
-                message={this.state.toast.message}
-                onClose={() => this.setToast(null)}
-                duration={this.state.toast.duration}
-                closable={this.state.toast.closable}
-              />
-            )}
-            <main>{this.renderCanvas()}</main>
+            <ExcalidrawAppStateContext.Provider value={this.state}>
+              <ExcalidrawElementsContext.Provider
+                value={this.scene.getNonDeletedElements()}
+              >
+                <LayerUI
+                  canvas={this.canvas}
+                  appState={this.state}
+                  files={this.files}
+                  setAppState={this.setAppState}
+                  actionManager={this.actionManager}
+                  elements={this.scene.getNonDeletedElements()}
+                  onCollabButtonClick={onCollabButtonClick}
+                  onLockToggle={this.toggleLock}
+                  onPenModeToggle={this.togglePenMode}
+                  onInsertElements={(elements) =>
+                    this.addElementsFromPasteOrLibrary({
+                      elements,
+                      position: "center",
+                      files: null,
+                    })
+                  }
+                  langCode={getLanguage().code}
+                  isCollaborating={this.props.isCollaborating}
+                  renderTopRightUI={renderTopRightUI}
+                  renderCustomFooter={renderFooter}
+                  renderCustomStats={renderCustomStats}
+                  showExitZenModeBtn={
+                    typeof this.props?.zenModeEnabled === "undefined" &&
+                    this.state.zenModeEnabled
+                  }
+                  showThemeBtn={
+                    typeof this.props?.theme === "undefined" &&
+                    this.props.UIOptions.canvasActions.theme
+                  }
+                  libraryReturnUrl={this.props.libraryReturnUrl}
+                  UIOptions={this.props.UIOptions}
+                  focusContainer={this.focusContainer}
+                  library={this.library}
+                  id={this.id}
+                  onImageAction={this.onImageAction}
+                />
+                <div className="excalidraw-textEditorContainer" />
+                <div className="excalidraw-contextMenuContainer" />
+                {selectedElement.length === 1 &&
+                  this.state.showHyperlinkPopup && (
+                    <Hyperlink
+                      key={selectedElement[0].id}
+                      element={selectedElement[0]}
+                      setAppState={this.setAppState}
+                      onLinkOpen={this.props.onLinkOpen}
+                    />
+                  )}
+                {this.state.toast !== null && (
+                  <Toast
+                    message={this.state.toast.message}
+                    onClose={() => this.setToast(null)}
+                    duration={this.state.toast.duration}
+                    closable={this.state.toast.closable}
+                  />
+                )}
+                <main>{this.renderCanvas()}</main>
+              </ExcalidrawElementsContext.Provider>{" "}
+            </ExcalidrawAppStateContext.Provider>
           </DeviceContext.Provider>
         </ExcalidrawContainerContext.Provider>
       </div>

+ 2 - 4
src/components/LayerUI.tsx

@@ -71,8 +71,8 @@ const LayerUI = ({
   appState,
   files,
   setAppState,
-  canvas,
   elements,
+  canvas,
   onCollabButtonClick,
   onLockToggle,
   onPenModeToggle,
@@ -210,8 +210,8 @@ const LayerUI = ({
             )}
           </Stack.Row>
           <BackgroundPickerAndDarkModeToggle
-            actionManager={actionManager}
             appState={appState}
+            actionManager={actionManager}
             setAppState={setAppState}
             showThemeBtn={showThemeBtn}
           />
@@ -244,7 +244,6 @@ const LayerUI = ({
           appState={appState}
           elements={elements}
           renderAction={actionManager.renderAction}
-          activeTool={appState.activeTool.type}
         />
       </Island>
     </Section>
@@ -279,7 +278,6 @@ const LayerUI = ({
       libraryReturnUrl={libraryReturnUrl}
       focusContainer={focusContainer}
       library={library}
-      theme={appState.theme}
       files={files}
       id={id}
       appState={appState}

+ 1 - 4
src/components/LibraryMenu.tsx

@@ -80,7 +80,6 @@ export const LibraryMenu = ({
   onInsertLibraryItems,
   pendingElements,
   onAddToLibrary,
-  theme,
   setAppState,
   files,
   libraryReturnUrl,
@@ -93,7 +92,6 @@ export const LibraryMenu = ({
   onClose: () => void;
   onInsertLibraryItems: (libraryItems: LibraryItems) => void;
   onAddToLibrary: () => void;
-  theme: AppState["theme"];
   files: BinaryFiles;
   setAppState: React.Component<any, AppState>["setState"];
   libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
@@ -105,7 +103,6 @@ export const LibraryMenu = ({
   const ref = useRef<HTMLDivElement | null>(null);
 
   const device = useDevice();
-
   useOnClickOutside(
     ref,
     useCallback(
@@ -290,7 +287,7 @@ export const LibraryMenu = ({
         appState={appState}
         libraryReturnUrl={libraryReturnUrl}
         library={library}
-        theme={theme}
+        theme={appState.theme}
         files={files}
         id={id}
         selectedItems={selectedItems}

+ 0 - 1
src/components/MobileMenu.tsx

@@ -221,7 +221,6 @@ export const MobileMenu = ({
                 appState={appState}
                 elements={elements}
                 renderAction={actionManager.renderAction}
-                activeTool={appState.activeTool.type}
               />
             </Section>
           ) : null}

+ 3 - 2
src/element/Hyperlink.tsx

@@ -32,6 +32,7 @@ import { getElementAbsoluteCoords } from "./";
 
 import "./Hyperlink.scss";
 import { trackEvent } from "../analytics";
+import { useExcalidrawAppState } from "../components/App";
 
 const CONTAINER_WIDTH = 320;
 const SPACE_BOTTOM = 85;
@@ -48,15 +49,15 @@ let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
 
 export const Hyperlink = ({
   element,
-  appState,
   setAppState,
   onLinkOpen,
 }: {
   element: NonDeletedExcalidrawElement;
-  appState: AppState;
   setAppState: React.Component<any, AppState>["setState"];
   onLinkOpen: ExcalidrawProps["onLinkOpen"];
 }) => {
+  const appState = useExcalidrawAppState();
+
   const linkVal = element.link || "";
 
   const [inputVal, setInputVal] = useState(linkVal);