Merge pull request #327 from mathuo/326-possible-to-stop-floating-windows-being-dragged-outside-visible-dockview-area

feat: floating group viewport rules
This commit is contained in:
mathuo 2023-09-12 22:35:59 +01:00 committed by GitHub
commit bac862a5fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 144 additions and 37 deletions

View File

@ -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",

View File

@ -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();

View File

@ -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);
}

View File

@ -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<P extends object = Parameters> {

View File

@ -69,6 +69,12 @@ export interface IDockviewReactProps {
leftHeaderActionsComponent?: React.FunctionComponent<IDockviewHeaderActionsProps>;
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;

View File

@ -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.
<MultiFrameworkContainer
height={600}
sandboxId="floatinggroup-dockview"

View File

@ -155,6 +155,10 @@ export const DockviewPersistance = (props: { theme?: string }) => {
setApi(event.api);
};
const [options, setOptions] = React.useState<
'boundedWithinViewport' | undefined
>(undefined);
return (
<div
style={{
@ -197,6 +201,17 @@ export const DockviewPersistance = (props: { theme?: string }) => {
>
Add Floating Group
</button>
<button
onClick={() => {
setOptions(
options === undefined
? 'boundedWithinViewport'
: undefined
);
}}
>
{`Bounds: ${options ? 'Within' : 'Overflow'}`}
</button>
<button
onClick={() => {
setDisableFloatingGroups((x) => !x);
@ -219,6 +234,7 @@ export const DockviewPersistance = (props: { theme?: string }) => {
leftHeaderActionsComponent={LeftComponent}
rightHeaderActionsComponent={RightComponent}
disableFloatingGroups={disableFloatingGroups}
floatingGroupBounds={options}
className={`${props.theme || 'dockview-theme-abyss'}`}
/>
</div>