mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 23:19:40 +01:00
web: split pane now supports adding/removing panes in place
This commit is contained in:
committed by
Abdullah Atta
parent
1d80ca7d3c
commit
2f6af50fe2
@@ -143,13 +143,19 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
|
||||
ref={navPane}
|
||||
autoSaveId="global-panel-group"
|
||||
direction="vertical"
|
||||
initialSizes={[180, 380]}
|
||||
onChange={(sizes) => {
|
||||
setIsNarrow(sizes[0] <= 70);
|
||||
}}
|
||||
>
|
||||
{!isFocusMode ? (
|
||||
<Pane minSize={50} snapSize={120} maxSize={300}>
|
||||
{isFocusMode ? null : (
|
||||
<Pane
|
||||
id="nav-pane"
|
||||
initialSize={180}
|
||||
className={`nav-pane`}
|
||||
minSize={50}
|
||||
snapSize={120}
|
||||
maxSize={300}
|
||||
>
|
||||
<NavigationMenu
|
||||
toggleNavigationContainer={(state) => {
|
||||
setShow(state || !show);
|
||||
@@ -157,12 +163,15 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
|
||||
isTablet={isNarrow}
|
||||
/>
|
||||
</Pane>
|
||||
) : null}
|
||||
{!isFocusMode && show && (
|
||||
)}
|
||||
{!isFocusMode && show ? (
|
||||
<Pane
|
||||
id="list-pane"
|
||||
initialSize={380}
|
||||
style={{ flex: 1, display: "flex" }}
|
||||
snapSize={200}
|
||||
maxSize={500}
|
||||
className="list-pane"
|
||||
>
|
||||
<ScopedThemeProvider
|
||||
className="listMenu"
|
||||
@@ -178,9 +187,11 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
|
||||
<CachedRouter />
|
||||
</ScopedThemeProvider>
|
||||
</Pane>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<Pane
|
||||
id="editor-pane"
|
||||
className="editor-pane"
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
|
||||
@@ -146,11 +146,7 @@ export default function TabsView() {
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<SplitPane
|
||||
direction="vertical"
|
||||
initialSizes={documentPreview ? [Infinity, 435] : [Infinity]}
|
||||
autoSaveId={"editor-panels"}
|
||||
>
|
||||
<SplitPane direction="vertical" autoSaveId={"editor-panels"}>
|
||||
<Pane id="editor-panel" className="editor-pane">
|
||||
{sessions.map((session) => (
|
||||
<Freeze key={session.id} freeze={session.id !== activeSessionId}>
|
||||
@@ -166,7 +162,7 @@ export default function TabsView() {
|
||||
</Pane>
|
||||
|
||||
{documentPreview ? (
|
||||
<Pane id="pdf-preview-panel" minSize={435}>
|
||||
<Pane id="pdf-preview-panel" initialSize={435} minSize={435}>
|
||||
<ScopedThemeProvider
|
||||
scope="editorSidebar"
|
||||
id="editorSidebar"
|
||||
@@ -203,7 +199,7 @@ export default function TabsView() {
|
||||
) : null}
|
||||
|
||||
{isTOCVisible && activeSessionId ? (
|
||||
<Pane minSize={300}>
|
||||
<Pane id="table-of-contents-pane" initialSize={300} minSize={300}>
|
||||
<TableOfContents sessionId={activeSessionId} />
|
||||
</Pane>
|
||||
) : null}
|
||||
|
||||
@@ -46,6 +46,16 @@ import { IAxis, ISplitProps, IPaneConfigs } from "./types";
|
||||
import Config from "../../utils/config";
|
||||
export { Pane };
|
||||
|
||||
type PaneOptions = {
|
||||
min: number;
|
||||
max: number;
|
||||
snap: number;
|
||||
size: number;
|
||||
nextSize?: number;
|
||||
initialSize: number;
|
||||
collapsed: boolean;
|
||||
};
|
||||
|
||||
export type SplitPaneImperativeHandle = {
|
||||
collapse: (index: number) => void;
|
||||
expand: (index: number) => void;
|
||||
@@ -56,10 +66,10 @@ export const SplitPane = React.forwardRef<
|
||||
>(function SplitPane(
|
||||
{
|
||||
children,
|
||||
initialSizes,
|
||||
allowResize = true,
|
||||
direction = "vertical",
|
||||
className: wrapClassName,
|
||||
sashStyle,
|
||||
sashRender = (_, active) => (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -79,43 +89,72 @@ export const SplitPane = React.forwardRef<
|
||||
) {
|
||||
const axis = useRef<IAxis>({ x: 0, y: 0 });
|
||||
const wrapper = useRef<HTMLDivElement>(null);
|
||||
const sizes = useRef<number[]>([]);
|
||||
const collapsed = useRef<boolean[]>(
|
||||
Config.get(`csp:${autoSaveId}:collapsed`, [])
|
||||
);
|
||||
const sashPosSizes = useRef<number[]>([]);
|
||||
const panes = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const sashes = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const paneLimitSizes = useRef<{ min: number; max: number; snap: number }[]>(
|
||||
[]
|
||||
);
|
||||
const paneSizes = useRef<PaneOptions[]>([]);
|
||||
const wrapSize = useRef(0);
|
||||
const childrenLength = childrenToArray(children).length;
|
||||
const autoSaveKey = autoSaveId ? `csp:${autoSaveId}` : undefined;
|
||||
|
||||
const { sizeName, splitPos, splitAxis } = useMemo(
|
||||
() =>
|
||||
({
|
||||
sizeName: direction === "vertical" ? "width" : "height",
|
||||
splitPos: direction === "vertical" ? "left" : "top",
|
||||
splitAxis: direction === "vertical" ? "x" : "y"
|
||||
} as const),
|
||||
({
|
||||
sizeName: direction === "vertical" ? "width" : "height",
|
||||
splitPos: direction === "vertical" ? "left" : "top",
|
||||
splitAxis: direction === "vertical" ? "x" : "y"
|
||||
} as const),
|
||||
[direction]
|
||||
);
|
||||
|
||||
const updatePaneLimitSizes = useCallback((children: React.ReactNode) => {
|
||||
paneLimitSizes.current =
|
||||
childrenToArray(children).map((childNode) => {
|
||||
const limits = { min: 0, max: Infinity, snap: 0 };
|
||||
if (React.isValidElement(childNode) && childNode.type === Pane) {
|
||||
const { minSize, maxSize, snapSize } =
|
||||
childNode.props as IPaneConfigs;
|
||||
limits.min = assertsSize(minSize, wrapSize.current, 0);
|
||||
limits.max = assertsSize(maxSize, wrapSize.current);
|
||||
limits.snap = assertsSize(snapSize, wrapSize.current, 0);
|
||||
}
|
||||
return limits;
|
||||
}) || [];
|
||||
}, []);
|
||||
const updatePaneLimitSizes = useCallback(
|
||||
(children: React.ReactNode) => {
|
||||
paneSizes.current =
|
||||
childrenToArray(children).map((childNode) => {
|
||||
const limits: PaneOptions = {
|
||||
min: 0,
|
||||
max: Infinity,
|
||||
snap: 0,
|
||||
size: Infinity,
|
||||
initialSize: Infinity,
|
||||
collapsed: false
|
||||
};
|
||||
if (React.isValidElement(childNode) && childNode.type === Pane) {
|
||||
const { minSize, maxSize, snapSize, initialSize, id, collapsed } =
|
||||
childNode.props as IPaneConfigs;
|
||||
limits.min = assertsSize(minSize, wrapSize.current, 0);
|
||||
limits.max = assertsSize(maxSize, wrapSize.current);
|
||||
limits.snap = assertsSize(snapSize, wrapSize.current, 0);
|
||||
limits.initialSize = assertsSize(initialSize, wrapSize.current);
|
||||
|
||||
Object.defineProperty(limits, "collapsed", {
|
||||
get() {
|
||||
return Config.get(`${autoSaveKey}-${id}:collapsed`, collapsed);
|
||||
},
|
||||
set(v) {
|
||||
if (v == null) Config.remove(`${autoSaveKey}-${id}:collapsed`);
|
||||
else Config.set(`${autoSaveKey}-${id}:collapsed`, v);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(limits, "size", {
|
||||
get() {
|
||||
return Config.get(
|
||||
`${autoSaveKey}-${id}`,
|
||||
assertsSize(initialSize, wrapSize.current)
|
||||
);
|
||||
},
|
||||
set(v) {
|
||||
if (v === null || v === undefined || v === Infinity)
|
||||
Config.remove(`${autoSaveKey}-${id}`);
|
||||
else Config.set(`${autoSaveKey}-${id}`, v);
|
||||
}
|
||||
});
|
||||
}
|
||||
return limits;
|
||||
}) || [];
|
||||
},
|
||||
[autoSaveKey]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (wrapSize.current === 0) {
|
||||
@@ -124,32 +163,17 @@ export const SplitPane = React.forwardRef<
|
||||
}
|
||||
|
||||
if (wrapSize.current === 0) return;
|
||||
|
||||
updatePaneLimitSizes(children);
|
||||
setSizes(
|
||||
sizes.current.length === childrenLength
|
||||
? sizes.current
|
||||
: Config.get(`csp:${autoSaveId}`, initialSizes),
|
||||
wrapSize.current,
|
||||
false
|
||||
);
|
||||
}, [initialSizes, children, childrenLength]);
|
||||
setSizes(paneSizes.current, wrapSize.current, false);
|
||||
}, [children, childrenLength]);
|
||||
|
||||
const setSizes = useCallback(
|
||||
function setSizes(
|
||||
paneSizes: (number | string)[],
|
||||
paneLimits: PaneOptions[],
|
||||
wrapSize: number,
|
||||
notify = true
|
||||
) {
|
||||
const normalized = normalizeSizes(
|
||||
children,
|
||||
paneSizes.map((size, i) =>
|
||||
collapsed.current[i] ? paneLimitSizes.current[i].min : size
|
||||
),
|
||||
initialSizes,
|
||||
wrapSize
|
||||
);
|
||||
|
||||
const normalized = normalizeSizes(children, paneLimits, wrapSize);
|
||||
sashPosSizes.current = normalized.reduce(
|
||||
(a, b) => [...a, a[a.length - 1] + b],
|
||||
[0]
|
||||
@@ -157,11 +181,15 @@ export const SplitPane = React.forwardRef<
|
||||
|
||||
for (let i = 0; i < panes.current.length; ++i) {
|
||||
const pane = panes.current[i];
|
||||
if (!pane) continue;
|
||||
const size = normalized[i];
|
||||
const sashPos = sashPosSizes.current[i];
|
||||
if (!pane) continue;
|
||||
const limits = paneSizes.current[i];
|
||||
pane.style[sizeName] = `${size}px`;
|
||||
pane.style[splitPos] = `${sashPos}px`;
|
||||
if (limits.collapsed || size === limits.min)
|
||||
pane.classList.add("collapsed");
|
||||
else pane.classList.remove("collapsed");
|
||||
}
|
||||
|
||||
for (let i = 0; i < sashes.current.length; ++i) {
|
||||
@@ -172,35 +200,20 @@ export const SplitPane = React.forwardRef<
|
||||
}
|
||||
}
|
||||
|
||||
sizes.current = normalizeSizes(
|
||||
children,
|
||||
paneSizes,
|
||||
initialSizes,
|
||||
wrapSize
|
||||
);
|
||||
paneSizes.current.forEach((limits, index) => {
|
||||
limits.size = normalized[index];
|
||||
});
|
||||
|
||||
if (autoSaveId) {
|
||||
Config.set(`csp:${autoSaveId}`, sizes.current);
|
||||
Config.set(`csp:${autoSaveId}:collapsed`, collapsed.current);
|
||||
}
|
||||
if (notify) onChange(normalized);
|
||||
},
|
||||
[
|
||||
children,
|
||||
initialSizes,
|
||||
onChange,
|
||||
autoSaveId,
|
||||
sizeName,
|
||||
splitPos,
|
||||
resizerSize
|
||||
]
|
||||
[children, onChange, sizeName, splitPos, resizerSize]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wrapper.current) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
if (!sizes.current.length) return;
|
||||
if (!paneSizes.current.length) return;
|
||||
|
||||
const [entry] = entries;
|
||||
const newSize = entry.contentRect ? entry.contentRect[sizeName] : 0;
|
||||
@@ -229,7 +242,7 @@ export const SplitPane = React.forwardRef<
|
||||
// }
|
||||
|
||||
wrapSize.current = newSize;
|
||||
setSizes(sizes.current, wrapSize.current);
|
||||
setSizes(paneSizes.current, wrapSize.current);
|
||||
});
|
||||
resizeObserver.observe(wrapper.current);
|
||||
return () => {
|
||||
@@ -242,12 +255,12 @@ export const SplitPane = React.forwardRef<
|
||||
() => {
|
||||
return {
|
||||
collapse: (index: number) => {
|
||||
collapsed.current[index] = true;
|
||||
setSizes(sizes.current, wrapSize.current);
|
||||
paneSizes.current[index].collapsed = true;
|
||||
setSizes(paneSizes.current, wrapSize.current);
|
||||
},
|
||||
expand: (index: number) => {
|
||||
collapsed.current[index] = false;
|
||||
setSizes(sizes.current, wrapSize.current);
|
||||
paneSizes.current[index].collapsed = false;
|
||||
setSizes(paneSizes.current, wrapSize.current);
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -282,20 +295,17 @@ export const SplitPane = React.forwardRef<
|
||||
let distanceX = curAxis[splitAxis] - axis.current[splitAxis];
|
||||
axis.current = { x: e.pageX, y: e.pageY };
|
||||
|
||||
const currentSize = sizes.current[i];
|
||||
const currentPaneLimits = paneLimitSizes.current[i];
|
||||
const nextPaneLimits = paneLimitSizes.current[i + 1];
|
||||
const currentPane = paneSizes.current[i];
|
||||
const nextPane = paneSizes.current[i + 1];
|
||||
const rightBorder = sashPosSizes.current[i + 2];
|
||||
|
||||
if (currentSize + distanceX >= rightBorder)
|
||||
distanceX = rightBorder - currentSize;
|
||||
|
||||
const nextSizes = [...sizes.current];
|
||||
if (currentPane.size + distanceX >= rightBorder)
|
||||
distanceX = rightBorder - currentPane.size;
|
||||
|
||||
// if current pane size is out of limit, adjust the previous pane
|
||||
if (
|
||||
currentSize + distanceX >= currentPaneLimits.max ||
|
||||
currentSize + distanceX <= currentPaneLimits.min
|
||||
currentPane.size + distanceX >= currentPane.max ||
|
||||
currentPane.size + distanceX <= currentPane.min
|
||||
) {
|
||||
if (i > 0) {
|
||||
// reset axis
|
||||
@@ -305,27 +315,32 @@ export const SplitPane = React.forwardRef<
|
||||
return;
|
||||
}
|
||||
|
||||
nextSizes[i] += distanceX;
|
||||
currentPane.nextSize =
|
||||
(currentPane.nextSize || currentPane.size) + distanceX;
|
||||
// keep the next pane size in the min-max range
|
||||
nextSizes[i + 1] = Math.min(
|
||||
nextPaneLimits.max,
|
||||
Math.max(nextPaneLimits.min, nextSizes[i + 1] - distanceX)
|
||||
nextPane.nextSize = Math.min(
|
||||
nextPane.max,
|
||||
Math.max(nextPane.min, (nextPane.nextSize || nextPane.size) - distanceX)
|
||||
);
|
||||
|
||||
// snapping logic
|
||||
if (currentPaneLimits.snap > 0) {
|
||||
if (distanceX < 0 && nextSizes[i] <= currentPaneLimits.snap / 2) {
|
||||
nextSizes[i] = currentPaneLimits.min;
|
||||
} else if (nextSizes[i] < currentPaneLimits.snap) {
|
||||
if (currentPane.snap > 0) {
|
||||
if (distanceX < 0 && currentPane.nextSize <= currentPane.snap / 2) {
|
||||
currentPane.nextSize = currentPane.min;
|
||||
} else if (currentPane.nextSize < currentPane.snap) {
|
||||
// reset axis
|
||||
axis.current[splitAxis] += -distanceX;
|
||||
return;
|
||||
}
|
||||
}
|
||||
nextPane.size = nextPane.nextSize;
|
||||
currentPane.size = currentPane.nextSize;
|
||||
nextPane.nextSize = undefined;
|
||||
currentPane.nextSize = undefined;
|
||||
|
||||
setSizes(nextSizes, wrapSize.current);
|
||||
setSizes(paneSizes.current, wrapSize.current);
|
||||
},
|
||||
[paneLimitSizes, setSizes, splitAxis]
|
||||
[paneSizes, setSizes, splitAxis]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -339,9 +354,7 @@ export const SplitPane = React.forwardRef<
|
||||
ref={wrapper}
|
||||
{...others}
|
||||
>
|
||||
{React.Children.map(children, (childNode, childIndex) => {
|
||||
if (!childNode) return null;
|
||||
|
||||
{childrenToArray(children).map((childNode, childIndex) => {
|
||||
const isPane = React.isValidElement(childNode)
|
||||
? childNode.type === Pane
|
||||
: false;
|
||||
@@ -350,6 +363,7 @@ export const SplitPane = React.forwardRef<
|
||||
|
||||
return (
|
||||
<Pane
|
||||
id={paneProps.id}
|
||||
key={childIndex}
|
||||
paneRef={(e) => (panes.current[childIndex] = e)}
|
||||
className={classNames(paneClassName, paneProps.className)}
|
||||
@@ -370,18 +384,17 @@ export const SplitPane = React.forwardRef<
|
||||
: sashHorizontalClassName
|
||||
)}
|
||||
style={{
|
||||
[sizeName]: resizerSize
|
||||
[sizeName]: resizerSize,
|
||||
...sashStyle
|
||||
}}
|
||||
render={sashRender.bind(null, index)}
|
||||
onDragStart={dragStart}
|
||||
onDragging={(e) => onDragging(e, index)}
|
||||
onDragEnd={dragEnd}
|
||||
onDoubleClick={() => {
|
||||
sizes.current[index] = assertsSize(
|
||||
initialSizes[index],
|
||||
wrapSize.current
|
||||
);
|
||||
setSizes(sizes.current, wrapSize.current);
|
||||
paneSizes.current[index].size =
|
||||
paneSizes.current[index].initialSize;
|
||||
setSizes(paneSizes.current, wrapSize.current);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -390,20 +403,19 @@ export const SplitPane = React.forwardRef<
|
||||
});
|
||||
|
||||
function childrenToArray(children: React.ReactNode) {
|
||||
return React.Children.toArray(children).filter(Boolean);
|
||||
return React.Children.toArray(children);
|
||||
}
|
||||
|
||||
function normalizeSizes(
|
||||
children: React.ReactNode,
|
||||
currentSizes: (string | number)[],
|
||||
initialSizes: (string | number)[],
|
||||
panes: PaneOptions[],
|
||||
wrapSize: number
|
||||
): number[] {
|
||||
let count = 0;
|
||||
let curSum = 0;
|
||||
const res = childrenToArray(children).map((_, index) => {
|
||||
const initialSize = assertsSize(initialSizes[index], wrapSize);
|
||||
const size = assertsSize(currentSizes[index], wrapSize);
|
||||
const initialSize = panes[index].initialSize;
|
||||
const size = panes[index].collapsed ? panes[index].min : panes[index].size;
|
||||
initialSize === Infinity ? count++ : (curSum += size);
|
||||
return size;
|
||||
});
|
||||
@@ -411,7 +423,7 @@ function normalizeSizes(
|
||||
if (count > 0 || curSum > wrapSize) {
|
||||
const average = (wrapSize - curSum) / count;
|
||||
return res.map((size, index) => {
|
||||
const initialSize = assertsSize(initialSizes[index], wrapSize);
|
||||
const initialSize = panes[index].initialSize;
|
||||
return initialSize === Infinity ? average : size;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
.react-split__sash {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
transition: background-color 0.1s;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
|
||||
@@ -38,34 +38,15 @@ export interface ICacheSizes {
|
||||
|
||||
export interface ISplitProps extends HTMLElementProps {
|
||||
autoSaveId?: string;
|
||||
/**
|
||||
* Should allowed to resized
|
||||
*
|
||||
* default is true
|
||||
*/
|
||||
allowResize?: boolean;
|
||||
/**
|
||||
* How to split the space
|
||||
*
|
||||
* default is vertical
|
||||
*/
|
||||
direction: "vertical" | "horizontal";
|
||||
/**
|
||||
* Only support controlled mode, so it's required
|
||||
*/
|
||||
initialSizes: (string | number)[];
|
||||
sashRender?: (index: number, active: boolean) => React.ReactNode;
|
||||
onChange?: (sizes: number[]) => void;
|
||||
onDragStart?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onDragEnd?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
className?: string;
|
||||
sashClassName?: string;
|
||||
// performanceMode?: boolean;
|
||||
/**
|
||||
* Specify the size fo resizer
|
||||
*
|
||||
* defualt size is 4px
|
||||
*/
|
||||
sashStyle?: React.CSSProperties;
|
||||
sashSize?: number;
|
||||
}
|
||||
|
||||
@@ -87,9 +68,11 @@ export interface ISashContentProps {
|
||||
}
|
||||
|
||||
export interface IPaneConfigs {
|
||||
id?: string;
|
||||
id: string;
|
||||
paneRef?: React.LegacyRef<HTMLDivElement>;
|
||||
maxSize?: number | string;
|
||||
minSize?: number | string;
|
||||
snapSize?: number | string;
|
||||
initialSize?: number | string;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,6 @@ function Notebook(props: NotebookProps) {
|
||||
<SplitPane
|
||||
ref={pane}
|
||||
direction="horizontal"
|
||||
initialSizes={[Infinity, 250]}
|
||||
autoSaveId={`notebook-panel-sizes:${rootId}`}
|
||||
onChange={([_, subnotebooksPane]) => {
|
||||
setIsCollapsed((isCollapsed) => {
|
||||
@@ -105,7 +104,7 @@ function Notebook(props: NotebookProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Pane style={{ display: "flex" }}>
|
||||
<Pane id="notes-pane" style={{ display: "flex" }}>
|
||||
<Notes
|
||||
header={
|
||||
<NotebookHeader
|
||||
@@ -116,7 +115,7 @@ function Notebook(props: NotebookProps) {
|
||||
}
|
||||
/>
|
||||
</Pane>
|
||||
<Pane minSize={30}>
|
||||
<Pane id="subnotebooks-pane" initialSize={250} minSize={30}>
|
||||
<SubNotebooks
|
||||
isCollapsed={isCollapsed}
|
||||
rootId={rootId}
|
||||
|
||||
Reference in New Issue
Block a user