mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
Canvas: Add element snapping and alignment (#80407)
Co-authored-by: drew08t <drew08@gmail.com> Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com>
This commit is contained in:
parent
c377644c48
commit
b0130ecb82
@ -107,6 +107,16 @@ When right clicking an element, you are able to edit, delete, duplicate, and mod
|
||||
|
||||
{{< figure src="/static/img/docs/canvas-panel/canvas-context-menu-9-2-0.png" max-width="750px" caption="Canvas element context menu" >}}
|
||||
|
||||
### Element snapping and alignment
|
||||
|
||||
When you're moving elements around the canvas, snapping and alignment guides help you create more precise layouts.
|
||||
|
||||
{{% admonition type="note" %}}
|
||||
Currently, element snapping and alignment only works when the canvas is not zoomed in.
|
||||
{{% /admonition %}}
|
||||
|
||||
<!-- TODO: Add gif showcasing feature (when creating what's new entry for 10.4) -->
|
||||
|
||||
## Canvas options
|
||||
|
||||
### Inline editing
|
||||
|
@ -13,10 +13,37 @@ type SceneTransformWrapperProps = {
|
||||
export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransformWrapperProps) => {
|
||||
const onZoom = (zoomPanPinchRef: ReactZoomPanPinchRef) => {
|
||||
const scale = zoomPanPinchRef.state.scale;
|
||||
scene.scale = scale;
|
||||
};
|
||||
|
||||
const onZoomStop = (zoomPanPinchRef: ReactZoomPanPinchRef) => {
|
||||
const scale = zoomPanPinchRef.state.scale;
|
||||
scene.scale = scale;
|
||||
updateMoveable(scale);
|
||||
};
|
||||
|
||||
const onTransformed = (
|
||||
_: ReactZoomPanPinchRef,
|
||||
state: {
|
||||
scale: number;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
}
|
||||
) => {
|
||||
const scale = state.scale;
|
||||
scene.scale = scale;
|
||||
updateMoveable(scale);
|
||||
};
|
||||
|
||||
const updateMoveable = (scale: number) => {
|
||||
if (scene.moveable && scale > 0) {
|
||||
scene.moveable.zoom = 1 / scale;
|
||||
if (scale === 1) {
|
||||
scene.moveable.snappable = true;
|
||||
} else {
|
||||
scene.moveable.snappable = false;
|
||||
}
|
||||
}
|
||||
scene.scale = scale;
|
||||
};
|
||||
|
||||
const onSceneContainerMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
@ -38,9 +65,8 @@ export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransf
|
||||
doubleClick={{ mode: 'reset' }}
|
||||
ref={scene.transformComponentRef}
|
||||
onZoom={onZoom}
|
||||
onTransformed={(_, state) => {
|
||||
scene.scale = state.scale;
|
||||
}}
|
||||
onZoomStop={onZoomStop}
|
||||
onTransformed={onTransformed}
|
||||
limitToBounds={true}
|
||||
disabled={!config.featureToggles.canvasPanelPanZoom || !scene.shouldPanZoom}
|
||||
panning={{ allowLeftClickPan: false }}
|
||||
|
@ -397,9 +397,19 @@ export class Scene {
|
||||
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 snappable
|
||||
snappable: allowChanges,
|
||||
snapDirections: snapDirections,
|
||||
elementSnapDirections: elementSnapDirections,
|
||||
elementGuidelines: targetElements,
|
||||
|
||||
ables: [dimensionViewable, constraintViewable(this), settingsViewable(this)],
|
||||
props: {
|
||||
dimensionViewable: allowChanges,
|
||||
@ -426,9 +436,27 @@ export class Scene {
|
||||
.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', (event) => {
|
||||
.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);
|
||||
@ -463,6 +491,11 @@ export class Scene {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -478,11 +511,24 @@ export class Scene {
|
||||
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,
|
||||
@ -491,6 +537,17 @@ export class Scene {
|
||||
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) {
|
||||
@ -531,6 +588,19 @@ export class Scene {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user