Dynamic Dashboards: Improve drag and drop for responsive grid (#102613)
This commit is contained in:
@@ -151,7 +151,7 @@ export function PanelChrome({
|
||||
const panelContentId = useId();
|
||||
const panelTitleId = useId().replace(/:/g, '_');
|
||||
const { isSelected, onSelect, isSelectable } = useElementSelection(selectionId);
|
||||
const pointerDownEvt = useRef<React.PointerEvent | undefined>();
|
||||
const pointerDownPos = useRef<{ screenX: number; screenY: number }>({ screenX: 0, screenY: 0 });
|
||||
|
||||
const hasHeader = !hoverHeader;
|
||||
|
||||
@@ -203,11 +203,11 @@ export function PanelChrome({
|
||||
evt.stopPropagation();
|
||||
|
||||
const distance = Math.hypot(
|
||||
pointerDownEvt.current?.screenX ?? 0 - evt.screenX,
|
||||
pointerDownEvt.current?.screenY ?? 0 - evt.screenY
|
||||
pointerDownPos.current.screenX - evt.screenX,
|
||||
pointerDownPos.current.screenY - evt.screenY
|
||||
);
|
||||
|
||||
pointerDownEvt.current = undefined;
|
||||
pointerDownPos.current = { screenX: 0, screenY: 0 };
|
||||
|
||||
// If we are dragging some distance or clicking on elements that should cancel dragging (panel menu, etc)
|
||||
if (
|
||||
@@ -222,7 +222,10 @@ export function PanelChrome({
|
||||
|
||||
const onPointerDown = (evt: React.PointerEvent) => {
|
||||
evt.stopPropagation();
|
||||
pointerDownEvt.current = evt;
|
||||
|
||||
pointerDownPos.current = { screenX: evt.screenX, screenY: evt.screenY };
|
||||
|
||||
onDragStart?.(evt);
|
||||
};
|
||||
|
||||
const headerContent = (
|
||||
@@ -350,11 +353,6 @@ export function PanelChrome({
|
||||
className={cx(styles.headerContainer, dragClass)}
|
||||
style={headerStyles}
|
||||
data-testid="header-container"
|
||||
onPointerMove={() => {
|
||||
if (pointerDownEvt.current) {
|
||||
onDragStart?.(pointerDownEvt.current);
|
||||
}
|
||||
}}
|
||||
onPointerDown={onPointerDown}
|
||||
onMouseEnter={isSelectable ? onHeaderEnter : undefined}
|
||||
onMouseLeave={isSelectable ? onHeaderLeave : undefined}
|
||||
|
||||
@@ -14,6 +14,11 @@ export class LayoutOrchestrator extends SceneObjectBase<LayoutOrchestratorState>
|
||||
/** Offset from top-left corner of drag handle. */
|
||||
public dragOffset = { top: 0, left: 0 };
|
||||
|
||||
/** The drop zone closest to the current mouse position while dragging. */
|
||||
public activeDropZone: (DropZone & { layout: SceneObjectRef<SceneLayoutWithDragAndDrop> }) | undefined;
|
||||
|
||||
private _sceneLayouts: SceneLayoutWithDragAndDrop[] = [];
|
||||
|
||||
/** Used in `ResponsiveGridLayout`'s `onPointerDown` method */
|
||||
public onDragStart = (e: PointerEvent, panel: VizPanel) => {
|
||||
const closestLayoutItem = closestOfType(panel, isDashboardLayoutItem);
|
||||
@@ -27,18 +32,39 @@ export class LayoutOrchestrator extends SceneObjectBase<LayoutOrchestratorState>
|
||||
return;
|
||||
}
|
||||
|
||||
this._sceneLayouts = sceneGraph
|
||||
.findAllObjects(this.getRoot(), isSceneLayoutWithDragAndDrop)
|
||||
.filter(isSceneLayoutWithDragAndDrop);
|
||||
|
||||
document.body.setPointerCapture(e.pointerId);
|
||||
|
||||
const targetRect = e.target.getBoundingClientRect();
|
||||
this.dragOffset = { top: e.y - targetRect.top, left: e.x - targetRect.left };
|
||||
this.setState({ activeLayoutItemRef: closestLayoutItem.getRef() });
|
||||
|
||||
closestLayoutItem.containerRef.current?.style.setProperty('--x-pos', `${e.x}px`);
|
||||
closestLayoutItem.containerRef.current?.style.setProperty('--y-pos', `${e.y}px`);
|
||||
|
||||
const state: Partial<LayoutOrchestratorState> = { activeLayoutItemRef: closestLayoutItem.getRef() };
|
||||
|
||||
this._adjustXY({ x: e.x, y: e.y }, closestLayoutItem);
|
||||
|
||||
this.activeDropZone = this.findClosestDropZone({ x: e.clientX, y: e.clientY });
|
||||
if (this.activeDropZone) {
|
||||
state.placeholder = new DropZonePlaceholder({
|
||||
top: this.activeDropZone.top,
|
||||
left: this.activeDropZone.left,
|
||||
width: this.activeDropZone.right - this.activeDropZone.left,
|
||||
height: this.activeDropZone.bottom - this.activeDropZone.top,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState(state);
|
||||
|
||||
document.addEventListener('pointermove', this.onDrag);
|
||||
document.addEventListener('pointerup', this.onDragEnd);
|
||||
document.body.classList.add('dragging-active');
|
||||
};
|
||||
|
||||
/** The drop zone closest to the current mouse position while dragging. */
|
||||
public activeDropZone: (DropZone & { layout: SceneObjectRef<SceneLayoutWithDragAndDrop> }) | undefined;
|
||||
|
||||
/** Called every tick while a panel is actively being dragged */
|
||||
public onDrag = (e: PointerEvent) => {
|
||||
const layoutItemContainer = this.state.activeLayoutItemRef?.resolve().containerRef.current;
|
||||
@@ -49,9 +75,10 @@ export class LayoutOrchestrator extends SceneObjectBase<LayoutOrchestratorState>
|
||||
|
||||
const cursorPos: Point = { x: e.clientX, y: e.clientY };
|
||||
|
||||
layoutItemContainer.style.setProperty('--x-pos', `${cursorPos.x}px`);
|
||||
layoutItemContainer.style.setProperty('--y-pos', `${cursorPos.y}px`);
|
||||
this._adjustXY(cursorPos);
|
||||
|
||||
const closestDropZone = this.findClosestDropZone(cursorPos);
|
||||
|
||||
if (!dropZonesAreEqual(this.activeDropZone, closestDropZone)) {
|
||||
this.activeDropZone = closestDropZone;
|
||||
if (this.activeDropZone) {
|
||||
@@ -90,15 +117,7 @@ export class LayoutOrchestrator extends SceneObjectBase<LayoutOrchestratorState>
|
||||
}
|
||||
|
||||
this.moveLayoutItem(activeLayoutItem, targetLayout);
|
||||
this.setState({
|
||||
activeLayoutItemRef: undefined,
|
||||
});
|
||||
this.state.placeholder?.setState({
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
this.setState({ activeLayoutItemRef: undefined, placeholder: undefined });
|
||||
this.activeDropZone = undefined;
|
||||
activeLayoutItemContainer?.removeAttribute('style');
|
||||
};
|
||||
@@ -116,12 +135,9 @@ export class LayoutOrchestrator extends SceneObjectBase<LayoutOrchestratorState>
|
||||
}
|
||||
|
||||
public findClosestDropZone(p: Point) {
|
||||
const sceneLayouts = sceneGraph
|
||||
.findAllObjects(this.getRoot(), isSceneLayoutWithDragAndDrop)
|
||||
.filter(isSceneLayoutWithDragAndDrop);
|
||||
let closestDropZone: (DropZone & { layout: SceneObjectRef<SceneLayoutWithDragAndDrop> }) | undefined = undefined;
|
||||
let closestDistance = Number.MAX_VALUE;
|
||||
for (const layout of sceneLayouts) {
|
||||
for (const layout of this._sceneLayouts) {
|
||||
const curClosestDropZone = layout.closestDropZone(p);
|
||||
if (curClosestDropZone.distanceToPoint < closestDistance) {
|
||||
closestDropZone = { ...curClosestDropZone, layout: layout.getRef() };
|
||||
@@ -131,6 +147,12 @@ export class LayoutOrchestrator extends SceneObjectBase<LayoutOrchestratorState>
|
||||
|
||||
return closestDropZone;
|
||||
}
|
||||
|
||||
private _adjustXY(p: Point, activeLayoutItem = this.state.activeLayoutItemRef?.resolve()) {
|
||||
const container = activeLayoutItem?.containerRef.current;
|
||||
container?.style.setProperty('--x-pos', `${p.x}px`);
|
||||
container?.style.setProperty('--y-pos', `${p.y}px`);
|
||||
}
|
||||
}
|
||||
|
||||
function dropZonesAreEqual(a?: DropZone, b?: DropZone) {
|
||||
|
||||
+4
-1
@@ -26,6 +26,9 @@ export interface ResponsiveGridLayoutState extends SceneObjectState, ResponsiveG
|
||||
|
||||
/** True when the items should be lazy loaded */
|
||||
isLazy?: boolean;
|
||||
|
||||
/** True when the items should be draggable */
|
||||
isDraggable?: boolean;
|
||||
}
|
||||
|
||||
export interface ResponsiveGridLayoutOptions {
|
||||
@@ -87,7 +90,7 @@ export class ResponsiveGridLayout
|
||||
};
|
||||
|
||||
public isDraggable(): boolean {
|
||||
return true;
|
||||
return this.state.isDraggable ?? false;
|
||||
}
|
||||
|
||||
public getDragClass(): string {
|
||||
|
||||
+11
-9
@@ -5,7 +5,12 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan
|
||||
import { NewObjectAddedToCanvasEvent, ObjectRemovedFromCanvasEvent } from '../../edit-pane/shared';
|
||||
import { joinCloneKeys } from '../../utils/clone';
|
||||
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
|
||||
import { getGridItemKeyForPanelId, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils';
|
||||
import {
|
||||
forceRenderChildren,
|
||||
getGridItemKeyForPanelId,
|
||||
getPanelIdForVizPanel,
|
||||
getVizPanelKeyForPanelId,
|
||||
} from '../../utils/utils';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
@@ -45,14 +50,6 @@ export class ResponsiveGridLayoutManager
|
||||
autoRows: 'minmax(300px, auto)',
|
||||
};
|
||||
|
||||
public constructor(state: ResponsiveGridLayoutManagerState) {
|
||||
super(state);
|
||||
|
||||
// @ts-ignore
|
||||
this.state.layout.getDragClassCancel = () => 'drag-cancel';
|
||||
this.state.layout.isDraggable = () => true;
|
||||
}
|
||||
|
||||
public addPanel(vizPanel: VizPanel) {
|
||||
const panelId = dashboardSceneGraph.getNextPanelId(this);
|
||||
|
||||
@@ -131,6 +128,11 @@ export class ResponsiveGridLayoutManager
|
||||
return panels;
|
||||
}
|
||||
|
||||
public editModeChanged(isEditing: boolean) {
|
||||
this.state.layout.setState({ isDraggable: isEditing });
|
||||
forceRenderChildren(this.state.layout, true);
|
||||
}
|
||||
|
||||
public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
|
||||
return this.clone({
|
||||
layout: this.state.layout.clone({
|
||||
|
||||
+2
-2
@@ -59,8 +59,9 @@ export function ResponsiveGridLayoutRenderer({ model }: SceneComponentProps<Resp
|
||||
? {
|
||||
width: item.cachedBoundingBox.right - item.cachedBoundingBox.left,
|
||||
height: item.cachedBoundingBox.bottom - item.cachedBoundingBox.top,
|
||||
// adjust the panel position to mouse position
|
||||
translate: `${-layoutOrchestrator.dragOffset.left}px ${-layoutOrchestrator.dragOffset.top}px`,
|
||||
// --x/y-pos are set in LayoutOrchestrator
|
||||
// adjust the panel position on the screen
|
||||
transform: `translate(var(--x-pos), var(--y-pos))`,
|
||||
}
|
||||
: {}
|
||||
@@ -106,7 +107,6 @@ const getStyles = (theme: GrafanaTheme2, state: ResponsiveGridLayoutState) => ({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
dragging: css({
|
||||
position: 'fixed',
|
||||
|
||||
Reference in New Issue
Block a user