Canvas: Ability to move and resize elements freely (#40179)

This commit is contained in:
Nathan Marrs 2021-10-12 10:52:15 -07:00 committed by GitHub
parent 48d73cb148
commit f7576b3cdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 369 additions and 19 deletions

View File

@ -295,6 +295,7 @@
"monaco-promql": "^1.7.2",
"mousetrap": "1.6.5",
"mousetrap-global-bind": "1.1.0",
"moveable": "0.26.0",
"ol": "6.7.0",
"papaparse": "5.3.0",
"pluralize": "^8.0.0",
@ -331,6 +332,7 @@
"rst2html": "github:thoward/rst2html#990cb89f2a300cdd9151790be377c4c0840df809",
"rxjs": "7.3.0",
"search-query-parser": "1.5.4",
"selecto": "1.13.0",
"semver": "^7.1.3",
"slate": "0.47.8",
"slate-plain-serializer": "0.7.10",

View File

@ -1,4 +1,6 @@
import React, { CSSProperties } from 'react';
import { OnDrag, OnResize } from 'react-moveable/declaration/types';
import {
BackgroundImageSize,
CanvasElementItem,
@ -15,7 +17,11 @@ export class ElementState {
readonly UID = counter++;
revId = 0;
style: CSSProperties = {};
sizeStyle: CSSProperties = {};
dataStyle: CSSProperties = {};
// Filled in by ref
div?: HTMLDivElement;
// Calculated
width = 100;
@ -34,10 +40,9 @@ export class ElementState {
this.height = height;
// Update the CSS position
this.style = {
...this.style,
width,
height,
this.sizeStyle = {
...this.options.placement,
position: 'absolute',
};
}
@ -96,18 +101,14 @@ export class ElementState {
}
}
css.width = this.width;
css.height = this.height;
this.style = css;
this.dataStyle = css;
}
/** Recursivly visit all nodes */
/** Recursively visit all nodes */
visit(visitor: (v: ElementState) => void) {
visitor(this);
}
// Something changed
onChange(options: CanvasElementOptions) {
if (this.item.id !== options.type) {
this.item = canvasElementRegistry.getIfExists(options.type) ?? notFoundItem;
@ -126,10 +127,41 @@ export class ElementState {
return { ...this.options };
}
initElement = (target: HTMLDivElement) => {
this.div = target;
let placement = this.options.placement;
if (!placement) {
placement = {
left: 0,
top: 0,
};
this.options.placement = placement;
}
};
applyDrag = (event: OnDrag) => {
const placement = this.options.placement;
placement!.top = event.top;
placement!.left = event.left;
event.target.style.top = `${event.top}px`;
event.target.style.left = `${event.left}px`;
};
applyResize = (event: OnResize) => {
const placement = this.options.placement;
placement!.height = event.height;
placement!.width = event.width;
event.target.style.height = `${event.height}px`;
event.target.style.width = `${event.width}px`;
};
render() {
const { item } = this;
return (
<div key={`${this.UID}/${this.revId}`} style={this.style}>
<div key={`${this.UID}/${this.revId}`} style={{ ...this.sizeStyle, ...this.dataStyle }} ref={this.initElement}>
<item.display config={this.options.config} width={this.width} height={this.height} data={this.data} />
</div>
);

View File

@ -48,6 +48,11 @@ export class GroupState extends ElementState {
for (const elem of this.elements) {
elem.updateSize(this.width, this.height);
}
// The group forced to full width (for now)
this.sizeStyle.width = width;
this.sizeStyle.height = height;
this.sizeStyle.position = 'absolute';
}
updateData(ctx: DimensionContext) {
@ -59,13 +64,13 @@ export class GroupState extends ElementState {
render() {
return (
<div key={`${this.UID}/${this.revId}`} style={this.style}>
<div key={`${this.UID}/${this.revId}`} style={{ ...this.sizeStyle, ...this.dataStyle }}>
{this.elements.map((v) => v.render())}
</div>
);
}
/** Recursivly visit all nodes */
/** Recursively visit all nodes */
visit(visitor: (v: ElementState) => void) {
super.visit(visitor);
for (const e of this.elements) {

View File

@ -1,5 +1,9 @@
import React, { CSSProperties } from 'react';
import { css } from '@emotion/css';
import { ReplaySubject } from 'rxjs';
import Moveable from 'moveable';
import Selecto from 'selecto';
import { config } from 'app/core/config';
import { GrafanaTheme2, PanelData } from '@grafana/data';
import { stylesFactory } from '@grafana/ui';
@ -17,7 +21,6 @@ import {
getResourceDimensionFromData,
getTextDimensionFromData,
} from 'app/features/dimensions/utils';
import { ReplaySubject } from 'rxjs';
import { GroupState } from './group';
import { ElementState } from './element';
@ -93,11 +96,84 @@ export class Scene {
this.onSave(this.root.getSaveModel());
}
private findElementByTarget = (target: HTMLElement | SVGElement): ElementState | undefined => {
return this.root.elements.find((element) => element.div === target);
};
initMoveable = (sceneContainer: HTMLDivElement) => {
const targetElements: HTMLDivElement[] = [];
this.root.elements.forEach((element: ElementState) => {
targetElements.push(element.div!);
});
const selecto = new Selecto({
container: sceneContainer,
selectableTargets: targetElements,
});
const moveable = new Moveable(sceneContainer, {
draggable: true,
resizable: true,
})
.on('clickGroup', (event) => {
selecto.clickTarget(event.inputEvent, event.inputTarget);
})
.on('drag', (event) => {
const targetedElement = this.findElementByTarget(event.target);
targetedElement!.applyDrag(event);
})
.on('dragGroup', (e) => {
e.events.forEach((event) => {
const targetedElement = this.findElementByTarget(event.target);
targetedElement!.applyDrag(event);
});
})
.on('resize', (event) => {
const targetedElement = this.findElementByTarget(event.target);
targetedElement!.applyResize(event);
})
.on('resizeGroup', (e) => {
e.events.forEach((event) => {
const targetedElement = this.findElementByTarget(event.target);
targetedElement!.applyResize(event);
});
});
let targets: Array<HTMLElement | SVGElement> = [];
selecto
.on('dragStart', (event) => {
const selectedTarget = event.inputEvent.target;
const isTargetMoveableElement =
moveable.isMoveableElement(selectedTarget) ||
targets.some((target) => target === selectedTarget || target.contains(selectedTarget));
if (isTargetMoveableElement) {
// Prevent drawing selection box when selected target is a moveable element
event.stop();
}
})
.on('selectEnd', (event) => {
targets = event.selected;
moveable.target = targets;
if (event.isDragStart) {
event.inputEvent.preventDefault();
setTimeout(() => {
moveable.dragStart(event.inputEvent);
});
}
});
};
render() {
return (
<div key={this.revId} className={this.styles.wrap} style={this.style}>
<div>
<div key={this.revId} className={this.styles.wrap} style={this.style} ref={this.initMoveable}>
{this.root.render()}
</div>
</div>
);
}
}

View File

@ -34,7 +34,7 @@ export class CanvasPanel extends Component<Props, State> {
};
// Only the initial options are ever used.
// later changs are all controled by the scene
// later changes are all controlled by the scene
this.scene = new Scene(this.props.options.root, this.onUpdateScene);
this.scene.updateSize(props.width, props.height);
this.scene.updateData(props.data);

View File

@ -5,7 +5,7 @@
"state": "alpha",
"info": {
"description": "Explict element placement",
"description": "Explicit element placement",
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"

235
yarn.lock
View File

@ -1932,6 +1932,13 @@ __metadata:
languageName: node
linkType: hard
"@daybrush/utils@npm:^1.0.0, @daybrush/utils@npm:^1.1.1, @daybrush/utils@npm:^1.3.1, @daybrush/utils@npm:^1.4.0, @daybrush/utils@npm:^1.6.0":
version: 1.6.0
resolution: "@daybrush/utils@npm:1.6.0"
checksum: 2579bc17e0d53ffbcb01c14c1f6baf32294f42b370e597d855e11cc5ab4de975502e91114c3e5bd05f76c8a6f83352c40ba60ac74c2fd1c3a741ce2e8899b137
languageName: node
linkType: hard
"@discoveryjs/json-ext@npm:^0.5.0":
version: 0.5.5
resolution: "@discoveryjs/json-ext@npm:0.5.5"
@ -1939,6 +1946,29 @@ __metadata:
languageName: node
linkType: hard
"@egjs/agent@npm:^2.2.1":
version: 2.3.0
resolution: "@egjs/agent@npm:2.3.0"
checksum: 2506e8feff559ae256d5bf6ad15be5e207eb1b22d0c19aabe8292c41288ad7a0fe0527ca8a4c9227e16381a0e71237c6d061b5337d460a2cfad04859b41b0f20
languageName: node
linkType: hard
"@egjs/children-differ@npm:^1.0.1":
version: 1.0.1
resolution: "@egjs/children-differ@npm:1.0.1"
dependencies:
"@egjs/list-differ": ^1.0.0
checksum: 087f286822a93cc5ac2ba0beb9f332f3828a8870fea0e9d0a177078c96fc265ce74979be827a78dab674a833e3a22d4fe60636112a1aae93252e0a38f58ff754
languageName: node
linkType: hard
"@egjs/list-differ@npm:^1.0.0":
version: 1.0.0
resolution: "@egjs/list-differ@npm:1.0.0"
checksum: d1827d134ddab12a024358367cb8a3b70d2ba0f286e5beb6ce2a6bbd021594f26b02c38b2d8b3d1dfd76bd7d1fcda54de3b40decaa9b4fc939230f6e7d531d0c
languageName: node
linkType: hard
"@emotion/babel-plugin@npm:^11.0.0":
version: 11.3.0
resolution: "@emotion/babel-plugin@npm:11.3.0"
@ -4977,6 +5007,33 @@ __metadata:
languageName: node
linkType: hard
"@scena/dragscroll@npm:^1.0.1, @scena/dragscroll@npm:^1.0.2":
version: 1.0.2
resolution: "@scena/dragscroll@npm:1.0.2"
dependencies:
"@scena/event-emitter": ^1.0.2
checksum: 1680e30014fb768aa39319b0f2a053962a119249ab81a1d831669af7770409553a4c60c9fdb916c04d5174894f91d4f8c7c77d896e647934a7fee106d80d2683
languageName: node
linkType: hard
"@scena/event-emitter@npm:^1.0.2, @scena/event-emitter@npm:^1.0.3, @scena/event-emitter@npm:^1.0.5":
version: 1.0.5
resolution: "@scena/event-emitter@npm:1.0.5"
dependencies:
"@daybrush/utils": ^1.1.1
checksum: 400e0f6ab847aff6c1a6a6b62a0187193bdf9dedbe002ac31764b104bddb3c3bd1631df4252f52aa1677879842e96359aca46e025ed78d53c01facf8c76a5559
languageName: node
linkType: hard
"@scena/matrix@npm:^1.0.0, @scena/matrix@npm:^1.1.1":
version: 1.1.1
resolution: "@scena/matrix@npm:1.1.1"
dependencies:
"@daybrush/utils": ^1.4.0
checksum: e96aeab712e5fac4a437b1c1c2f5b445e47050c8b94dac2354bbece7d298b46bab5ea0049a24b360385f4edef8010538ee8d6f0ead570fbb83fdad9fe7e7b180
languageName: node
linkType: hard
"@sentry/browser@npm:5.25.0":
version: 5.25.0
resolution: "@sentry/browser@npm:5.25.0"
@ -13161,6 +13218,28 @@ __metadata:
languageName: node
linkType: hard
"css-styled@npm:^1.0.0":
version: 1.0.0
resolution: "css-styled@npm:1.0.0"
dependencies:
"@daybrush/utils": ^1.0.0
string-hash: ^1.1.3
peerDependencies:
"@daybrush/utils": ">=1.0.0"
checksum: 3f2f995938f03db4e691d1aa6e17e902eae8e6462da7f5b17964398c44466d7c799060268114b45de67824144498cafaa2f944c12257910a76c848581b969f44
languageName: node
linkType: hard
"css-to-mat@npm:^1.0.3":
version: 1.0.3
resolution: "css-to-mat@npm:1.0.3"
dependencies:
"@daybrush/utils": ^1.3.1
"@scena/matrix": ^1.0.0
checksum: a3fc98bfad2e500187736ad977f8753484c793a0aca453d811e11e778a4e26c320aa6419c1c12d7eb451c975d291d25a38139815a2c4b1a833bf044fc5501a4d
languageName: node
linkType: hard
"css-tree@npm:1.0.0-alpha.37":
version: 1.0.0-alpha.37
resolution: "css-tree@npm:1.0.0-alpha.37"
@ -16785,6 +16864,20 @@ __metadata:
languageName: node
linkType: hard
"framework-utils@npm:^0.3.4":
version: 0.3.4
resolution: "framework-utils@npm:0.3.4"
checksum: 7edc71c02da231ffc0bd3823c7cc590de205f58771f1829b9e4094a5d18cf0b092fc86f6cbf89d3e1549680451000bf43a5ba3eb67c468486fb620f27cf8fe66
languageName: node
linkType: hard
"framework-utils@npm:^1.1.0":
version: 1.1.0
resolution: "framework-utils@npm:1.1.0"
checksum: 01b61ead1756ad9bb0d281d22d35c83ee9df3e5e3d63b2d19eb9ca4ba9e69439b698c623ac8161df3744233331017bfd79d82c44e1e61fbb30f0ea6b48d883f9
languageName: node
linkType: hard
"fresh@npm:0.5.2":
version: 0.5.2
resolution: "fresh@npm:0.5.2"
@ -17099,6 +17192,16 @@ fsevents@~2.1.2:
languageName: node
linkType: hard
"gesto@npm:^1.2.1, gesto@npm:^1.4.0":
version: 1.4.0
resolution: "gesto@npm:1.4.0"
dependencies:
"@daybrush/utils": ^1.0.0
"@scena/event-emitter": ^1.0.2
checksum: 2514adc783f42e3012564618d67eae65fc6557703b559bbf6c223975137fd7956d4aea8bbc4e7d988cc1940f78f9ed1a554403c63c4053f3d113506ab84de2be
languageName: node
linkType: hard
"get-caller-file@npm:^1.0.1":
version: 1.0.3
resolution: "get-caller-file@npm:1.0.3"
@ -17878,6 +17981,7 @@ fsevents@~2.1.2:
monaco-promql: ^1.7.2
mousetrap: 1.6.5
mousetrap-global-bind: 1.1.0
moveable: 0.26.0
mutationobserver-shim: 0.3.3
ngtemplate-loader: 2.1.0
nodemon: 2.0.2
@ -17933,6 +18037,7 @@ fsevents@~2.1.2:
sass-lint: 1.12.1
sass-loader: 12.1.0
search-query-parser: 1.5.4
selecto: 1.13.0
semver: ^7.1.3
sinon: 8.1.1
slate: 0.47.8
@ -21385,6 +21490,24 @@ fsevents@~2.1.2:
languageName: node
linkType: hard
"keycode@npm:^2.2.0":
version: 2.2.0
resolution: "keycode@npm:2.2.0"
checksum: cb91c2940a892f1444a41fc08339b8831445a6b095af9103e3061ea7d4bdbfc420135dcb5d9257020e35c374468bb7d4495ea9fcea54e5760196daff3c874fa4
languageName: node
linkType: hard
"keycon@npm:^1.1.2":
version: 1.1.2
resolution: "keycon@npm:1.1.2"
dependencies:
"@daybrush/utils": ^1.0.0
"@scena/event-emitter": ^1.0.2
keycode: ^2.2.0
checksum: 7be74aaf8360824b4aa7bf7a401fa4bc586522ead95ea04d24857ce3320c56d6e3d9a6bb691c5a575f1381124bcbc6b1e5f22a46241d2e2af324ab7b06305d2f
languageName: node
linkType: hard
"kind-of@npm:^3.0.2, kind-of@npm:^3.0.3, kind-of@npm:^3.2.0":
version: 3.2.2
resolution: "kind-of@npm:3.2.2"
@ -23187,6 +23310,16 @@ fsevents@~2.1.2:
languageName: node
linkType: hard
"moveable@npm:0.26.0":
version: 0.26.0
resolution: "moveable@npm:0.26.0"
dependencies:
"@scena/event-emitter": ^1.0.3
react-compat-moveable: ~0.14.0
checksum: f4ee3b8b5562c60534ee7843125234fd549faf7c3be3e09c56e4ec4c12333f3d6715eed60314843bcf0ebe66585024e0dfe129f8d9848040bcbdc8be2da17de3
languageName: node
linkType: hard
"ms@npm:2.0.0":
version: 2.0.0
resolution: "ms@npm:2.0.0"
@ -24300,6 +24433,15 @@ fsevents@~2.1.2:
languageName: node
linkType: hard
"overlap-area@npm:^1.0.0":
version: 1.0.0
resolution: "overlap-area@npm:1.0.0"
dependencies:
"@daybrush/utils": ^1.3.1
checksum: 37d82ea4ea914f7e27c33cf12e93a99e07103bca4180cbfb835328d3fe33a6f31e9500c644f2638a07f478bebc7a261db6d97187c58d967d4736b60cd31230f0
languageName: node
linkType: hard
"overlayscrollbars@npm:^1.13.1":
version: 1.13.1
resolution: "overlayscrollbars@npm:1.13.1"
@ -27452,6 +27594,46 @@ fsevents@~2.1.2:
languageName: node
linkType: hard
"react-compat-css-styled@npm:^1.0.5":
version: 1.0.6
resolution: "react-compat-css-styled@npm:1.0.6"
dependencies:
"@daybrush/utils": ^1.0.0
css-styled: ^1.0.0
framework-utils: ^0.3.4
react-css-styled: ^1.0.2
react-simple-compat: ^1.1.0
peerDependencies:
framework-utils: ">=0.3.4"
checksum: cb3b9158879fa9ed2d238b1ef3fafd4609414312ad648f71fb8faefb5479ee0633c54835a5a618c4a6467bd8772be2d2d2487c5552c0d65eef8f7b6ac548d9de
languageName: node
linkType: hard
"react-compat-moveable@npm:~0.14.0":
version: 0.14.0
resolution: "react-compat-moveable@npm:0.14.0"
dependencies:
"@daybrush/utils": ^1.6.0
"@scena/dragscroll": ^1.0.1
react-compat-css-styled: ^1.0.5
react-moveable: ~0.29.0
react-simple-compat: ^1.2.1
checksum: 3aa4dbbeaa5c12d974d84c23f0567476cd14346ccc6b06f9b8062cda4c40bbd1f335bf21f5829ebdacf7e9f27803e6e8ffc5f9cbed35cb008049730b27388bc5
languageName: node
linkType: hard
"react-css-styled@npm:^1.0.1, react-css-styled@npm:^1.0.2":
version: 1.0.2
resolution: "react-css-styled@npm:1.0.2"
dependencies:
css-styled: ^1.0.0
framework-utils: ^0.3.4
peerDependencies:
framework-utils: ">=0.3.4"
checksum: a05f3fd46eff98af574f134e58bad0b2b5cafc404878d7142a5870858050169b5148368c9b273f74e25da2f09b79341aecff5a2b7114220a7f5b518755cbe7a1
languageName: node
linkType: hard
"react-custom-scrollbars-2@npm:4.4.0":
version: 4.4.0
resolution: "react-custom-scrollbars-2@npm:4.4.0"
@ -27797,6 +27979,24 @@ fsevents@~2.1.2:
languageName: node
linkType: hard
"react-moveable@npm:~0.29.0":
version: 0.29.0
resolution: "react-moveable@npm:0.29.0"
dependencies:
"@daybrush/utils": ^1.6.0
"@egjs/agent": ^2.2.1
"@egjs/children-differ": ^1.0.1
"@scena/dragscroll": ^1.0.1
"@scena/matrix": ^1.1.1
css-to-mat: ^1.0.3
framework-utils: ^1.1.0
gesto: ^1.2.1
overlap-area: ^1.0.0
react-css-styled: ^1.0.1
checksum: 00c09100017873826f47dab061181529bfa753341cec8e48c809931c5c6d1bbbc6eebf3186f4d7055ad3cf180f93f042b4dc868903984879afeec307af1050ae
languageName: node
linkType: hard
"react-popper-tooltip@npm:^3.1.1":
version: 3.1.1
resolution: "react-popper-tooltip@npm:3.1.1"
@ -27989,6 +28189,16 @@ fsevents@~2.1.2:
languageName: node
linkType: hard
"react-simple-compat@npm:^1.1.0, react-simple-compat@npm:^1.2.1":
version: 1.2.1
resolution: "react-simple-compat@npm:1.2.1"
dependencies:
"@daybrush/utils": ^1.0.0
"@egjs/list-differ": ^1.0.0
checksum: 07a47e750fd0cef65bbdc23007ca7fe1c61283dbd04219a0e46a655f9cb58c13165f1d384c9d4e56efee3a44df111bfb89bf966e1ef8842610e22578359b70c2
languageName: node
linkType: hard
"react-sizeme@npm:^3.0.1":
version: 3.0.2
resolution: "react-sizeme@npm:3.0.2"
@ -29693,6 +29903,24 @@ fsevents@~2.1.2:
languageName: node
linkType: hard
"selecto@npm:1.13.0":
version: 1.13.0
resolution: "selecto@npm:1.13.0"
dependencies:
"@daybrush/utils": ^1.4.0
"@egjs/children-differ": ^1.0.1
"@scena/dragscroll": ^1.0.2
"@scena/event-emitter": ^1.0.5
css-styled: ^1.0.0
css-to-mat: ^1.0.3
framework-utils: ^1.1.0
gesto: ^1.4.0
keycon: ^1.1.2
overlap-area: ^1.0.0
checksum: af8bbf085b6979b48e4bd1282baf31e339f49d719e190e89504cdf4db5e54f62a90ddd766c4635576d428c80857b849888ee0c1c227ff775e9293484153c39d0
languageName: node
linkType: hard
"selfsigned@npm:^1.10.11":
version: 1.10.11
resolution: "selfsigned@npm:1.10.11"
@ -30833,6 +31061,13 @@ fsevents@~2.1.2:
languageName: node
linkType: hard
"string-hash@npm:^1.1.3":
version: 1.1.3
resolution: "string-hash@npm:1.1.3"
checksum: 104b8667a5e0dc71bfcd29fee09cb88c6102e27bfb07c55f95535d90587d016731d52299380052e514266f4028a7a5172e0d9ac58e2f8f5001be61dc77c0754d
languageName: node
linkType: hard
"string-length@npm:^4.0.1":
version: 4.0.2
resolution: "string-length@npm:4.0.2"