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/dockview-app",
"/packages/docs/sandboxes/events-dockview", "/packages/docs/sandboxes/events-dockview",
"/packages/docs/sandboxes/externaldnd-dockview", "/packages/docs/sandboxes/externaldnd-dockview",
"/packages/docs/sandboxes/floatinggroup-dockview",
"/packages/docs/sandboxes/fullwidthtab-dockview", "/packages/docs/sandboxes/fullwidthtab-dockview",
"/packages/docs/sandboxes/groupcontol-dockview", "/packages/docs/sandboxes/groupcontol-dockview",
"/packages/docs/sandboxes/iframe-dockview", "/packages/docs/sandboxes/iframe-dockview",
@ -31,4 +32,4 @@
"/packages/docs/sandboxes/javascript/vanilla-dockview" "/packages/docs/sandboxes/javascript/vanilla-dockview"
], ],
"node": "16" "node": "16"
} }

View File

@ -1,3 +1,4 @@
import { toHaveDescription } from '@testing-library/jest-dom/matchers';
import { import {
getElementsByTagName, getElementsByTagName,
quasiDefaultPrevented, quasiDefaultPrevented,
@ -39,6 +40,14 @@ export class Overlay extends CompositeDisposable {
private static MINIMUM_HEIGHT = 20; private static MINIMUM_HEIGHT = 20;
private static MINIMUM_WIDTH = 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( constructor(
private readonly options: { private readonly options: {
height: number; height: number;
@ -47,8 +56,8 @@ export class Overlay extends CompositeDisposable {
top: number; top: number;
container: HTMLElement; container: HTMLElement;
content: HTMLElement; content: HTMLElement;
minimumInViewportWidth: number; minimumInViewportWidth?: number;
minimumInViewportHeight: number; minimumInViewportHeight?: number;
} }
) { ) {
super(); super();
@ -105,16 +114,13 @@ export class Overlay extends CompositeDisposable {
// region: ensure bounds within allowable limits // region: ensure bounds within allowable limits
// a minimum width of minimumViewportWidth must be inside the viewport // a minimum width of minimumViewportWidth must be inside the viewport
const xOffset = Math.max( const xOffset = Math.max(0, this.getMinimumWidth(overlayRect.width));
0,
overlayRect.width - this.options.minimumInViewportWidth
);
// a minimum height of minimumViewportHeight must be inside the viewport // a minimum height of minimumViewportHeight must be inside the viewport
const yOffset = Math.max( const yOffset =
0, typeof this.options.minimumInViewportHeight === 'number'
overlayRect.height - this.options.minimumInViewportHeight ? Math.max(0, this.getMinimumHeight(overlayRect.height))
); : 0;
const left = clamp( const left = clamp(
overlayRect.left - containerRect.left, overlayRect.left - containerRect.left,
@ -194,12 +200,13 @@ export class Overlay extends CompositeDisposable {
const xOffset = Math.max( const xOffset = Math.max(
0, 0,
overlayRect.width - this.options.minimumInViewportWidth this.getMinimumWidth(overlayRect.width)
); );
const yOffset = Math.max( const yOffset = Math.max(
0, 0,
overlayRect.height - this.options.minimumInViewportHeight
this.options.minimumInViewportHeight ? this.getMinimumHeight(overlayRect.height)
: 0
); );
const left = clamp( const left = clamp(
@ -350,20 +357,16 @@ export class Overlay extends CompositeDisposable {
let left: number | undefined = undefined; let left: number | undefined = undefined;
let width: number | undefined = undefined; let width: number | undefined = undefined;
const minimumInViewportHeight = const moveTop = () => {
this.options.minimumInViewportHeight;
const minimumInViewportWidth =
this.options.minimumInViewportWidth;
function moveTop(): void {
top = clamp( top = clamp(
y, y,
-Number.MAX_VALUE, -Number.MAX_VALUE,
startPosition!.originalY + startPosition!.originalY +
startPosition!.originalHeight > startPosition!.originalHeight >
containerRect.height containerRect.height
? containerRect.height - ? this.getMinimumHeight(
minimumInViewportHeight containerRect.height
)
: Math.max( : Math.max(
0, 0,
startPosition!.originalY + startPosition!.originalY +
@ -375,31 +378,33 @@ export class Overlay extends CompositeDisposable {
startPosition!.originalY + startPosition!.originalY +
startPosition!.originalHeight - startPosition!.originalHeight -
top; top;
} };
function moveBottom(): void { const moveBottom = () => {
top = top =
startPosition!.originalY - startPosition!.originalY -
startPosition!.originalHeight; startPosition!.originalHeight;
height = clamp( height = clamp(
y - top, y - top,
top < 0 top < 0 &&
? -top + minimumInViewportHeight typeof this.options
.minimumInViewportHeight === 'number'
? -top +
this.options.minimumInViewportHeight
: Overlay.MINIMUM_HEIGHT, : Overlay.MINIMUM_HEIGHT,
Number.MAX_VALUE Number.MAX_VALUE
); );
} };
function moveLeft(): void { const moveLeft = () => {
left = clamp( left = clamp(
x, x,
-Number.MAX_VALUE, -Number.MAX_VALUE,
startPosition!.originalX + startPosition!.originalX +
startPosition!.originalWidth > startPosition!.originalWidth >
containerRect.width containerRect.width
? containerRect.width - ? this.getMinimumWidth(containerRect.width)
minimumInViewportWidth
: Math.max( : Math.max(
0, 0,
startPosition!.originalX + startPosition!.originalX +
@ -412,21 +417,24 @@ export class Overlay extends CompositeDisposable {
startPosition!.originalX + startPosition!.originalX +
startPosition!.originalWidth - startPosition!.originalWidth -
left; left;
} };
function moveRight(): void { const moveRight = () => {
left = left =
startPosition!.originalX - startPosition!.originalX -
startPosition!.originalWidth; startPosition!.originalWidth;
width = clamp( width = clamp(
x - left, x - left,
left < 0 left < 0 &&
? -left + minimumInViewportWidth typeof this.options
.minimumInViewportWidth === 'number'
? -left +
this.options.minimumInViewportWidth
: Overlay.MINIMUM_WIDTH, : Overlay.MINIMUM_WIDTH,
Number.MAX_VALUE Number.MAX_VALUE
); );
} };
switch (direction) { switch (direction) {
case 'top': 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 { override dispose(): void {
this._element.remove(); this._element.remove();
super.dispose(); super.dispose();

View File

@ -56,6 +56,8 @@ import {
TabDragEvent, TabDragEvent,
} from './components/titlebar/tabsContainer'; } from './components/titlebar/tabsContainer';
const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100;
export interface PanelReference { export interface PanelReference {
update: (event: { params: { [key: string]: any } }) => void; update: (event: { params: { [key: string]: any } }) => void;
remove: () => void; remove: () => void;
@ -91,6 +93,7 @@ export type DockviewComponentUpdateOptions = Pick<
| 'createLeftHeaderActionsElement' | 'createLeftHeaderActionsElement'
| 'createRightHeaderActionsElement' | 'createRightHeaderActionsElement'
| 'disableFloatingGroups' | 'disableFloatingGroups'
| 'floatingGroupBounds'
>; >;
export interface DockviewDropEvent extends GroupviewDropEvent { export interface DockviewDropEvent extends GroupviewDropEvent {
@ -378,8 +381,18 @@ export class DockviewComponent
width: coord?.width ?? 300, width: coord?.width ?? 300,
left: overlayLeft, left: overlayLeft,
top: overlayTop, top: overlayTop,
minimumInViewportWidth: 100, minimumInViewportWidth:
minimumInViewportHeight: 100, 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'); const el = group.element.querySelector('.void-container');
@ -475,6 +488,9 @@ export class DockviewComponent
const hasOrientationChanged = const hasOrientationChanged =
typeof options.orientation === 'string' && typeof options.orientation === 'string' &&
this.gridview.orientation !== options.orientation; this.gridview.orientation !== options.orientation;
const hasFloatingGroupOptionsChanged =
options.floatingGroupBounds !== undefined &&
options.floatingGroupBounds !== this.options.floatingGroupBounds;
this._options = { ...this.options, ...options }; this._options = { ...this.options, ...options };
@ -482,6 +498,30 @@ export class DockviewComponent
this.gridview.orientation = options.orientation!; 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); this.layout(this.gridview.width, this.gridview.height, true);
} }

View File

@ -87,6 +87,12 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions {
singleTabMode?: 'fullwidth' | 'default'; singleTabMode?: 'fullwidth' | 'default';
parentElement?: HTMLElement; parentElement?: HTMLElement;
disableFloatingGroups?: boolean; disableFloatingGroups?: boolean;
floatingGroupBounds?:
| 'boundedWithinViewport'
| {
minimumHeightWithinViewport?: number;
minimumWidthWithinViewport?: number;
};
} }
export interface PanelOptions<P extends object = Parameters> { export interface PanelOptions<P extends object = Parameters> {

View File

@ -69,6 +69,12 @@ export interface IDockviewReactProps {
leftHeaderActionsComponent?: React.FunctionComponent<IDockviewHeaderActionsProps>; leftHeaderActionsComponent?: React.FunctionComponent<IDockviewHeaderActionsProps>;
singleTabMode?: 'fullwidth' | 'default'; singleTabMode?: 'fullwidth' | 'default';
disableFloatingGroups?: boolean; disableFloatingGroups?: boolean;
floatingGroupBounds?:
| 'boundedWithinViewport'
| {
minimumHeightWithinViewport?: number;
minimumWidthWithinViewport?: number;
};
} }
const DEFAULT_REACT_TAB = 'props.defaultTabComponent'; const DEFAULT_REACT_TAB = 'props.defaultTabComponent';
@ -162,6 +168,7 @@ export const DockviewReact = React.forwardRef(
), ),
singleTabMode: props.singleTabMode, singleTabMode: props.singleTabMode,
disableFloatingGroups: props.disableFloatingGroups, disableFloatingGroups: props.disableFloatingGroups,
floatingGroupBounds: props.floatingGroupBounds,
}); });
const { clientWidth, clientHeight } = domRef.current; const { clientWidth, clientHeight } = domRef.current;
@ -205,6 +212,15 @@ export const DockviewReact = React.forwardRef(
}); });
}, [props.components]); }, [props.components]);
React.useEffect(() => {
if (!dockviewRef.current) {
return;
}
dockviewRef.current.updateOptions({
floatingGroupBounds: props.floatingGroupBounds,
});
}, [props.floatingGroupBounds]);
React.useEffect(() => { React.useEffect(() => {
if (!dockviewRef.current) { if (!dockviewRef.current) {
return; 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 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. 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 <MultiFrameworkContainer
height={600} height={600}
sandboxId="floatinggroup-dockview" sandboxId="floatinggroup-dockview"

View File

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