grafana_bak/public/app/features/canvas/runtime/sceneElementManagement.ts
2025-04-01 10:38:02 +09:00

171 lines
5.2 KiB
TypeScript

import { first } from 'rxjs/operators';
import { Placement } from 'app/plugins/panel/canvas/panelcfg.gen';
import { LayerActionID } from 'app/plugins/panel/canvas/types';
import { ElementState } from './element';
import { FrameState } from './frame';
import { RootElement } from './root';
import { Scene } from './scene';
// TODO: Consider whether or not reorderElements + updateElements should be moved to TreeNavigationEditor
// Reorder elements in the DOM when rearranging them in the tree navigation editor
export const reorderElements = (src: ElementState, dest: ElementState, dragToGap: boolean, destPosition: number) => {
switch (dragToGap) {
case true:
switch (destPosition) {
case -1:
// top of the tree
if (src.parent instanceof FrameState) {
// move outside the frame
if (dest.parent) {
updateElements(src, dest.parent, dest.parent.elements.length);
src.updateData(dest.parent.scene.context);
}
} else {
dest.parent?.reorderTree(src, dest, true);
}
break;
default:
if (dest.parent) {
updateElements(src, dest.parent, dest.parent.elements.indexOf(dest));
src.updateData(dest.parent.scene.context);
}
break;
}
break;
case false:
if (dest instanceof FrameState) {
if (src.parent === dest) {
// same frame parent
src.parent?.reorderTree(src, dest, true);
} else {
updateElements(src, dest);
src.updateData(dest.scene.context);
}
} else if (src.parent === dest.parent) {
src.parent?.reorderTree(src, dest);
} else {
if (dest.parent) {
updateElements(src, dest.parent);
src.updateData(dest.parent.scene.context);
}
}
break;
}
};
// Reorders canvas elements
const updateElements = (src: ElementState, dest: FrameState | RootElement, idx: number | null = null) => {
src.parent?.doAction(LayerActionID.Delete, src);
src.parent = dest;
const elementContainer = src.div?.getBoundingClientRect();
src.setPlacementFromConstraint(elementContainer, dest.div?.getBoundingClientRect());
const destIndex = idx ?? dest.elements.length - 1;
dest.elements.splice(destIndex, 0, src);
dest.scene.save();
dest.reinitializeMoveable();
};
// Finds the element state if it exists for a given DOM element
export const findElementByTarget = (target: Element, sceneElements: ElementState[]): ElementState | undefined => {
// We will probably want to add memoization to this as we are calling on drag / resize
const stack = [...sceneElements];
while (stack.length > 0) {
const currentElement = stack.shift();
if (currentElement && currentElement.div && currentElement.div === target) {
return currentElement;
}
const nestedElements = currentElement instanceof FrameState ? currentElement.elements : [];
for (const nestedElement of nestedElements) {
stack.unshift(nestedElement);
}
}
return undefined;
};
// Nest selected elements into a frame object
export const frameSelection = (scene: Scene) => {
scene.selection.pipe(first()).subscribe((currentSelectedElements) => {
const currentLayer = currentSelectedElements[0].parent!;
const newLayer = new FrameState(
{
type: 'frame',
name: scene.getNextElementName(true),
elements: [],
},
scene,
currentSelectedElements[0].parent
);
const framePlacement = generateFrameContainer(currentSelectedElements);
newLayer.options.placement = framePlacement;
currentSelectedElements.forEach((element: ElementState) => {
const elementContainer = element.div?.getBoundingClientRect();
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
element.setPlacementFromConstraint(elementContainer, framePlacement as DOMRect);
currentLayer.doAction(LayerActionID.Delete, element);
newLayer.doAction(LayerActionID.Duplicate, element, false, false);
});
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
newLayer.setPlacementFromConstraint(framePlacement as DOMRect, currentLayer.div?.getBoundingClientRect());
currentLayer.elements.push(newLayer);
scene.byName.set(newLayer.getName(), newLayer);
scene.save();
});
};
// Helper function to generate the a frame object based on the selected elements' dimensions
const generateFrameContainer = (elements: ElementState[]): Placement => {
let minTop = Infinity;
let minLeft = Infinity;
let maxRight = 0;
let maxBottom = 0;
elements.forEach((element: ElementState) => {
const elementContainer = element.div?.getBoundingClientRect();
if (!elementContainer) {
return;
}
if (minTop > elementContainer.top) {
minTop = elementContainer.top;
}
if (minLeft > elementContainer.left) {
minLeft = elementContainer.left;
}
if (maxRight < elementContainer.right) {
maxRight = elementContainer.right;
}
if (maxBottom < elementContainer.bottom) {
maxBottom = elementContainer.bottom;
}
});
return {
top: minTop,
left: minLeft,
width: maxRight - minLeft,
height: maxBottom - minTop,
};
};