diff --git a/src/allotment.tsx b/src/allotment.tsx index 46fb0bae..9d81177c 100644 --- a/src/allotment.tsx +++ b/src/allotment.tsx @@ -7,13 +7,20 @@ import React, { useLayoutEffect, useMemo, useRef, + useState, } from "react"; import useResizeObserver from "use-resize-observer"; import styles from "./allotment.module.css"; import { isIOS } from "./helpers/platform"; import { Orientation, setGlobalSashSize } from "./sash"; -import { Sizing, SplitView, SplitViewOptions } from "./split-view/split-view"; +import { + LayoutService, + PaneView, + Sizing, + SplitView, + SplitViewOptions, +} from "./split-view/split-view"; function isPane(item: React.ReactNode): item is typeof Pane { return (item as any).type.displayName === "Allotment.Pane"; @@ -39,7 +46,7 @@ export interface CommonProps { export type PaneProps = { children: React.ReactNode; - preferredSize?: number; + preferredSize?: number | string; visible?: boolean; } & CommonProps; @@ -106,6 +113,10 @@ const Allotment = forwardRef( const splitViewPropsRef = useRef(new Map()); const splitViewRef = useRef(null); const splitViewViewRef = useRef(new Map()); + const layoutService = useRef(new LayoutService()); + const views = useRef([]); + + const [dimensionsInitialized, setDimensionsInitialized] = useState(false); if (process.env.NODE_ENV !== "production" && sizes) { console.warn( @@ -159,16 +170,20 @@ const Allotment = forwardRef( previousKeys.current[index] ); + const view = new PaneView(layoutService.current, { + element: document.createElement("div"), + minimumSize: props?.minSize ?? minSize, + maximumSize: props?.maxSize ?? maxSize, + ...(props?.preferredSize && { + preferredSize: props?.preferredSize, + }), + snap: props?.snap ?? snap, + }); + return { container: [...splitViewViewRef.current.values()][index], size: size, - view: { - element: document.createElement("div"), - minimumSize: props?.minSize ?? minSize, - maximumSize: props?.maxSize ?? maxSize, - snap: props?.snap ?? snap, - layout: () => {}, - }, + view: view, }; }), }, @@ -204,18 +219,16 @@ const Allotment = forwardRef( if (onReset) { onReset(); } else { - const keys = childrenArray.map((child) => child.key as string); - const resizeToPreferredSize = (index: number): boolean => { - const props = splitViewPropsRef.current.get(keys[index]); + const view = views.current?.[index]; - if (typeof props?.preferredSize !== "number") { + if (typeof view?.preferredSize !== "number") { return false; } splitViewRef.current?.resizeView( index, - Math.round(props.preferredSize) + Math.round(view.preferredSize) ); return true; @@ -229,8 +242,6 @@ const Allotment = forwardRef( return; } - console.log("distributeViewSizes"); - splitViewRef.current?.distributeViewSizes(); } }); @@ -247,58 +258,85 @@ const Allotment = forwardRef( * Add, remove or update views as children change */ useEffect(() => { - const keys = childrenArray.map((child) => child.key as string); + if (dimensionsInitialized) { + const keys = childrenArray.map((child) => child.key as string); - const enter = keys.filter((key) => !previousKeys.current.includes(key)); - const update = keys.filter((key) => previousKeys.current.includes(key)); - const exit = previousKeys.current.map((key) => !keys.includes(key)); + const enter = keys.filter((key) => !previousKeys.current.includes(key)); + const update = keys.filter((key) => previousKeys.current.includes(key)); + const exit = previousKeys.current.map((key) => !keys.includes(key)); - exit.forEach((flag, index) => { - if (flag) { - splitViewRef.current?.removeView(index); - } - }); + exit.forEach((flag, index) => { + if (flag) { + splitViewRef.current?.removeView(index); + views.current.splice(index, 1); + } + }); - for (const enterKey of enter) { - const props = splitViewPropsRef.current.get(enterKey); + for (const enterKey of enter) { + const props = splitViewPropsRef.current.get(enterKey); - splitViewRef.current?.addView( - splitViewViewRef.current.get(enterKey)!, - { + const view = new PaneView(layoutService.current, { element: document.createElement("div"), minimumSize: props?.minSize ?? minSize, maximumSize: props?.maxSize ?? maxSize, + ...(props?.preferredSize && { + preferredSize: props?.preferredSize, + }), snap: props?.snap ?? snap, - layout: () => {}, - }, - Sizing.Distribute, - keys.findIndex((key) => key === enterKey) - ); - } + }); + + splitViewRef.current?.addView( + splitViewViewRef.current.get(enterKey)!, + view, + Sizing.Distribute, + keys.findIndex((key) => key === enterKey) + ); + + views.current.splice( + keys.findIndex((key) => key === enterKey), + 0, + view + ); + } + + for (const enterKey of enter) { + const index = keys.findIndex((key) => key === enterKey); + + const preferredSize = views.current[index].preferredSize; + + if (preferredSize !== undefined) { + splitViewRef.current?.resizeView(index, preferredSize); + } + } - for (const updateKey of [...enter, ...update]) { - const props = splitViewPropsRef.current.get(updateKey); - const index = keys.findIndex((key) => key === updateKey); + for (const updateKey of [...enter, ...update]) { + const props = splitViewPropsRef.current.get(updateKey); + const index = keys.findIndex((key) => key === updateKey); - if (props && isPaneProps(props)) { - if (props.visible !== undefined) { - if (splitViewRef.current?.isViewVisible(index) !== props.visible) { - splitViewRef.current?.setViewVisible(index, props.visible); + if (props && isPaneProps(props)) { + if (props.visible !== undefined) { + if ( + splitViewRef.current?.isViewVisible(index) !== props.visible + ) { + splitViewRef.current?.setViewVisible(index, props.visible); + } } } } - } - if (enter.length > 0 || exit.length > 0) { - previousKeys.current = keys; + if (enter.length > 0 || exit.length > 0) { + previousKeys.current = keys; + } } - }, [childrenArray, maxSize, minSize, snap]); + }, [childrenArray, dimensionsInitialized, maxSize, minSize, snap]); useResizeObserver({ ref: containerRef, onResize: ({ width, height }) => { if (width && height) { splitViewRef.current?.layout(vertical ? height : width); + layoutService.current.layout(vertical ? height : width); + setDimensionsInitialized(true); } }, }); @@ -320,7 +358,7 @@ const Allotment = forwardRef( )} >
- {React.Children.toArray(children).map((child, index) => { + {React.Children.toArray(children).map((child) => { if (!React.isValidElement(child)) { return null; } diff --git a/src/helpers/string.ts b/src/helpers/string.ts new file mode 100644 index 00000000..7c81064a --- /dev/null +++ b/src/helpers/string.ts @@ -0,0 +1,10 @@ +/** + * Checks if `string` ends with the given target string. + */ +export function endsWith(string: string, target: string): boolean { + const length = string.length; + + const position = length - target.length; + + return position >= 0 && string.slice(position, length) === target; +} diff --git a/src/split-view/split-view.ts b/src/split-view/split-view.ts index 4c51bee6..63f5aeb8 100644 --- a/src/split-view/split-view.ts +++ b/src/split-view/split-view.ts @@ -4,6 +4,7 @@ import clamp from "lodash.clamp"; import styles from "../allotment.module.css"; import { pushToEnd, range } from "../helpers/array"; import { Disposable } from "../helpers/disposable"; +import { endsWith } from "../helpers/string"; import { Orientation, Sash, @@ -145,6 +146,92 @@ export interface View { setVisible?(visible: boolean): void; } +export class LayoutService { + private _size!: number; + + get size() { + return this._size; + } + + public layout(size: number) { + this._size = size; + } +} + +export interface PaneViewOptions { + element: HTMLElement; + minimumSize?: number; + maximumSize?: number; + preferredSize?: number | string; + snap?: boolean; +} + +export class PaneView implements View { + public minimumSize: number = 0; + public maximumSize: number = Number.POSITIVE_INFINITY; + + readonly element: HTMLElement; + readonly snap: boolean; + + private layoutService: LayoutService; + private _preferredSize: () => number | undefined; + + get preferredSize() { + return this._preferredSize(); + } + + constructor(layoutService: LayoutService, options: PaneViewOptions) { + this.layoutService = layoutService; + this.element = options.element; + + this.minimumSize = + typeof options.minimumSize === "number" ? options.minimumSize : 30; + + this.maximumSize = + typeof options.maximumSize === "number" + ? options.maximumSize + : Number.POSITIVE_INFINITY; + + if (typeof options.preferredSize === "number") { + this._preferredSize = () => { + return options.preferredSize as number; + }; + } else if (typeof options.preferredSize === "string") { + if (endsWith(options.preferredSize, "%")) { + const percentage = Number(options.preferredSize.slice(0, -1)) / 100; + + this._preferredSize = () => { + return percentage * this.layoutService.size; + }; + } else if (endsWith(options.preferredSize, "px")) { + const pixels = Number(options.preferredSize.slice(0, -2)) / 100; + + this._preferredSize = () => { + return pixels; + }; + } else if (Number.parseFloat(options.preferredSize)) { + const number = Number.parseFloat(options.preferredSize); + + this._preferredSize = () => { + return number; + }; + } else { + this._preferredSize = () => { + return undefined; + }; + } + } else { + this._preferredSize = () => { + return undefined; + }; + } + + this.snap = typeof options.snap === "boolean" ? options.snap : false; + } + + layout(_size: number): void {} +} + type ViewItemSize = number | { cachedVisibleSize: number }; abstract class ViewItem { @@ -662,7 +749,6 @@ export class SplitView extends EventEmitter implements Disposable { for (const item of flexibleViewItems) { item.size = clamp(size, item.minimumSize, item.maximumSize); - console.log(item.size); } this.relayout(); diff --git a/stories/advanced.stories.module.css b/stories/advanced.stories.module.css index 5fd6bca5..9442014c 100644 --- a/stories/advanced.stories.module.css +++ b/stories/advanced.stories.module.css @@ -3,5 +3,5 @@ color: #cccccc; font-family: sans-serif; height: 480px; - width: 640px; + width: 1024px; } diff --git a/stories/advanced.stories.tsx b/stories/advanced.stories.tsx index 0f101a0d..5d707b87 100644 --- a/stories/advanced.stories.tsx +++ b/stories/advanced.stories.tsx @@ -57,7 +57,13 @@ export const VisualStudioCode: Story = ({ const [openEditors, setOpenEditors] = useState(DOCUMENTS); const sidebar = ( - + - + { diff --git a/stories/allotment.stories.tsx b/stories/allotment.stories.tsx index 7c63864b..472463ce 100644 --- a/stories/allotment.stories.tsx +++ b/stories/allotment.stories.tsx @@ -385,3 +385,19 @@ export const FixedSize: Story = (args) => { ); }; FixedSize.args = {}; + +export const PreferredSize: Story = (args) => { + return ( +
+ + + + + + + + +
+ ); +}; +PreferredSize.args = {};