mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Refactor scene.tsx for better maintainability (#88517)
Co-authored-by: drew08t <drew08@gmail.com>
This commit is contained in:
parent
2fcc4d8cd7
commit
12a45fdeca
@ -3,6 +3,7 @@ import { MoveableManagerInterface, Renderer } from 'moveable';
|
||||
import { VerticalConstraint, HorizontalConstraint } from 'app/plugins/panel/canvas/panelcfg.gen';
|
||||
|
||||
import { Scene } from './scene';
|
||||
import { findElementByTarget } from './sceneElementManagement';
|
||||
|
||||
export const settingsViewable = (scene: Scene) => ({
|
||||
name: 'settingsViewable',
|
||||
@ -99,7 +100,7 @@ export const constraintViewable = (scene: Scene) => ({
|
||||
events: [],
|
||||
render(moveable: MoveableManagerInterface<unknown, unknown>, React: Renderer) {
|
||||
const rect = moveable.getRect();
|
||||
const targetElement = scene.findElementByTarget(moveable.state.target!);
|
||||
const targetElement = findElementByTarget(moveable.state.target!, scene.root.elements);
|
||||
|
||||
// If selection is more than 1 element don't display constraint visualizations
|
||||
if (scene.selecto?.getSelectedTargets() && scene.selecto?.getSelectedTargets().length > 1) {
|
||||
|
@ -14,6 +14,7 @@ import { canvasElementRegistry } from '../registry';
|
||||
import { ElementState } from './element';
|
||||
import { RootElement } from './root';
|
||||
import { Scene } from './scene';
|
||||
import { initMoveable } from './sceneAbleManagement';
|
||||
|
||||
const DEFAULT_OFFSET = 10;
|
||||
const HORIZONTAL_OFFSET = 50;
|
||||
@ -111,7 +112,7 @@ export class FrameState extends ElementState {
|
||||
reinitializeMoveable() {
|
||||
// Need to first clear current selection and then re-init moveable with slight delay
|
||||
this.scene.clearCurrentSelection();
|
||||
setTimeout(() => this.scene.initMoveable(true, this.scene.isEditingEnabled));
|
||||
setTimeout(() => initMoveable(true, this.scene.isEditingEnabled, this.scene));
|
||||
}
|
||||
|
||||
// ??? or should this be on the element directly?
|
||||
|
@ -3,7 +3,6 @@ import Moveable from 'moveable';
|
||||
import React, { createRef, CSSProperties, RefObject } from 'react';
|
||||
import { ReactZoomPanPinchContentRef } from 'react-zoom-pan-pinch';
|
||||
import { BehaviorSubject, ReplaySubject, Subject, Subscription } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
import Selecto from 'selecto';
|
||||
|
||||
import { AppEvents, PanelData } from '@grafana/data';
|
||||
@ -27,15 +26,9 @@ import {
|
||||
} from 'app/features/dimensions/utils';
|
||||
import { CanvasContextMenu } from 'app/plugins/panel/canvas/components/CanvasContextMenu';
|
||||
import { CanvasTooltip } from 'app/plugins/panel/canvas/components/CanvasTooltip';
|
||||
import { CONNECTION_ANCHOR_DIV_ID } from 'app/plugins/panel/canvas/components/connections/ConnectionAnchors';
|
||||
import {
|
||||
Connections,
|
||||
CONNECTION_VERTEX_ADD_ID,
|
||||
CONNECTION_VERTEX_ID,
|
||||
} from 'app/plugins/panel/canvas/components/connections/Connections';
|
||||
import { HorizontalConstraint, Placement, VerticalConstraint } from 'app/plugins/panel/canvas/panelcfg.gen';
|
||||
import { AnchorPoint, CanvasTooltipPayload, LayerActionID } from 'app/plugins/panel/canvas/types';
|
||||
import { getParent, getTransformInstance } from 'app/plugins/panel/canvas/utils';
|
||||
import { Connections } from 'app/plugins/panel/canvas/components/connections/Connections';
|
||||
import { AnchorPoint, CanvasTooltipPayload } from 'app/plugins/panel/canvas/types';
|
||||
import { getTransformInstance } from 'app/plugins/panel/canvas/utils';
|
||||
|
||||
import appEvents from '../../../core/app_events';
|
||||
import { CanvasPanel } from '../../../plugins/panel/canvas/CanvasPanel';
|
||||
@ -43,10 +36,11 @@ import { CanvasFrameOptions } from '../frame';
|
||||
import { DEFAULT_CANVAS_ELEMENT_CONFIG } from '../registry';
|
||||
|
||||
import { SceneTransformWrapper } from './SceneTransformWrapper';
|
||||
import { constraintViewable, dimensionViewable, settingsViewable } from './ables';
|
||||
import { ElementState } from './element';
|
||||
import { FrameState } from './frame';
|
||||
import { RootElement } from './root';
|
||||
import { initMoveable } from './sceneAbleManagement';
|
||||
import { findElementByTarget } from './sceneElementManagement';
|
||||
|
||||
export interface SelectionParams {
|
||||
targets: Array<HTMLElement | SVGElement>;
|
||||
@ -176,7 +170,7 @@ export class Scene {
|
||||
if (this.div) {
|
||||
// If editing is enabled, clear selecto instance
|
||||
const destroySelecto = enableEditing;
|
||||
this.initMoveable(destroySelecto, enableEditing);
|
||||
initMoveable(destroySelecto, enableEditing, this);
|
||||
this.currentLayer = this.root;
|
||||
this.selection.next([]);
|
||||
this.connections.select(undefined);
|
||||
@ -210,126 +204,24 @@ export class Scene {
|
||||
}
|
||||
}
|
||||
|
||||
frameSelection() {
|
||||
this.selection.pipe(first()).subscribe((currentSelectedElements) => {
|
||||
const currentLayer = currentSelectedElements[0].parent!;
|
||||
|
||||
const newLayer = new FrameState(
|
||||
{
|
||||
type: 'frame',
|
||||
name: this.getNextElementName(true),
|
||||
elements: [],
|
||||
},
|
||||
this,
|
||||
currentSelectedElements[0].parent
|
||||
);
|
||||
|
||||
const framePlacement = this.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);
|
||||
|
||||
this.byName.set(newLayer.getName(), newLayer);
|
||||
|
||||
this.save();
|
||||
});
|
||||
}
|
||||
|
||||
private 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,
|
||||
};
|
||||
};
|
||||
|
||||
clearCurrentSelection(skipNextSelectionBroadcast = false) {
|
||||
this.skipNextSelectionBroadcast = skipNextSelectionBroadcast;
|
||||
let event: MouseEvent = new MouseEvent('click');
|
||||
this.selecto?.clickTarget(event, this.div);
|
||||
}
|
||||
|
||||
updateCurrentLayer(newLayer: FrameState) {
|
||||
this.currentLayer = newLayer;
|
||||
this.clearCurrentSelection();
|
||||
this.save();
|
||||
}
|
||||
|
||||
save = (updateMoveable = false) => {
|
||||
this.onSave(this.root.getSaveModel());
|
||||
|
||||
if (updateMoveable) {
|
||||
setTimeout(() => {
|
||||
if (this.div) {
|
||||
this.initMoveable(true, this.isEditingEnabled);
|
||||
initMoveable(true, this.isEditingEnabled, this);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
findElementByTarget = (target: Element): ElementState | undefined => {
|
||||
// We will probably want to add memoization to this as we are calling on drag / resize
|
||||
|
||||
const stack = [...this.root.elements];
|
||||
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;
|
||||
};
|
||||
|
||||
setNonTargetPointerEvents = (target: Element, disablePointerEvents: boolean) => {
|
||||
const stack = [...this.root.elements];
|
||||
while (stack.length > 0) {
|
||||
@ -363,7 +255,7 @@ export class Scene {
|
||||
}
|
||||
};
|
||||
|
||||
private updateSelection = (selection: SelectionParams) => {
|
||||
updateSelection = (selection: SelectionParams) => {
|
||||
this.moveable!.target = selection.targets;
|
||||
if (this.skipNextSelectionBroadcast) {
|
||||
this.skipNextSelectionBroadcast = false;
|
||||
@ -373,431 +265,11 @@ export class Scene {
|
||||
if (selection.frame) {
|
||||
this.selection.next([selection.frame]);
|
||||
} else {
|
||||
const s = selection.targets.map((t) => this.findElementByTarget(t)!);
|
||||
const s = selection.targets.map((t) => findElementByTarget(t, this.root.elements)!);
|
||||
this.selection.next(s);
|
||||
}
|
||||
};
|
||||
|
||||
private generateTargetElements = (rootElements: ElementState[]): HTMLDivElement[] => {
|
||||
let targetElements: HTMLDivElement[] = [];
|
||||
|
||||
const stack = [...rootElements];
|
||||
while (stack.length > 0) {
|
||||
const currentElement = stack.shift();
|
||||
|
||||
if (currentElement && currentElement.div) {
|
||||
targetElements.push(currentElement.div);
|
||||
}
|
||||
|
||||
const nestedElements = currentElement instanceof FrameState ? currentElement.elements : [];
|
||||
for (const nestedElement of nestedElements) {
|
||||
stack.unshift(nestedElement);
|
||||
}
|
||||
}
|
||||
|
||||
return targetElements;
|
||||
};
|
||||
|
||||
disableCustomables = () => {
|
||||
this.moveable!.props = {
|
||||
dimensionViewable: false,
|
||||
constraintViewable: false,
|
||||
settingsViewable: false,
|
||||
};
|
||||
};
|
||||
|
||||
enableCustomables = () => {
|
||||
this.moveable!.props = {
|
||||
dimensionViewable: true,
|
||||
constraintViewable: true,
|
||||
settingsViewable: true,
|
||||
};
|
||||
};
|
||||
|
||||
initMoveable = (destroySelecto = false, allowChanges = true) => {
|
||||
const targetElements = this.generateTargetElements(this.root.elements);
|
||||
|
||||
if (destroySelecto && this.selecto) {
|
||||
this.selecto.destroy();
|
||||
}
|
||||
|
||||
this.selecto = new Selecto({
|
||||
container: this.div,
|
||||
rootContainer: getParent(this),
|
||||
selectableTargets: targetElements,
|
||||
toggleContinueSelect: 'shift',
|
||||
selectFromInside: false,
|
||||
hitRate: 0,
|
||||
});
|
||||
|
||||
const snapDirections = { top: true, left: true, bottom: true, right: true, center: true, middle: true };
|
||||
const elementSnapDirections = { top: true, left: true, bottom: true, right: true, center: true, middle: true };
|
||||
|
||||
this.moveable = new Moveable(this.div!, {
|
||||
draggable: allowChanges && !this.editModeEnabled.getValue(),
|
||||
resizable: allowChanges,
|
||||
|
||||
// Setup rotatable
|
||||
rotatable: allowChanges,
|
||||
throttleRotate: 5,
|
||||
rotationPosition: ['top', 'right'],
|
||||
|
||||
// Setup snappable
|
||||
snappable: allowChanges,
|
||||
snapDirections: snapDirections,
|
||||
elementSnapDirections: elementSnapDirections,
|
||||
elementGuidelines: targetElements,
|
||||
|
||||
ables: [dimensionViewable, constraintViewable(this), settingsViewable(this)],
|
||||
props: {
|
||||
dimensionViewable: allowChanges,
|
||||
constraintViewable: allowChanges,
|
||||
settingsViewable: allowChanges,
|
||||
},
|
||||
origin: false,
|
||||
})
|
||||
.on('rotateStart', () => {
|
||||
this.disableCustomables();
|
||||
})
|
||||
.on('rotate', (event) => {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
|
||||
if (targetedElement) {
|
||||
targetedElement.applyRotate(event);
|
||||
}
|
||||
})
|
||||
.on('rotateGroup', (e) => {
|
||||
for (let event of e.events) {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
if (targetedElement) {
|
||||
targetedElement.applyRotate(event);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('rotateEnd', () => {
|
||||
this.enableCustomables();
|
||||
// Update the editor with the new rotation
|
||||
this.moved.next(Date.now());
|
||||
})
|
||||
.on('click', (event) => {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
let elementSupportsEditing = false;
|
||||
if (targetedElement) {
|
||||
elementSupportsEditing = targetedElement.item.hasEditMode ?? false;
|
||||
}
|
||||
|
||||
if (event.isDouble && allowChanges && !this.editModeEnabled.getValue() && elementSupportsEditing) {
|
||||
this.editModeEnabled.next(true);
|
||||
}
|
||||
})
|
||||
.on('clickGroup', (event) => {
|
||||
this.selecto!.clickTarget(event.inputEvent, event.inputTarget);
|
||||
})
|
||||
.on('dragStart', (event) => {
|
||||
this.ignoreDataUpdate = true;
|
||||
this.setNonTargetPointerEvents(event.target, true);
|
||||
|
||||
// Remove the selected element from the snappable guidelines
|
||||
if (this.moveable && this.moveable.elementGuidelines) {
|
||||
const targetIndex = this.moveable.elementGuidelines.indexOf(event.target);
|
||||
if (targetIndex > -1) {
|
||||
this.moveable.elementGuidelines.splice(targetIndex, 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('dragGroupStart', (e) => {
|
||||
this.ignoreDataUpdate = true;
|
||||
|
||||
// Remove the selected elements from the snappable guidelines
|
||||
if (this.moveable && this.moveable.elementGuidelines) {
|
||||
for (let event of e.events) {
|
||||
const targetIndex = this.moveable.elementGuidelines.indexOf(event.target);
|
||||
if (targetIndex > -1) {
|
||||
this.moveable.elementGuidelines.splice(targetIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('drag', (event) => {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
if (targetedElement) {
|
||||
targetedElement.applyDrag(event);
|
||||
|
||||
if (this.connections.connectionsNeedUpdate(targetedElement) && this.moveableActionCallback) {
|
||||
this.moveableActionCallback(true);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('dragGroup', (e) => {
|
||||
let needsUpdate = false;
|
||||
for (let event of e.events) {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
if (targetedElement) {
|
||||
targetedElement.applyDrag(event);
|
||||
if (!needsUpdate) {
|
||||
needsUpdate = this.connections.connectionsNeedUpdate(targetedElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate && this.moveableActionCallback) {
|
||||
this.moveableActionCallback(true);
|
||||
}
|
||||
})
|
||||
.on('dragGroupEnd', (e) => {
|
||||
e.events.forEach((event) => {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
if (targetedElement) {
|
||||
if (targetedElement) {
|
||||
targetedElement.setPlacementFromConstraint(undefined, undefined, this.scale);
|
||||
}
|
||||
|
||||
// re-add the selected elements to the snappable guidelines
|
||||
if (this.moveable && this.moveable.elementGuidelines) {
|
||||
this.moveable.elementGuidelines.push(event.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.moved.next(Date.now());
|
||||
this.ignoreDataUpdate = false;
|
||||
})
|
||||
.on('dragEnd', (event) => {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
if (targetedElement) {
|
||||
targetedElement.setPlacementFromConstraint(undefined, undefined, this.scale);
|
||||
}
|
||||
|
||||
this.moved.next(Date.now());
|
||||
this.ignoreDataUpdate = false;
|
||||
this.setNonTargetPointerEvents(event.target, false);
|
||||
|
||||
// re-add the selected element to the snappable guidelines
|
||||
if (this.moveable && this.moveable.elementGuidelines) {
|
||||
this.moveable.elementGuidelines.push(event.target);
|
||||
}
|
||||
})
|
||||
.on('resizeStart', (event) => {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
|
||||
if (targetedElement) {
|
||||
// Remove the selected element from the snappable guidelines
|
||||
if (this.moveable && this.moveable.elementGuidelines) {
|
||||
const targetIndex = this.moveable.elementGuidelines.indexOf(event.target);
|
||||
if (targetIndex > -1) {
|
||||
this.moveable.elementGuidelines.splice(targetIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
targetedElement.tempConstraint = { ...targetedElement.options.constraint };
|
||||
targetedElement.options.constraint = {
|
||||
vertical: VerticalConstraint.Top,
|
||||
horizontal: HorizontalConstraint.Left,
|
||||
};
|
||||
targetedElement.setPlacementFromConstraint(undefined, undefined, this.scale);
|
||||
}
|
||||
})
|
||||
.on('resizeGroupStart', (e) => {
|
||||
// Remove the selected elements from the snappable guidelines
|
||||
if (this.moveable && this.moveable.elementGuidelines) {
|
||||
for (let event of e.events) {
|
||||
const targetIndex = this.moveable.elementGuidelines.indexOf(event.target);
|
||||
if (targetIndex > -1) {
|
||||
this.moveable.elementGuidelines.splice(targetIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('resize', (event) => {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
if (targetedElement) {
|
||||
targetedElement.applyResize(event, this.scale);
|
||||
|
||||
if (this.connections.connectionsNeedUpdate(targetedElement) && this.moveableActionCallback) {
|
||||
this.moveableActionCallback(true);
|
||||
}
|
||||
}
|
||||
this.moved.next(Date.now()); // TODO only on end
|
||||
})
|
||||
.on('resizeGroup', (e) => {
|
||||
let needsUpdate = false;
|
||||
for (let event of e.events) {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
if (targetedElement) {
|
||||
targetedElement.applyResize(event);
|
||||
|
||||
if (!needsUpdate) {
|
||||
needsUpdate = this.connections.connectionsNeedUpdate(targetedElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate && this.moveableActionCallback) {
|
||||
this.moveableActionCallback(true);
|
||||
}
|
||||
|
||||
this.moved.next(Date.now()); // TODO only on end
|
||||
})
|
||||
.on('resizeEnd', (event) => {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
|
||||
if (targetedElement) {
|
||||
if (targetedElement.tempConstraint) {
|
||||
targetedElement.options.constraint = targetedElement.tempConstraint;
|
||||
targetedElement.tempConstraint = undefined;
|
||||
}
|
||||
|
||||
targetedElement.setPlacementFromConstraint(undefined, undefined, this.scale);
|
||||
|
||||
// re-add the selected element to the snappable guidelines
|
||||
if (this.moveable && this.moveable.elementGuidelines) {
|
||||
this.moveable.elementGuidelines.push(event.target);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('resizeGroupEnd', (e) => {
|
||||
// re-add the selected elements to the snappable guidelines
|
||||
if (this.moveable && this.moveable.elementGuidelines) {
|
||||
for (let event of e.events) {
|
||||
this.moveable.elementGuidelines.push(event.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let targets: Array<HTMLElement | SVGElement> = [];
|
||||
this.selecto!.on('dragStart', (event) => {
|
||||
const selectedTarget = event.inputEvent.target;
|
||||
|
||||
// If selected target is a connection control, eject to handle connection event
|
||||
if (selectedTarget.id === CONNECTION_ANCHOR_DIV_ID) {
|
||||
this.connections.handleConnectionDragStart(selectedTarget, event.inputEvent.clientX, event.inputEvent.clientY);
|
||||
event.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// If selected target is a vertex, eject to handle vertex event
|
||||
if (selectedTarget.id === CONNECTION_VERTEX_ID) {
|
||||
this.connections.handleVertexDragStart(selectedTarget);
|
||||
event.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// If selected target is an add vertex point, eject to handle add vertex event
|
||||
if (selectedTarget.id === CONNECTION_VERTEX_ADD_ID) {
|
||||
this.connections.handleVertexAddDragStart(selectedTarget);
|
||||
event.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const isTargetMoveableElement =
|
||||
this.moveable!.isMoveableElement(selectedTarget) ||
|
||||
targets.some((target) => target === selectedTarget || target.contains(selectedTarget));
|
||||
|
||||
const isTargetAlreadySelected = this.selecto
|
||||
?.getSelectedTargets()
|
||||
.includes(selectedTarget.parentElement.parentElement);
|
||||
|
||||
// Apply grabbing cursor while dragging, applyLayoutStylesToDiv() resets it to grab when done
|
||||
if (
|
||||
this.isEditingEnabled &&
|
||||
!this.editModeEnabled.getValue() &&
|
||||
isTargetMoveableElement &&
|
||||
this.selecto?.getSelectedTargets().length
|
||||
) {
|
||||
this.selecto.getSelectedTargets()[0].style.cursor = 'grabbing';
|
||||
}
|
||||
|
||||
if (isTargetMoveableElement || isTargetAlreadySelected || !this.isEditingEnabled) {
|
||||
// Prevent drawing selection box when selected target is a moveable element or already selected
|
||||
event.stop();
|
||||
}
|
||||
})
|
||||
.on('select', () => {
|
||||
this.editModeEnabled.next(false);
|
||||
|
||||
// Hide connection anchors on select
|
||||
if (this.connections.connectionAnchorDiv) {
|
||||
this.connections.connectionAnchorDiv.style.display = 'none';
|
||||
}
|
||||
})
|
||||
.on('selectEnd', (event) => {
|
||||
targets = event.selected;
|
||||
this.updateSelection({ targets });
|
||||
|
||||
if (event.isDragStart) {
|
||||
if (this.isEditingEnabled && !this.editModeEnabled.getValue() && this.selecto?.getSelectedTargets().length) {
|
||||
this.selecto.getSelectedTargets()[0].style.cursor = 'grabbing';
|
||||
}
|
||||
event.inputEvent.preventDefault();
|
||||
event.data.timer = setTimeout(() => {
|
||||
this.moveable!.dragStart(event.inputEvent);
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('dragEnd', (event) => {
|
||||
clearTimeout(event.data.timer);
|
||||
});
|
||||
};
|
||||
|
||||
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) {
|
||||
this.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) {
|
||||
this.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 {
|
||||
this.updateElements(src, dest);
|
||||
src.updateData(dest.scene.context);
|
||||
}
|
||||
} else if (src.parent === dest.parent) {
|
||||
src.parent?.reorderTree(src, dest);
|
||||
} else {
|
||||
if (dest.parent) {
|
||||
this.updateElements(src, dest.parent);
|
||||
src.updateData(dest.parent.scene.context);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private 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();
|
||||
};
|
||||
|
||||
addToSelection = () => {
|
||||
try {
|
||||
let selection: SelectionParams = { targets: [] };
|
||||
|
382
public/app/features/canvas/runtime/sceneAbleManagement.ts
Normal file
382
public/app/features/canvas/runtime/sceneAbleManagement.ts
Normal file
@ -0,0 +1,382 @@
|
||||
import Moveable from 'moveable';
|
||||
import Selecto from 'selecto';
|
||||
|
||||
import { CONNECTION_ANCHOR_DIV_ID } from 'app/plugins/panel/canvas/components/connections/ConnectionAnchors';
|
||||
import {
|
||||
CONNECTION_VERTEX_ID,
|
||||
CONNECTION_VERTEX_ADD_ID,
|
||||
} from 'app/plugins/panel/canvas/components/connections/Connections';
|
||||
import { VerticalConstraint, HorizontalConstraint } from 'app/plugins/panel/canvas/panelcfg.gen';
|
||||
import { getParent } from 'app/plugins/panel/canvas/utils';
|
||||
|
||||
import { dimensionViewable, constraintViewable, settingsViewable } from './ables';
|
||||
import { ElementState } from './element';
|
||||
import { FrameState } from './frame';
|
||||
import { Scene } from './scene';
|
||||
import { findElementByTarget } from './sceneElementManagement';
|
||||
|
||||
// Helper function that disables custom able functionality
|
||||
const disableCustomables = (moveable: Moveable) => {
|
||||
moveable!.props = {
|
||||
dimensionViewable: false,
|
||||
constraintViewable: false,
|
||||
settingsViewable: false,
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function that enables custom able functionality
|
||||
const enableCustomables = (moveable: Moveable) => {
|
||||
moveable!.props = {
|
||||
dimensionViewable: true,
|
||||
constraintViewable: true,
|
||||
settingsViewable: true,
|
||||
};
|
||||
};
|
||||
|
||||
// Generate HTML element divs for every canvas element to configure selecto / moveable
|
||||
const generateTargetElements = (rootElements: ElementState[]): HTMLDivElement[] => {
|
||||
let targetElements: HTMLDivElement[] = [];
|
||||
|
||||
const stack = [...rootElements];
|
||||
while (stack.length > 0) {
|
||||
const currentElement = stack.shift();
|
||||
|
||||
if (currentElement && currentElement.div) {
|
||||
targetElements.push(currentElement.div);
|
||||
}
|
||||
|
||||
const nestedElements = currentElement instanceof FrameState ? currentElement.elements : [];
|
||||
for (const nestedElement of nestedElements) {
|
||||
stack.unshift(nestedElement);
|
||||
}
|
||||
}
|
||||
|
||||
return targetElements;
|
||||
};
|
||||
|
||||
// Main entry point for initializing / updating moveable and selecto configuration
|
||||
export const initMoveable = (destroySelecto = false, allowChanges = true, scene: Scene) => {
|
||||
const targetElements = generateTargetElements(scene.root.elements);
|
||||
|
||||
if (destroySelecto && scene.selecto) {
|
||||
scene.selecto.destroy();
|
||||
}
|
||||
|
||||
scene.selecto = new Selecto({
|
||||
container: scene.div,
|
||||
rootContainer: getParent(scene),
|
||||
selectableTargets: targetElements,
|
||||
toggleContinueSelect: 'shift',
|
||||
selectFromInside: false,
|
||||
hitRate: 0,
|
||||
});
|
||||
|
||||
const snapDirections = { top: true, left: true, bottom: true, right: true, center: true, middle: true };
|
||||
const elementSnapDirections = { top: true, left: true, bottom: true, right: true, center: true, middle: true };
|
||||
|
||||
scene.moveable = new Moveable(scene.div!, {
|
||||
draggable: allowChanges && !scene.editModeEnabled.getValue(),
|
||||
resizable: allowChanges,
|
||||
|
||||
// Setup rotatable
|
||||
rotatable: allowChanges,
|
||||
throttleRotate: 5,
|
||||
rotationPosition: ['top', 'right'],
|
||||
|
||||
// Setup snappable
|
||||
snappable: allowChanges,
|
||||
snapDirections: snapDirections,
|
||||
elementSnapDirections: elementSnapDirections,
|
||||
elementGuidelines: targetElements,
|
||||
|
||||
ables: [dimensionViewable, constraintViewable(scene), settingsViewable(scene)],
|
||||
props: {
|
||||
dimensionViewable: allowChanges,
|
||||
constraintViewable: allowChanges,
|
||||
settingsViewable: allowChanges,
|
||||
},
|
||||
origin: false,
|
||||
})
|
||||
.on('rotateStart', () => {
|
||||
disableCustomables(scene.moveable!);
|
||||
})
|
||||
.on('rotate', (event) => {
|
||||
const targetedElement = findElementByTarget(event.target, scene.root.elements);
|
||||
|
||||
if (targetedElement) {
|
||||
targetedElement.applyRotate(event);
|
||||
}
|
||||
})
|
||||
.on('rotateGroup', (e) => {
|
||||
for (let event of e.events) {
|
||||
const targetedElement = findElementByTarget(event.target, scene.root.elements);
|
||||
if (targetedElement) {
|
||||
targetedElement.applyRotate(event);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('rotateEnd', () => {
|
||||
enableCustomables(scene.moveable!);
|
||||
// Update the editor with the new rotation
|
||||
scene.moved.next(Date.now());
|
||||
})
|
||||
.on('click', (event) => {
|
||||
const targetedElement = findElementByTarget(event.target, scene.root.elements);
|
||||
let elementSupportsEditing = false;
|
||||
if (targetedElement) {
|
||||
elementSupportsEditing = targetedElement.item.hasEditMode ?? false;
|
||||
}
|
||||
|
||||
if (event.isDouble && allowChanges && !scene.editModeEnabled.getValue() && elementSupportsEditing) {
|
||||
scene.editModeEnabled.next(true);
|
||||
}
|
||||
})
|
||||
.on('clickGroup', (event) => {
|
||||
scene.selecto!.clickTarget(event.inputEvent, event.inputTarget);
|
||||
})
|
||||
.on('dragStart', (event) => {
|
||||
scene.ignoreDataUpdate = true;
|
||||
scene.setNonTargetPointerEvents(event.target, true);
|
||||
|
||||
// Remove the selected element from the snappable guidelines
|
||||
if (scene.moveable && scene.moveable.elementGuidelines) {
|
||||
const targetIndex = scene.moveable.elementGuidelines.indexOf(event.target);
|
||||
if (targetIndex > -1) {
|
||||
scene.moveable.elementGuidelines.splice(targetIndex, 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('dragGroupStart', (e) => {
|
||||
scene.ignoreDataUpdate = true;
|
||||
|
||||
// Remove the selected elements from the snappable guidelines
|
||||
if (scene.moveable && scene.moveable.elementGuidelines) {
|
||||
for (let event of e.events) {
|
||||
const targetIndex = scene.moveable.elementGuidelines.indexOf(event.target);
|
||||
if (targetIndex > -1) {
|
||||
scene.moveable.elementGuidelines.splice(targetIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('drag', (event) => {
|
||||
const targetedElement = findElementByTarget(event.target, scene.root.elements);
|
||||
if (targetedElement) {
|
||||
targetedElement.applyDrag(event);
|
||||
|
||||
if (scene.connections.connectionsNeedUpdate(targetedElement) && scene.moveableActionCallback) {
|
||||
scene.moveableActionCallback(true);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('dragGroup', (e) => {
|
||||
let needsUpdate = false;
|
||||
for (let event of e.events) {
|
||||
const targetedElement = findElementByTarget(event.target, scene.root.elements);
|
||||
if (targetedElement) {
|
||||
targetedElement.applyDrag(event);
|
||||
if (!needsUpdate) {
|
||||
needsUpdate = scene.connections.connectionsNeedUpdate(targetedElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate && scene.moveableActionCallback) {
|
||||
scene.moveableActionCallback(true);
|
||||
}
|
||||
})
|
||||
.on('dragGroupEnd', (e) => {
|
||||
e.events.forEach((event) => {
|
||||
const targetedElement = findElementByTarget(event.target, scene.root.elements);
|
||||
if (targetedElement) {
|
||||
if (targetedElement) {
|
||||
targetedElement.setPlacementFromConstraint(undefined, undefined, scene.scale);
|
||||
}
|
||||
|
||||
// re-add the selected elements to the snappable guidelines
|
||||
if (scene.moveable && scene.moveable.elementGuidelines) {
|
||||
scene.moveable.elementGuidelines.push(event.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scene.moved.next(Date.now());
|
||||
scene.ignoreDataUpdate = false;
|
||||
})
|
||||
.on('dragEnd', (event) => {
|
||||
const targetedElement = findElementByTarget(event.target, scene.root.elements);
|
||||
if (targetedElement) {
|
||||
targetedElement.setPlacementFromConstraint(undefined, undefined, scene.scale);
|
||||
}
|
||||
|
||||
scene.moved.next(Date.now());
|
||||
scene.ignoreDataUpdate = false;
|
||||
scene.setNonTargetPointerEvents(event.target, false);
|
||||
|
||||
// re-add the selected element to the snappable guidelines
|
||||
if (scene.moveable && scene.moveable.elementGuidelines) {
|
||||
scene.moveable.elementGuidelines.push(event.target);
|
||||
}
|
||||
})
|
||||
.on('resizeStart', (event) => {
|
||||
const targetedElement = findElementByTarget(event.target, scene.root.elements);
|
||||
|
||||
if (targetedElement) {
|
||||
// Remove the selected element from the snappable guidelines
|
||||
if (scene.moveable && scene.moveable.elementGuidelines) {
|
||||
const targetIndex = scene.moveable.elementGuidelines.indexOf(event.target);
|
||||
if (targetIndex > -1) {
|
||||
scene.moveable.elementGuidelines.splice(targetIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
targetedElement.tempConstraint = { ...targetedElement.options.constraint };
|
||||
targetedElement.options.constraint = {
|
||||
vertical: VerticalConstraint.Top,
|
||||
horizontal: HorizontalConstraint.Left,
|
||||
};
|
||||
targetedElement.setPlacementFromConstraint(undefined, undefined, scene.scale);
|
||||
}
|
||||
})
|
||||
.on('resizeGroupStart', (e) => {
|
||||
// Remove the selected elements from the snappable guidelines
|
||||
if (scene.moveable && scene.moveable.elementGuidelines) {
|
||||
for (let event of e.events) {
|
||||
const targetIndex = scene.moveable.elementGuidelines.indexOf(event.target);
|
||||
if (targetIndex > -1) {
|
||||
scene.moveable.elementGuidelines.splice(targetIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('resize', (event) => {
|
||||
const targetedElement = findElementByTarget(event.target, scene.root.elements);
|
||||
if (targetedElement) {
|
||||
targetedElement.applyResize(event, scene.scale);
|
||||
|
||||
if (scene.connections.connectionsNeedUpdate(targetedElement) && scene.moveableActionCallback) {
|
||||
scene.moveableActionCallback(true);
|
||||
}
|
||||
}
|
||||
scene.moved.next(Date.now()); // TODO only on end
|
||||
})
|
||||
.on('resizeGroup', (e) => {
|
||||
let needsUpdate = false;
|
||||
for (let event of e.events) {
|
||||
const targetedElement = findElementByTarget(event.target, scene.root.elements);
|
||||
if (targetedElement) {
|
||||
targetedElement.applyResize(event);
|
||||
|
||||
if (!needsUpdate) {
|
||||
needsUpdate = scene.connections.connectionsNeedUpdate(targetedElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate && scene.moveableActionCallback) {
|
||||
scene.moveableActionCallback(true);
|
||||
}
|
||||
|
||||
scene.moved.next(Date.now()); // TODO only on end
|
||||
})
|
||||
.on('resizeEnd', (event) => {
|
||||
const targetedElement = findElementByTarget(event.target, scene.root.elements);
|
||||
|
||||
if (targetedElement) {
|
||||
if (targetedElement.tempConstraint) {
|
||||
targetedElement.options.constraint = targetedElement.tempConstraint;
|
||||
targetedElement.tempConstraint = undefined;
|
||||
}
|
||||
|
||||
targetedElement.setPlacementFromConstraint(undefined, undefined, scene.scale);
|
||||
|
||||
// re-add the selected element to the snappable guidelines
|
||||
if (scene.moveable && scene.moveable.elementGuidelines) {
|
||||
scene.moveable.elementGuidelines.push(event.target);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('resizeGroupEnd', (e) => {
|
||||
// re-add the selected elements to the snappable guidelines
|
||||
if (scene.moveable && scene.moveable.elementGuidelines) {
|
||||
for (let event of e.events) {
|
||||
scene.moveable.elementGuidelines.push(event.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let targets: Array<HTMLElement | SVGElement> = [];
|
||||
scene
|
||||
.selecto!.on('dragStart', (event) => {
|
||||
const selectedTarget = event.inputEvent.target;
|
||||
|
||||
// If selected target is a connection control, eject to handle connection event
|
||||
if (selectedTarget.id === CONNECTION_ANCHOR_DIV_ID) {
|
||||
scene.connections.handleConnectionDragStart(selectedTarget, event.inputEvent.clientX, event.inputEvent.clientY);
|
||||
event.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// If selected target is a vertex, eject to handle vertex event
|
||||
if (selectedTarget.id === CONNECTION_VERTEX_ID) {
|
||||
scene.connections.handleVertexDragStart(selectedTarget);
|
||||
event.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// If selected target is an add vertex point, eject to handle add vertex event
|
||||
if (selectedTarget.id === CONNECTION_VERTEX_ADD_ID) {
|
||||
scene.connections.handleVertexAddDragStart(selectedTarget);
|
||||
event.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const isTargetMoveableElement =
|
||||
scene.moveable!.isMoveableElement(selectedTarget) ||
|
||||
targets.some((target) => target === selectedTarget || target.contains(selectedTarget));
|
||||
|
||||
const isTargetAlreadySelected = scene.selecto
|
||||
?.getSelectedTargets()
|
||||
.includes(selectedTarget.parentElement.parentElement);
|
||||
|
||||
// Apply grabbing cursor while dragging, applyLayoutStylesToDiv() resets it to grab when done
|
||||
if (
|
||||
scene.isEditingEnabled &&
|
||||
!scene.editModeEnabled.getValue() &&
|
||||
isTargetMoveableElement &&
|
||||
scene.selecto?.getSelectedTargets().length
|
||||
) {
|
||||
scene.selecto.getSelectedTargets()[0].style.cursor = 'grabbing';
|
||||
}
|
||||
|
||||
if (isTargetMoveableElement || isTargetAlreadySelected || !scene.isEditingEnabled) {
|
||||
// Prevent drawing selection box when selected target is a moveable element or already selected
|
||||
event.stop();
|
||||
}
|
||||
})
|
||||
.on('select', () => {
|
||||
scene.editModeEnabled.next(false);
|
||||
|
||||
// Hide connection anchors on select
|
||||
if (scene.connections.connectionAnchorDiv) {
|
||||
scene.connections.connectionAnchorDiv.style.display = 'none';
|
||||
}
|
||||
})
|
||||
.on('selectEnd', (event) => {
|
||||
targets = event.selected;
|
||||
scene.updateSelection({ targets });
|
||||
|
||||
if (event.isDragStart) {
|
||||
if (scene.isEditingEnabled && !scene.editModeEnabled.getValue() && scene.selecto?.getSelectedTargets().length) {
|
||||
scene.selecto.getSelectedTargets()[0].style.cursor = 'grabbing';
|
||||
}
|
||||
event.inputEvent.preventDefault();
|
||||
event.data.timer = setTimeout(() => {
|
||||
scene.moveable!.dragStart(event.inputEvent);
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('dragEnd', (event) => {
|
||||
clearTimeout(event.data.timer);
|
||||
});
|
||||
};
|
170
public/app/features/canvas/runtime/sceneElementManagement.ts
Normal file
170
public/app/features/canvas/runtime/sceneElementManagement.ts
Normal file
@ -0,0 +1,170 @@
|
||||
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,
|
||||
};
|
||||
};
|
@ -7,6 +7,7 @@ import { ContextMenu, MenuItem, MenuItemProps } from '@grafana/ui';
|
||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||
import { FrameState } from 'app/features/canvas/runtime/frame';
|
||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||
import { findElementByTarget } from 'app/features/canvas/runtime/sceneElementManagement';
|
||||
|
||||
import { CanvasPanel } from '../CanvasPanel';
|
||||
import { AnchorPoint, LayerActionID } from '../types';
|
||||
@ -96,7 +97,7 @@ export const CanvasContextMenu = ({ scene, panel, onVisibilityChange }: Props) =
|
||||
closeContextMenu();
|
||||
};
|
||||
|
||||
const element = scene.findElementByTarget(selectedElements[0]);
|
||||
const element = findElementByTarget(selectedElements[0], scene.root.elements);
|
||||
return (
|
||||
element &&
|
||||
element.item.hasEditMode && (
|
||||
|
@ -5,6 +5,7 @@ import { config } from '@grafana/runtime';
|
||||
import { CanvasConnection, ConnectionCoordinates, ConnectionPath } from 'app/features/canvas/element';
|
||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||
import { findElementByTarget } from 'app/features/canvas/runtime/sceneElementManagement';
|
||||
|
||||
import { ConnectionState } from '../../types';
|
||||
import {
|
||||
@ -111,7 +112,7 @@ export class Connections {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
elementTarget = this.scene.findElementByTarget(element);
|
||||
elementTarget = findElementByTarget(element, this.scene.root.elements);
|
||||
|
||||
if (!elementTarget && element.parentElement) {
|
||||
elementTarget = this.findElementTarget(element.parentElement);
|
||||
|
@ -8,6 +8,7 @@ import { config } from '@grafana/runtime';
|
||||
import { Button, Icon, Stack, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
|
||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||
import { frameSelection, reorderElements } from 'app/features/canvas/runtime/sceneElementManagement';
|
||||
|
||||
import { getGlobalStyles } from '../../globalStyles';
|
||||
import { Options } from '../../panelcfg.gen';
|
||||
@ -77,7 +78,7 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<unknown, Tree
|
||||
const data = onNodeDrop(info, treeData);
|
||||
|
||||
setTreeData(data);
|
||||
destEl.parent?.scene.reorderElements(srcEl, destEl, info.dropToGap, destPosition);
|
||||
reorderElements(srcEl, destEl, info.dropToGap, destPosition);
|
||||
};
|
||||
|
||||
const onExpand = (expandedKeys: Key[]) => {
|
||||
@ -118,7 +119,7 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<unknown, Tree
|
||||
// TODO: This functionality is currently kinda broken / no way to decouple / delete created frames at this time
|
||||
const onFrameSelection = () => {
|
||||
if (layer.scene) {
|
||||
layer.scene.frameSelection();
|
||||
frameSelection(layer.scene);
|
||||
} else {
|
||||
console.warn('no scene!');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user