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
This commit is contained in:
Aakansha Doshi 2022-08-20 22:49:44 +05:30 committed by GitHub
parent 46a61ad4df
commit ec350ba8b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 96 additions and 80 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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