mirror of
https://github.com/mathuo/dockview
synced 2025-02-13 03:45:47 +00:00
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:
commit
bac862a5fc
@ -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",
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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> {
|
||||||
|
@ -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;
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user