mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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" >}}
|
{{< 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
|
## Canvas options
|
||||||
|
|
||||||
### Inline editing
|
### Inline editing
|
||||||
|
@ -13,10 +13,37 @@ type SceneTransformWrapperProps = {
|
|||||||
export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransformWrapperProps) => {
|
export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransformWrapperProps) => {
|
||||||
const onZoom = (zoomPanPinchRef: ReactZoomPanPinchRef) => {
|
const onZoom = (zoomPanPinchRef: ReactZoomPanPinchRef) => {
|
||||||
const scale = zoomPanPinchRef.state.scale;
|
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) {
|
if (scene.moveable && scale > 0) {
|
||||||
scene.moveable.zoom = 1 / scale;
|
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>) => {
|
const onSceneContainerMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
@ -38,9 +65,8 @@ export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransf
|
|||||||
doubleClick={{ mode: 'reset' }}
|
doubleClick={{ mode: 'reset' }}
|
||||||
ref={scene.transformComponentRef}
|
ref={scene.transformComponentRef}
|
||||||
onZoom={onZoom}
|
onZoom={onZoom}
|
||||||
onTransformed={(_, state) => {
|
onZoomStop={onZoomStop}
|
||||||
scene.scale = state.scale;
|
onTransformed={onTransformed}
|
||||||
}}
|
|
||||||
limitToBounds={true}
|
limitToBounds={true}
|
||||||
disabled={!config.featureToggles.canvasPanelPanZoom || !scene.shouldPanZoom}
|
disabled={!config.featureToggles.canvasPanelPanZoom || !scene.shouldPanZoom}
|
||||||
panning={{ allowLeftClickPan: false }}
|
panning={{ allowLeftClickPan: false }}
|
||||||
|
@ -397,9 +397,19 @@ export class Scene {
|
|||||||
hitRate: 0,
|
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!, {
|
this.moveable = new Moveable(this.div!, {
|
||||||
draggable: allowChanges && !this.editModeEnabled.getValue(),
|
draggable: allowChanges && !this.editModeEnabled.getValue(),
|
||||||
resizable: allowChanges,
|
resizable: allowChanges,
|
||||||
|
|
||||||
|
// Setup snappable
|
||||||
|
snappable: allowChanges,
|
||||||
|
snapDirections: snapDirections,
|
||||||
|
elementSnapDirections: elementSnapDirections,
|
||||||
|
elementGuidelines: targetElements,
|
||||||
|
|
||||||
ables: [dimensionViewable, constraintViewable(this), settingsViewable(this)],
|
ables: [dimensionViewable, constraintViewable(this), settingsViewable(this)],
|
||||||
props: {
|
props: {
|
||||||
dimensionViewable: allowChanges,
|
dimensionViewable: allowChanges,
|
||||||
@ -426,9 +436,27 @@ export class Scene {
|
|||||||
.on('dragStart', (event) => {
|
.on('dragStart', (event) => {
|
||||||
this.ignoreDataUpdate = true;
|
this.ignoreDataUpdate = true;
|
||||||
this.setNonTargetPointerEvents(event.target, 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;
|
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) => {
|
.on('drag', (event) => {
|
||||||
const targetedElement = this.findElementByTarget(event.target);
|
const targetedElement = this.findElementByTarget(event.target);
|
||||||
@ -463,6 +491,11 @@ export class Scene {
|
|||||||
if (targetedElement) {
|
if (targetedElement) {
|
||||||
targetedElement.setPlacementFromConstraint(undefined, undefined, this.scale);
|
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.moved.next(Date.now());
|
||||||
this.ignoreDataUpdate = false;
|
this.ignoreDataUpdate = false;
|
||||||
this.setNonTargetPointerEvents(event.target, 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) => {
|
.on('resizeStart', (event) => {
|
||||||
const targetedElement = this.findElementByTarget(event.target);
|
const targetedElement = this.findElementByTarget(event.target);
|
||||||
|
|
||||||
if (targetedElement) {
|
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.tempConstraint = { ...targetedElement.options.constraint };
|
||||||
targetedElement.options.constraint = {
|
targetedElement.options.constraint = {
|
||||||
vertical: VerticalConstraint.Top,
|
vertical: VerticalConstraint.Top,
|
||||||
@ -491,6 +537,17 @@ export class Scene {
|
|||||||
targetedElement.setPlacementFromConstraint(undefined, undefined, this.scale);
|
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) => {
|
.on('resize', (event) => {
|
||||||
const targetedElement = this.findElementByTarget(event.target);
|
const targetedElement = this.findElementByTarget(event.target);
|
||||||
if (targetedElement) {
|
if (targetedElement) {
|
||||||
@ -531,6 +588,19 @@ export class Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
targetedElement.setPlacementFromConstraint(undefined, undefined, this.scale);
|
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