import { PointerEvent as ReactPointerEvent } from 'react'; import { sceneGraph, SceneObjectBase, SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes'; import { createPointerDistance } from '@grafana/ui'; import { DashboardScene } from './DashboardScene'; import { DashboardDropTarget, isDashboardDropTarget } from './types/DashboardDropTarget'; interface DashboardLayoutOrchestratorState extends SceneObjectState { draggingPanel?: SceneObjectRef; } export class DashboardLayoutOrchestrator extends SceneObjectBase { private _sourceDropTarget: DashboardDropTarget | null = null; private _lastDropTarget: DashboardDropTarget | null = null; private _pointerDistance = createPointerDistance(); private _isSelectedObject = false; public constructor() { super({}); this._onPointerMove = this._onPointerMove.bind(this); this._stopDraggingSync = this._stopDraggingSync.bind(this); this.addActivationHandler(() => this._activationHandler()); } private _activationHandler() { return () => { document.body.removeEventListener('pointermove', this._onPointerMove); document.body.removeEventListener('pointerup', this._stopDraggingSync); }; } public startDraggingSync(evt: ReactPointerEvent, panel: VizPanel): void { this._pointerDistance.set(evt); this._isSelectedObject = false; const dropTarget = sceneGraph.findObject(panel, isDashboardDropTarget); if (!dropTarget || !isDashboardDropTarget(dropTarget)) { return; } this._sourceDropTarget = dropTarget; this._lastDropTarget = dropTarget; document.body.addEventListener('pointermove', this._onPointerMove); document.body.addEventListener('pointerup', this._stopDraggingSync); this.setState({ draggingPanel: panel.getRef() }); } private _stopDraggingSync(_evt: PointerEvent) { const panel = this.state.draggingPanel?.resolve(); if (this._sourceDropTarget !== this._lastDropTarget) { // Wrapped in setTimeout to ensure that any event handlers are called // Useful for allowing react-grid-layout to remove placeholders, etc. setTimeout(() => { this._sourceDropTarget?.draggedPanelOutside?.(panel!); this._lastDropTarget?.draggedPanelInside?.(panel!); }); } document.body.removeEventListener('pointermove', this._onPointerMove); document.body.removeEventListener('pointerup', this._stopDraggingSync); this.setState({ draggingPanel: undefined }); } private _onPointerMove(evt: PointerEvent) { if (!this._isSelectedObject && this.state.draggingPanel && this._pointerDistance.check(evt)) { this._isSelectedObject = true; const panel = this.state.draggingPanel?.resolve(); this._getDashboard().state.editPane.selectObject(panel, panel.state.key!, { force: true, multi: false }); } const dropTarget = this._getDropTargetUnderMouse(evt) ?? this._sourceDropTarget; if (!dropTarget) { return; } if (dropTarget !== this._lastDropTarget) { this._lastDropTarget?.setIsDropTarget?.(false); this._lastDropTarget = dropTarget; if (dropTarget !== this._sourceDropTarget) { dropTarget.setIsDropTarget?.(true); } } } private _getDashboard(): DashboardScene { if (!(this.parent instanceof DashboardScene)) { throw new Error('Parent is not a DashboardScene'); } return this.parent; } private _getDropTargetUnderMouse(evt: MouseEvent): DashboardDropTarget | null { const elementsUnderPoint = document.elementsFromPoint(evt.clientX, evt.clientY); const cursorIsInSourceTarget = elementsUnderPoint.some( (el) => el.getAttribute('data-dashboard-drop-target-key') === this._sourceDropTarget?.state.key ); if (cursorIsInSourceTarget) { return null; } const key = elementsUnderPoint ?.find((element) => element.getAttribute('data-dashboard-drop-target-key')) ?.getAttribute('data-dashboard-drop-target-key'); if (!key) { return null; } const sceneObject = sceneGraph.findByKey(this._getDashboard(), key); if (!sceneObject || !isDashboardDropTarget(sceneObject)) { return null; } return sceneObject; } }