diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 52156e5a1..986267fe3 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -11,6 +11,7 @@ "/packages/docs/sandboxes/dockview-app", "/packages/docs/sandboxes/events-dockview", "/packages/docs/sandboxes/externaldnd-dockview", + "/packages/docs/sandboxes/floatinggroup-dockview", "/packages/docs/sandboxes/fullwidthtab-dockview", "/packages/docs/sandboxes/groupcontol-dockview", "/packages/docs/sandboxes/iframe-dockview", @@ -31,4 +32,4 @@ "/packages/docs/sandboxes/javascript/vanilla-dockview" ], "node": "16" -} \ No newline at end of file +} diff --git a/packages/dockview-core/src/dnd/overlay.ts b/packages/dockview-core/src/dnd/overlay.ts index 29fb4376f..236726f1b 100644 --- a/packages/dockview-core/src/dnd/overlay.ts +++ b/packages/dockview-core/src/dnd/overlay.ts @@ -1,3 +1,4 @@ +import { toHaveDescription } from '@testing-library/jest-dom/matchers'; import { getElementsByTagName, quasiDefaultPrevented, @@ -39,6 +40,14 @@ export class Overlay extends CompositeDisposable { private static MINIMUM_HEIGHT = 20; private static MINIMUM_WIDTH = 20; + set minimumInViewportWidth(value: number | undefined) { + this.options.minimumInViewportWidth = value; + } + + set minimumInViewportHeight(value: number | undefined) { + this.options.minimumInViewportHeight = value; + } + constructor( private readonly options: { height: number; @@ -47,8 +56,8 @@ export class Overlay extends CompositeDisposable { top: number; container: HTMLElement; content: HTMLElement; - minimumInViewportWidth: number; - minimumInViewportHeight: number; + minimumInViewportWidth?: number; + minimumInViewportHeight?: number; } ) { super(); @@ -105,16 +114,13 @@ export class Overlay extends CompositeDisposable { // region: ensure bounds within allowable limits // a minimum width of minimumViewportWidth must be inside the viewport - const xOffset = Math.max( - 0, - overlayRect.width - this.options.minimumInViewportWidth - ); + const xOffset = Math.max(0, this.getMinimumWidth(overlayRect.width)); // a minimum height of minimumViewportHeight must be inside the viewport - const yOffset = Math.max( - 0, - overlayRect.height - this.options.minimumInViewportHeight - ); + const yOffset = + typeof this.options.minimumInViewportHeight === 'number' + ? Math.max(0, this.getMinimumHeight(overlayRect.height)) + : 0; const left = clamp( overlayRect.left - containerRect.left, @@ -194,12 +200,13 @@ export class Overlay extends CompositeDisposable { const xOffset = Math.max( 0, - overlayRect.width - this.options.minimumInViewportWidth + this.getMinimumWidth(overlayRect.width) ); const yOffset = Math.max( 0, - overlayRect.height - - this.options.minimumInViewportHeight + this.options.minimumInViewportHeight + ? this.getMinimumHeight(overlayRect.height) + : 0 ); const left = clamp( @@ -350,20 +357,16 @@ export class Overlay extends CompositeDisposable { let left: number | undefined = undefined; let width: number | undefined = undefined; - const minimumInViewportHeight = - this.options.minimumInViewportHeight; - const minimumInViewportWidth = - this.options.minimumInViewportWidth; - - function moveTop(): void { + const moveTop = () => { top = clamp( y, -Number.MAX_VALUE, startPosition!.originalY + startPosition!.originalHeight > containerRect.height - ? containerRect.height - - minimumInViewportHeight + ? this.getMinimumHeight( + containerRect.height + ) : Math.max( 0, startPosition!.originalY + @@ -375,31 +378,33 @@ export class Overlay extends CompositeDisposable { startPosition!.originalY + startPosition!.originalHeight - top; - } + }; - function moveBottom(): void { + const moveBottom = () => { top = startPosition!.originalY - startPosition!.originalHeight; height = clamp( y - top, - top < 0 - ? -top + minimumInViewportHeight + top < 0 && + typeof this.options + .minimumInViewportHeight === 'number' + ? -top + + this.options.minimumInViewportHeight : Overlay.MINIMUM_HEIGHT, Number.MAX_VALUE ); - } + }; - function moveLeft(): void { + const moveLeft = () => { left = clamp( x, -Number.MAX_VALUE, startPosition!.originalX + startPosition!.originalWidth > containerRect.width - ? containerRect.width - - minimumInViewportWidth + ? this.getMinimumWidth(containerRect.width) : Math.max( 0, startPosition!.originalX + @@ -412,21 +417,24 @@ export class Overlay extends CompositeDisposable { startPosition!.originalX + startPosition!.originalWidth - left; - } + }; - function moveRight(): void { + const moveRight = () => { left = startPosition!.originalX - startPosition!.originalWidth; width = clamp( x - left, - left < 0 - ? -left + minimumInViewportWidth + left < 0 && + typeof this.options + .minimumInViewportWidth === 'number' + ? -left + + this.options.minimumInViewportWidth : Overlay.MINIMUM_WIDTH, Number.MAX_VALUE ); - } + }; switch (direction) { case 'top': @@ -477,6 +485,20 @@ export class Overlay extends CompositeDisposable { ); } + private getMinimumWidth(width: number) { + if (typeof this.options.minimumInViewportWidth === 'number') { + return width - this.options.minimumInViewportWidth; + } + return 0; + } + + private getMinimumHeight(height: number) { + if (typeof this.options.minimumInViewportHeight === 'number') { + return height - this.options.minimumInViewportHeight; + } + return height; + } + override dispose(): void { this._element.remove(); super.dispose(); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 118fc9632..04a0eb280 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -56,6 +56,8 @@ import { TabDragEvent, } from './components/titlebar/tabsContainer'; +const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; + export interface PanelReference { update: (event: { params: { [key: string]: any } }) => void; remove: () => void; @@ -91,6 +93,7 @@ export type DockviewComponentUpdateOptions = Pick< | 'createLeftHeaderActionsElement' | 'createRightHeaderActionsElement' | 'disableFloatingGroups' + | 'floatingGroupBounds' >; export interface DockviewDropEvent extends GroupviewDropEvent { @@ -378,8 +381,18 @@ export class DockviewComponent width: coord?.width ?? 300, left: overlayLeft, top: overlayTop, - minimumInViewportWidth: 100, - minimumInViewportHeight: 100, + minimumInViewportWidth: + this.options.floatingGroupBounds === 'boundedWithinViewport' + ? undefined + : this.options.floatingGroupBounds + ?.minimumWidthWithinViewport ?? + DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, + minimumInViewportHeight: + this.options.floatingGroupBounds === 'boundedWithinViewport' + ? undefined + : this.options.floatingGroupBounds + ?.minimumHeightWithinViewport ?? + DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, }); const el = group.element.querySelector('.void-container'); @@ -475,6 +488,9 @@ export class DockviewComponent const hasOrientationChanged = typeof options.orientation === 'string' && this.gridview.orientation !== options.orientation; + const hasFloatingGroupOptionsChanged = + options.floatingGroupBounds !== undefined && + options.floatingGroupBounds !== this.options.floatingGroupBounds; this._options = { ...this.options, ...options }; @@ -482,6 +498,30 @@ export class DockviewComponent this.gridview.orientation = options.orientation!; } + if (hasFloatingGroupOptionsChanged) { + for (const group of this.floatingGroups) { + switch (this.options.floatingGroupBounds) { + case 'boundedWithinViewport': + group.overlay.minimumInViewportHeight = undefined; + group.overlay.minimumInViewportWidth = undefined; + break; + case undefined: + group.overlay.minimumInViewportHeight = + DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE; + group.overlay.minimumInViewportWidth = + DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE; + break; + default: + group.overlay.minimumInViewportHeight = + this.options.floatingGroupBounds?.minimumHeightWithinViewport; + group.overlay.minimumInViewportWidth = + this.options.floatingGroupBounds?.minimumWidthWithinViewport; + } + + group.overlay.setBounds({}); + } + } + this.layout(this.gridview.width, this.gridview.height, true); } diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 23a06e0f5..920737a06 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -87,6 +87,12 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { singleTabMode?: 'fullwidth' | 'default'; parentElement?: HTMLElement; disableFloatingGroups?: boolean; + floatingGroupBounds?: + | 'boundedWithinViewport' + | { + minimumHeightWithinViewport?: number; + minimumWidthWithinViewport?: number; + }; } export interface PanelOptions

{ diff --git a/packages/dockview/src/dockview/dockview.tsx b/packages/dockview/src/dockview/dockview.tsx index 16effc460..7c6bcc435 100644 --- a/packages/dockview/src/dockview/dockview.tsx +++ b/packages/dockview/src/dockview/dockview.tsx @@ -69,6 +69,12 @@ export interface IDockviewReactProps { leftHeaderActionsComponent?: React.FunctionComponent; singleTabMode?: 'fullwidth' | 'default'; disableFloatingGroups?: boolean; + floatingGroupBounds?: + | 'boundedWithinViewport' + | { + minimumHeightWithinViewport?: number; + minimumWidthWithinViewport?: number; + }; } const DEFAULT_REACT_TAB = 'props.defaultTabComponent'; @@ -162,6 +168,7 @@ export const DockviewReact = React.forwardRef( ), singleTabMode: props.singleTabMode, disableFloatingGroups: props.disableFloatingGroups, + floatingGroupBounds: props.floatingGroupBounds, }); const { clientWidth, clientHeight } = domRef.current; @@ -205,6 +212,15 @@ export const DockviewReact = React.forwardRef( }); }, [props.components]); + React.useEffect(() => { + if (!dockviewRef.current) { + return; + } + dockviewRef.current.updateOptions({ + floatingGroupBounds: props.floatingGroupBounds, + }); + }, [props.floatingGroupBounds]); + React.useEffect(() => { if (!dockviewRef.current) { return; diff --git a/packages/docs/docs/components/dockview.mdx b/packages/docs/docs/components/dockview.mdx index 05ad7ac86..25f67edc5 100644 --- a/packages/docs/docs/components/dockview.mdx +++ b/packages/docs/docs/components/dockview.mdx @@ -405,6 +405,12 @@ Floating groups can be interacted with whilst holding the `shift` key activating Floating groups can be programatically added through the dockview `api` method `api.addFloatingGroup(...)` and you can check whether a group is floating via the `group.api.isFloating` property. See examples for full code. +You can control the bounding box of floating groups through the optional `floatingGroupBounds` options: + +- `boundedWithinViewport` will force the entire floating group to be bounded within the docks viewport. +- `{minimumHeightWithinViewport?: number, minimumWidthWithinViewport?: number}` sets the respective dimension minimums that must appears within the docks viewport +- If no options are provided the defaults of `100px` minimum height and width within the viewport are set. + { setApi(event.api); }; + const [options, setOptions] = React.useState< + 'boundedWithinViewport' | undefined + >(undefined); + return (

{ > Add Floating Group +