react.memo (#79)

This commit is contained in:
Mike Sawka 2024-06-26 09:31:43 -07:00 committed by GitHub
parent f036459dd5
commit 4f627a0342
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 322 additions and 262 deletions

View File

@ -174,7 +174,7 @@ interface FramelessBlockHeaderProps {
dragHandleRef?: React.RefObject<HTMLDivElement>;
}
const FramelessBlockHeader = ({ blockId, onClose, dragHandleRef }: FramelessBlockHeaderProps) => {
const FramelessBlockHeader = React.memo(({ blockId, onClose, dragHandleRef }: FramelessBlockHeaderProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
@ -193,7 +193,7 @@ const FramelessBlockHeader = ({ blockId, onClose, dragHandleRef }: FramelessBloc
)}
</div>
);
};
});
const hoverStateOff = "off";
const hoverStatePending = "pending";
@ -209,62 +209,56 @@ interface BlockFrameProps {
dragHandleRef?: React.RefObject<HTMLDivElement>;
}
const BlockFrame_Tech = ({
blockId,
onClose,
onClick,
preview,
blockRef,
dragHandleRef,
children,
}: BlockFrameProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
return jotai.atom((get) => {
const winData = get(atoms.waveWindow);
return winData.activeblockid === blockId;
const BlockFrame_Tech = React.memo(
({ blockId, onClose, onClick, preview, blockRef, dragHandleRef, children }: BlockFrameProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
return jotai.atom((get) => {
const winData = get(atoms.waveWindow);
return winData.activeblockid === blockId;
});
});
});
let isFocused = jotai.useAtomValue(isFocusedAtom);
const blockIcon = useBlockIcon(blockId);
let isFocused = jotai.useAtomValue(isFocusedAtom);
const blockIcon = useBlockIcon(blockId);
if (preview) {
isFocused = true;
}
let style: React.CSSProperties = {};
if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
style.borderColor = blockData.meta["frame:bordercolor"];
}
if (isFocused && blockData?.meta?.["frame:bordercolor:focused"]) {
style.borderColor = blockData.meta["frame:bordercolor:focused"];
}
return (
<div
className={clsx(
"block",
"block-frame-tech",
isFocused ? "block-focused" : null,
preview ? "block-preview" : null
)}
onClick={onClick}
ref={blockRef}
style={style}
>
if (preview) {
isFocused = true;
}
let style: React.CSSProperties = {};
if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
style.borderColor = blockData.meta["frame:bordercolor"];
}
if (isFocused && blockData?.meta?.["frame:bordercolor:focused"]) {
style.borderColor = blockData.meta["frame:bordercolor:focused"];
}
return (
<div
className="block-frame-tech-header"
ref={dragHandleRef}
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, onClose)}
className={clsx(
"block",
"block-frame-tech",
isFocused ? "block-focused" : null,
preview ? "block-preview" : null
)}
onClick={onClick}
ref={blockRef}
style={style}
>
{getBlockHeaderText(blockIcon, blockData, settingsConfig)}
<div
className="block-frame-tech-header"
ref={dragHandleRef}
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, onClose)}
>
{getBlockHeaderText(blockIcon, blockData, settingsConfig)}
</div>
<div className={clsx("block-frame-tech-close")} onClick={onClose}>
<i className="fa fa-solid fa-xmark fa-fw" />
</div>
{preview ? <div className="block-frame-preview" /> : children}
</div>
<div className={clsx("block-frame-tech-close")} onClick={onClose}>
<i className="fa fa-solid fa-xmark fa-fw" />
</div>
{preview ? <div className="block-frame-preview" /> : children}
</div>
);
};
);
}
);
const BlockFrame_Frameless = ({
blockId,
@ -340,7 +334,7 @@ const BlockFrame_Frameless = ({
);
};
const BlockFrame = (props: BlockFrameProps) => {
const BlockFrame = React.memo((props: BlockFrameProps) => {
const blockId = props.blockId;
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const tabData = jotai.useAtomValue(atoms.tabAtom);
@ -360,7 +354,7 @@ const BlockFrame = (props: BlockFrameProps) => {
FrameElem = BlockFrame_Frameless;
}
return <FrameElem {...props} />;
};
});
function blockViewToIcon(view: string): string {
console.log("blockViewToIcon", view);
@ -398,7 +392,16 @@ function useBlockIcon(blockId: string): string {
return blockIcon;
}
const Block = ({ blockId, onClose, dragHandleRef }: BlockProps) => {
const wm = new WeakMap();
let wmCounter = 0;
function getObjectId(obj: any): number {
if (!wm.has(obj)) {
wm.set(obj, wmCounter++);
}
return wm.get(obj);
}
const Block = React.memo(({ blockId, onClose, dragHandleRef }: BlockProps) => {
let blockElem: JSX.Element = null;
const focusElemRef = React.useRef<HTMLInputElement>(null);
const blockRef = React.useRef<HTMLDivElement>(null);
@ -417,24 +420,29 @@ const Block = ({ blockId, onClose, dragHandleRef }: BlockProps) => {
setBlockFocus(blockId);
}, [blockClicked]);
const setBlockClickedTrue = React.useCallback(() => {
setBlockClicked(true);
}, []);
if (!blockId || !blockData) return null;
if (blockDataLoading) {
blockElem = <CenteredDiv>Loading...</CenteredDiv>;
} else if (blockData.view === "term") {
blockElem = <TerminalView blockId={blockId} />;
blockElem = <TerminalView key={blockId} blockId={blockId} />;
} else if (blockData.view === "preview") {
blockElem = <PreviewView blockId={blockId} />;
blockElem = <PreviewView key={blockId} blockId={blockId} />;
} else if (blockData.view === "plot") {
blockElem = <PlotView />;
blockElem = <PlotView key={blockId} />;
} else if (blockData.view === "codeedit") {
blockElem = <CodeEdit text={null} filename={null} />;
blockElem = <CodeEdit key={blockId} text={null} filename={null} />;
}
return (
<BlockFrame
key={blockId}
blockId={blockId}
onClose={onClose}
preview={false}
onClick={() => setBlockClicked(true)}
onClick={setBlockClickedTrue}
blockRef={blockRef}
dragHandleRef={dragHandleRef}
>
@ -448,6 +456,6 @@ const Block = ({ blockId, onClose, dragHandleRef }: BlockProps) => {
</div>
</BlockFrame>
);
};
});
export { Block, BlockFrame };

View File

@ -6,7 +6,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
className?: string;
}
const Button: React.FC<ButtonProps> = ({ className = "primary", children, disabled, ...props }) => {
const Button: React.FC<ButtonProps> = React.memo(({ className = "primary", children, disabled, ...props }) => {
const hasIcon = React.Children.toArray(children).some(
(child) => React.isValidElement(child) && (child as React.ReactElement).type === "svg"
);
@ -23,6 +23,6 @@ const Button: React.FC<ButtonProps> = ({ className = "primary", children, disabl
{children}
</button>
);
};
});
export { Button };

View File

@ -6,6 +6,7 @@ import { ContextMenuModel } from "@/store/contextmenu";
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
import { clsx } from "clsx";
import * as React from "react";
import { forwardRef, useEffect, useRef, useState } from "react";
import "./tab.less";
@ -22,129 +23,131 @@ interface TabProps {
onLoaded: () => void;
}
const Tab = forwardRef<HTMLDivElement, TabProps>(
({ id, active, isFirst, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => {
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id));
const [originalName, setOriginalName] = useState("");
const [isEditable, setIsEditable] = useState(false);
const Tab = React.memo(
forwardRef<HTMLDivElement, TabProps>(
({ id, active, isFirst, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => {
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id));
const [originalName, setOriginalName] = useState("");
const [isEditable, setIsEditable] = useState(false);
const editableRef = useRef<HTMLDivElement>(null);
const editableTimeoutRef = useRef<NodeJS.Timeout>();
const loadedRef = useRef(false);
const editableRef = useRef<HTMLDivElement>(null);
const editableTimeoutRef = useRef<NodeJS.Timeout>();
const loadedRef = useRef(false);
useEffect(() => {
if (tabData?.name) {
setOriginalName(tabData.name);
}
}, [tabData]);
useEffect(() => {
if (tabData?.name) {
setOriginalName(tabData.name);
}
}, [tabData]);
useEffect(() => {
return () => {
if (editableTimeoutRef.current) {
clearTimeout(editableTimeoutRef.current);
useEffect(() => {
return () => {
if (editableTimeoutRef.current) {
clearTimeout(editableTimeoutRef.current);
}
};
}, []);
const handleDoubleClick = (event) => {
event.stopPropagation();
setIsEditable(true);
editableTimeoutRef.current = setTimeout(() => {
if (editableRef.current) {
editableRef.current.focus();
document.execCommand("selectAll", false);
}
}, 0);
};
const handleBlur = () => {
let newText = editableRef.current.innerText.trim();
newText = newText || originalName;
editableRef.current.innerText = newText;
setIsEditable(false);
services.ObjectService.UpdateTabName(id, newText);
};
const handleKeyDown = (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === "a") {
event.preventDefault();
if (editableRef.current) {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(editableRef.current);
selection.removeAllRanges();
selection.addRange(range);
}
return;
}
if (event.key === "Enter") {
event.preventDefault();
if (editableRef.current.innerText.trim() === "") {
editableRef.current.innerText = originalName;
}
editableRef.current.blur();
} else if (event.key === "Escape") {
editableRef.current.innerText = originalName;
editableRef.current.blur();
} else if (
editableRef.current.innerText.length >= 8 &&
!["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(event.key)
) {
event.preventDefault();
}
};
}, []);
const handleDoubleClick = (event) => {
event.stopPropagation();
setIsEditable(true);
editableTimeoutRef.current = setTimeout(() => {
if (editableRef.current) {
editableRef.current.focus();
document.execCommand("selectAll", false);
useEffect(() => {
if (!loadedRef.current) {
onLoaded();
loadedRef.current = true;
}
}, 0);
};
}, [onLoaded]);
const handleBlur = () => {
let newText = editableRef.current.innerText.trim();
newText = newText || originalName;
editableRef.current.innerText = newText;
setIsEditable(false);
services.ObjectService.UpdateTabName(id, newText);
};
// Prevent drag from being triggered on mousedown
const handleMouseDownOnClose = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.stopPropagation();
};
const handleKeyDown = (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === "a") {
event.preventDefault();
if (editableRef.current) {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(editableRef.current);
selection.removeAllRanges();
selection.addRange(range);
}
return;
function handleContextMenu(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
e.preventDefault();
let menu: ContextMenuItem[] = [];
menu.push({ label: "Copy TabId", click: () => navigator.clipboard.writeText(id) });
menu.push({ type: "separator" });
menu.push({ label: "Close Tab", click: () => onClose(null) });
ContextMenuModel.showContextMenu(menu, e);
}
if (event.key === "Enter") {
event.preventDefault();
if (editableRef.current.innerText.trim() === "") {
editableRef.current.innerText = originalName;
}
editableRef.current.blur();
} else if (event.key === "Escape") {
editableRef.current.innerText = originalName;
editableRef.current.blur();
} else if (
editableRef.current.innerText.length >= 8 &&
!["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(event.key)
) {
event.preventDefault();
}
};
useEffect(() => {
if (!loadedRef.current) {
onLoaded();
loadedRef.current = true;
}
}, [onLoaded]);
// Prevent drag from being triggered on mousedown
const handleMouseDownOnClose = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.stopPropagation();
};
function handleContextMenu(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
e.preventDefault();
let menu: ContextMenuItem[] = [];
menu.push({ label: "Copy TabId", click: () => navigator.clipboard.writeText(id) });
menu.push({ type: "separator" });
menu.push({ label: "Close Tab", click: () => onClose(null) });
ContextMenuModel.showContextMenu(menu, e);
}
return (
<div
ref={ref}
className={clsx("tab", { active, isDragging, "before-active": isBeforeActive })}
onMouseDown={onDragStart}
onClick={onSelect}
onContextMenu={handleContextMenu}
data-tab-id={id}
>
{isFirst && <div className="vertical-line first" />}
return (
<div
ref={editableRef}
className={clsx("name", { focused: isEditable })}
contentEditable={isEditable}
onDoubleClick={handleDoubleClick}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
suppressContentEditableWarning={true}
ref={ref}
className={clsx("tab", { active, isDragging, "before-active": isBeforeActive })}
onMouseDown={onDragStart}
onClick={onSelect}
onContextMenu={handleContextMenu}
data-tab-id={id}
>
{tabData?.name}
{isFirst && <div className="vertical-line first" />}
<div
ref={editableRef}
className={clsx("name", { focused: isEditable })}
contentEditable={isEditable}
onDoubleClick={handleDoubleClick}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
suppressContentEditableWarning={true}
>
{tabData?.name}
</div>
{!isDragging && <div className="vertical-line" />}
{active && <div className="mask" />}
<Button className="secondary ghost close" onClick={onClose} onMouseDown={handleMouseDownOnClose}>
<i className="fa fa-solid fa-xmark" />
</Button>
</div>
{!isDragging && <div className="vertical-line" />}
{active && <div className="mask" />}
<Button className="secondary ghost close" onClick={onClose} onMouseDown={handleMouseDownOnClose}>
<i className="fa fa-solid fa-xmark" />
</Button>
</div>
);
}
);
}
)
);
export { Tab };

View File

@ -37,7 +37,7 @@ interface TabBarProps {
workspace: Workspace;
}
const TabBar = ({ workspace }: TabBarProps) => {
const TabBar = React.memo(({ workspace }: TabBarProps) => {
const [tabIds, setTabIds] = useState<string[]>([]);
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
const [draggingTab, setDraggingTab] = useState<string>();
@ -497,6 +497,6 @@ const TabBar = ({ workspace }: TabBarProps) => {
<WindowDrag ref={draggerRightRef} className="right" />
</div>
);
};
});
export { TabBar };

View File

@ -4,16 +4,17 @@
import { Block, BlockFrame } from "@/app/block/block";
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
import * as React from "react";
import { CenteredDiv, CenteredLoadingDiv } from "@/element/quickelems";
import { TileLayout } from "@/faraday/index";
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
import { useAtomValue } from "jotai";
import { useCallback, useMemo } from "react";
import { useMemo } from "react";
import { getApi } from "../store/global";
import "./tabcontent.less";
const TabContent = ({ tabId }: { tabId: string }) => {
const TabContent = React.memo(({ tabId }: { tabId: string }) => {
const oref = useMemo(() => WOS.makeORef("tab", tabId), [tabId]);
const loadingAtom = useMemo(() => WOS.getWaveObjectLoadingAtom(oref), [oref]);
const tabLoading = useAtomValue(loadingAtom);
@ -21,31 +22,40 @@ const TabContent = ({ tabId }: { tabId: string }) => {
const layoutStateAtom = useMemo(() => getLayoutStateAtomForTab(tabId, tabAtom), [tabAtom, tabId]);
const tabData = useAtomValue(tabAtom);
const renderBlock = useCallback(
(
const tileLayoutContents = useMemo(() => {
function renderBlock(
tabData: TabLayoutData,
ready: boolean,
onClose: () => void,
dragHandleRef: React.RefObject<HTMLDivElement>
) => {
) {
if (!tabData.blockId || !ready) {
return null;
}
return <Block blockId={tabData.blockId} onClose={onClose} dragHandleRef={dragHandleRef} />;
},
[]
);
return (
<Block
key={tabData.blockId}
blockId={tabData.blockId}
onClose={onClose}
dragHandleRef={dragHandleRef}
/>
);
}
const renderPreview = useCallback((tabData: TabLayoutData) => {
return <BlockFrame blockId={tabData.blockId} preview={true} />;
}, []);
function renderPreview(tabData: TabLayoutData) {
return <BlockFrame key={tabData.blockId} blockId={tabData.blockId} preview={true} />;
}
const onNodeDelete = useCallback((data: TabLayoutData) => {
return services.ObjectService.DeleteBlock(data.blockId);
}, []);
function onNodeDelete(data: TabLayoutData) {
return services.ObjectService.DeleteBlock(data.blockId);
}
const getCursorPoint = useCallback(() => {
return getApi().getCursorPoint();
return {
renderContent: renderBlock,
renderPreview: renderPreview,
tabId: tabId,
onNodeDelete: onNodeDelete,
};
}, []);
if (tabLoading) {
@ -68,15 +78,12 @@ const TabContent = ({ tabId }: { tabId: string }) => {
<div className="tabcontent">
<TileLayout
key={tabId}
tabId={tabId}
renderContent={renderBlock}
renderPreview={renderPreview}
contents={tileLayoutContents}
layoutTreeStateAtom={layoutStateAtom}
onNodeDelete={onNodeDelete}
getCursorPoint={getCursorPoint}
getCursorPoint={getApi().getCursorPoint}
/>
</div>
);
};
});
export { TabContent };

View File

@ -67,6 +67,13 @@
&.col-size {
text-align: right;
}
.dir-table-lastmod,
.dir-table-modestr,
.dir-table-size,
.dir-table-type {
color: var(--secondary-text-color);
}
}
}
}

View File

@ -158,37 +158,40 @@ function DirectoryTable({ data, cwd, setFileName }: DirectoryTableProps) {
() => [
columnHelper.accessor("mimetype", {
cell: (info) => <i className={getIconFromMimeType(info.getValue() ?? "")}></i>,
header: () => <span></span>,
header: () => <span>Type</span>,
id: "logo",
size: 25,
enableSorting: false,
}),
columnHelper.accessor("path", {
cell: (info) => info.getValue(),
cell: (info) => <span className="dir-table-path">{info.getValue()}</span>,
header: () => <span>Name</span>,
sortingFn: "alphanumeric",
}),
columnHelper.accessor("modestr", {
cell: (info) => info.getValue(),
cell: (info) => <span className="dir-table-modestr">{info.getValue()}</span>,
header: () => <span>Permissions</span>,
size: 91,
sortingFn: "alphanumeric",
}),
columnHelper.accessor("modtime", {
cell: (info) =>
getLastModifiedTime(info.getValue(), settings.datetime.locale, settings.datetime.format),
cell: (info) => (
<span className="dir-table-lastmod">
{getLastModifiedTime(info.getValue(), settings.datetime.locale, settings.datetime.format)}
</span>
),
header: () => <span>Last Modified</span>,
size: 185,
sortingFn: "datetime",
}),
columnHelper.accessor("size", {
cell: (info) => getBestUnit(info.getValue()),
cell: (info) => <span className="dir-table-size">{getBestUnit(info.getValue())}</span>,
header: () => <span>Size</span>,
size: 55,
sortingFn: "auto",
}),
columnHelper.accessor("mimetype", {
cell: (info) => info.getValue(),
cell: (info) => <span className="dir-table-type">{info.getValue()}</span>,
header: () => <span>Type</span>,
sortingFn: "alphanumeric",
}),

View File

@ -14,7 +14,7 @@ import "./workspace.less";
const iconRegex = /^[a-z0-9-]+$/;
function Widgets() {
const Widgets = React.memo(() => {
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
const newWidgetModalVisible = React.useState(false);
async function clickTerminal() {
@ -92,27 +92,27 @@ function Widgets() {
</div>
</div>
);
}
});
function WorkspaceElem() {
const WorkspaceElem = React.memo(() => {
const windowData = jotai.useAtomValue(atoms.waveWindow);
const activeTabId = windowData?.activetabid;
const ws = jotai.useAtomValue(atoms.workspace);
return (
<div className="workspace">
<TabBar workspace={ws} />
<TabBar key={ws.oid} workspace={ws} />
<div className="workspace-tabcontent">
{activeTabId == "" ? (
<CenteredDiv>No Active Tab</CenteredDiv>
) : (
<>
<TabContent key={windowData.workspaceid} tabId={activeTabId} />
<TabContent key={activeTabId} tabId={activeTabId} />
<Widgets />
</>
)}
</div>
</div>
);
}
});
export { WorkspaceElem as Workspace };

View File

@ -37,11 +37,11 @@ import {
import "./tilelayout.less";
import { Dimensions, FlexDirection, setTransform as createTransform, determineDropDirection } from "./utils";
export interface TileLayoutProps<T> {
/**
* The atom containing the layout tree state.
*/
layoutTreeStateAtom: WritableLayoutTreeStateAtom<T>;
/**
* contains callbacks and information about the contents (or styling) of of the TileLayout
* nothing in here is specific to the TileLayout itself
*/
export interface TileLayoutContents<T> {
/**
* A callback that accepts the data from the leaf node and displays the leaf contents to the user.
*/
@ -64,6 +64,18 @@ export interface TileLayoutProps<T> {
* tabId this TileLayout is associated with
*/
tabId: string;
}
export interface TileLayoutProps<T> {
/**
* The atom containing the layout tree state.
*/
layoutTreeStateAtom: WritableLayoutTreeStateAtom<T>;
/**
* callbacks and information about the contents (or styling) of the TileLayout or contents
*/
contents: TileLayoutContents<T>;
/**
* A callback for getting the cursor point in reference to the current window. This removes Electron as a runtime dependency, allowing for better integration with Storybook.
@ -75,15 +87,7 @@ export interface TileLayoutProps<T> {
const DragPreviewWidth = 300;
const DragPreviewHeight = 300;
export const TileLayout = <T,>({
layoutTreeStateAtom,
tabId,
className,
renderContent,
renderPreview,
onNodeDelete,
getCursorPoint,
}: TileLayoutProps<T>) => {
export const TileLayout = React.memo(<T,>({ layoutTreeStateAtom, contents, getCursorPoint }: TileLayoutProps<T>) => {
const overlayContainerRef = useRef<HTMLDivElement>(null);
const displayContainerRef = useRef<HTMLDivElement>(null);
@ -126,7 +130,7 @@ export const TileLayout = <T,>({
const [layoutLeafTransforms, setLayoutLeafTransformsRaw] = useState<Record<string, CSSProperties>>({});
const setLayoutLeafTransforms = (transforms: Record<string, CSSProperties>) => {
globalLayoutTransformsMap.set(tabId, transforms);
globalLayoutTransformsMap.set(contents.tabId, transforms);
setLayoutLeafTransformsRaw(transforms);
};
@ -247,30 +251,23 @@ export const TileLayout = <T,>({
// console.log("calling dispatch", deleteAction);
dispatch(deleteAction);
// console.log("calling onNodeDelete", node);
await onNodeDelete?.(node.data);
await contents.onNodeDelete?.(node.data);
// console.log("node deleted");
},
[onNodeDelete, dispatch]
[contents.onNodeDelete, dispatch]
);
return (
<Suspense>
<div className={clsx("tile-layout", className, { animate })} onPointerOut={onPointerLeave}>
<div className={clsx("tile-layout", contents.className, { animate })} onPointerOut={onPointerLeave}>
<div key="display" ref={displayContainerRef} className="display-container">
{layoutLeafTransforms &&
layoutTreeState.leafs.map((leaf) => {
return (
<DisplayNode
key={leaf.id}
layoutNode={leaf}
renderContent={renderContent}
renderPreview={renderPreview}
transform={layoutLeafTransforms[leaf.id]}
onLeafClose={onLeafClose}
ready={animate}
/>
);
})}
<DisplayNodesWrapper
contents={contents}
ready={animate}
onLeafClose={onLeafClose}
layoutTreeState={layoutTreeState}
layoutLeafTransforms={layoutLeafTransforms}
/>
</div>
<Placeholder
key="placeholder"
@ -296,21 +293,63 @@ export const TileLayout = <T,>({
</div>
</Suspense>
);
};
});
interface DisplayNodesWrapperProps<T> {
/**
* The layout tree state.
*/
layoutTreeState: LayoutTreeState<T>;
/**
* contains callbacks and information about the contents (or styling) of of the TileLayout
*/
contents: TileLayoutContents<T>;
/**
* A callback that is called when a leaf node gets closed.
* @param node The node that is closed.
*/
onLeafClose: (node: LayoutNode<T>) => void;
/**
* A series of CSS properties used to display a leaf node with the correct dimensions and position, as determined from its corresponding OverlayNode.
*/
layoutLeafTransforms: Record<string, CSSProperties>;
/**
* Determines whether the leaf nodes are ready to be displayed to the user.
*/
ready: boolean;
}
const DisplayNodesWrapper = React.memo(
<T,>({ layoutTreeState, contents, onLeafClose, layoutLeafTransforms, ready }: DisplayNodesWrapperProps<T>) => {
if (!layoutLeafTransforms) {
return null;
}
return layoutTreeState.leafs.map((leaf) => {
return (
<DisplayNode
key={leaf.id}
layoutNode={leaf}
contents={contents}
transform={layoutLeafTransforms[leaf.id]}
onLeafClose={onLeafClose}
ready={ready}
/>
);
});
}
);
interface DisplayNodeProps<T> {
/**
* The leaf node object, containing the data needed to display the leaf contents to the user.
*/
layoutNode: LayoutNode<T>;
/**
* A callback that accepts the data from the leaf node and displays the leaf contents to the user.
* contains callbacks and information about the contents (or styling) of of the TileLayout
*/
renderContent: ContentRenderer<T>;
/**
* A callback that accepts the data from the leaf node and returns a preview that can be shown when the user drags a node.
*/
renderPreview?: PreviewRenderer<T>;
contents: TileLayoutContents<T>;
/**
* A callback that is called when a leaf node gets closed.
* @param node The node that is closed.
@ -331,14 +370,7 @@ const dragItemType = "TILE_ITEM";
/**
* The draggable and displayable portion of a leaf node in a layout tree.
*/
const DisplayNode = <T,>({
layoutNode,
renderContent,
renderPreview,
transform,
onLeafClose,
ready,
}: DisplayNodeProps<T>) => {
const DisplayNode = React.memo(<T,>({ layoutNode, contents, transform, onLeafClose, ready }: DisplayNodeProps<T>) => {
const tileNodeRef = useRef<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null);
const previewRef = useRef<HTMLDivElement>(null);
@ -370,11 +402,11 @@ const DisplayNode = <T,>({
transform: `scale(${1 / devicePixelRatio})`,
}}
>
{renderPreview?.(layoutNode.data)}
{contents.renderPreview?.(layoutNode.data)}
</div>
</div>
);
}, [renderPreview, devicePixelRatio]);
}, [contents.renderPreview, devicePixelRatio]);
const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null);
const [previewImageGeneration, setPreviewImageGeneration] = useState(0);
@ -414,7 +446,7 @@ const DisplayNode = <T,>({
return (
layoutNode.data && (
<div key="leaf" className="tile-leaf">
{renderContent(layoutNode.data, ready, onClose, dragHandleRef)}
{contents.renderContent(layoutNode.data, ready, onClose, dragHandleRef)}
</div>
)
);
@ -436,7 +468,7 @@ const DisplayNode = <T,>({
{previewElement}
</div>
);
};
});
interface OverlayNodeProps<T> {
/**