Closed asnaseer-resilient closed 3 years ago
Hey @asnaseer-resilient
You can achieve this by customizing the MoveItemsState
, here:
You can add a check like this:
// ...
fire: (event: ActionEvent<React.MouseEvent>) => {
// If the event happened on a <input />, skip moving the node
if (event.event.target.tagName === 'INPUT') {
this.eject();
return;
}
// ...
}
// ...
You can change the condition to whatever you want... for example, you can use:
if (event.event.target.hasAttribute('data-avoid-dragging'))
And add
<input
name="my-input"
+ data-avoid-dragging
/>
This way, you'll be more specific about what DOM elements should activate dragging on the nodes.
Here's an example of how to customize State
on your project:
@renato-bohler Thank you very much yet again for your excellent pointers. I will try this out this week.
Actually I just tried your suggestion and found that I had to do a little bit more to get this to work. This is what I ended up doing:
Customised MoveItemsState with a slight modification to what you suggested by getting it to walk up the DOM tree until it either finds an element with the data-no-drag attribute or it reaches the top of the DOM tree. I did this because all my node form elements are contained in a common form container so I only had to add that attribute to this common form container. Here is a snippet of my code:
...
fire: event => {
const target = event.event.target as HTMLElement | null;
let parentElement = target;
while (parentElement && !parentElement.hasAttribute('data-no-drag')) {
parentElement = parentElement.parentElement;
}
if (parentElement && parentElement.hasAttribute('data-no-drag')) {
this.eject();
return;
}
...
Everything now works just as I wanted.
I really appreciate the time you take to explain things really well.
@asnaseer-resilient could you please give us a little sample code? The examples are lacking user input as showed in your image. I guess #911 would be thankful as well
@asnaseer-resilient could you please give us a little sample code? The examples are lacking user input as showed in your image. I guess #911 would be thankful as well
I placed all the customised components in their own folder in my project - here is the code for each of the customisations that I mentioned in my comments above:
MoveItemsState.ts
import { MouseEvent } from 'react';
import {
Action,
ActionEvent,
AbstractDisplacementState,
AbstractDisplacementStateEvent,
BaseModel,
BasePositionModel,
CanvasEngine,
InputType,
State
} from '@projectstorm/react-canvas-core';
import { Point } from '@projectstorm/geometry';
export class MoveItemsState<E extends CanvasEngine = CanvasEngine> extends AbstractDisplacementState<E> {
initialPositions: {
[id: string]: {
point: Point;
item: BaseModel;
};
} = {};
constructor() {
super({
name: 'move-items'
});
this.registerAction(
new Action({
type: InputType.MOUSE_DOWN,
fire: event => {
const target = event.event.target as HTMLElement | null;
// find the first parent element that is allowed to be dragged (an element can be marked as non-draggable by specifying the "data-no-drag" attribute)
let parentElement = target;
while (parentElement && !parentElement.hasAttribute('data-no-drag')) {
parentElement = parentElement.parentElement;
}
// if we could not find any draggable parent element then reject the drag
if (parentElement && parentElement.hasAttribute('data-no-drag')) {
this.eject();
return;
}
const element = this.engine.getActionEventBus().getModelForEvent(event as ActionEvent<MouseEvent<Element, globalThis.MouseEvent>>);
if (!element) {
return;
}
if (!element.isSelected()) {
this.engine.getModel().clearSelection();
}
element.setSelected(true);
this.engine.repaintCanvas();
}
})
);
}
activated(previous: State) {
super.activated(previous);
this.initialPositions = {};
}
fireMouseMoved(event: AbstractDisplacementStateEvent) {
const items = this.engine.getModel().getSelectedEntities();
const model = this.engine.getModel();
for (const item of items) {
if (item instanceof BasePositionModel) {
if (item.isLocked()) {
continue;
}
if (!this.initialPositions[item.getID()]) {
this.initialPositions[item.getID()] = {
point: item.getPosition(),
item: item
};
}
const pos = this.initialPositions[item.getID()].point;
item.setPosition(model.getGridPosition(pos.x + event.virtualDisplacementX), model.getGridPosition(pos.y + event.virtualDisplacementY));
}
}
this.engine.repaintCanvas();
}
}
DragDiagramItemsState.ts
import { MouseEvent } from 'react';
import * as _ from 'lodash';
import { Action, InputType } from '@projectstorm/react-canvas-core';
import { DiagramEngine, LinkModel, PointModel, PortModel } from '@projectstorm/react-diagrams';
import { MoveItemsState } from './MoveItemsState';
export class DragDiagramItemsState extends MoveItemsState<DiagramEngine> {
constructor() {
super();
this.registerAction(
new Action({
type: InputType.MOUSE_UP,
fire: event => {
const item = this.engine.getMouseElement(event.event as MouseEvent<Element, globalThis.MouseEvent>);
if (item instanceof PortModel) {
_.forEach(this.initialPositions, position => {
if (position.item instanceof PointModel) {
const link = position.item.getParent() as LinkModel;
// only care about the last links
if (link.getLastPoint() !== position.item) {
return;
}
if (link.getSourcePort().canLinkToPort(item)) {
link.setTargetPort(item);
item.reportPosition();
this.engine.repaintCanvas();
}
}
});
}
}
})
);
}
}
DefaultState.ts
import { MouseEvent } from 'react';
import { Action, ActionEvent, DragCanvasState, InputType, SelectingState, State } from '@projectstorm/react-canvas-core';
import { MoveItemsState } from './MoveItemsState';
export class DefaultState extends State {
constructor() {
super({
name: 'default'
});
this.childStates = [new SelectingState()];
// determine what was clicked on
this.registerAction(
new Action({
type: InputType.MOUSE_DOWN,
fire: event => {
const element = this.engine.getActionEventBus().getModelForEvent(event as ActionEvent<MouseEvent<Element, globalThis.MouseEvent>>);
// the canvas was clicked on, transition to the dragging canvas state
if (!element) {
this.transitionWithEvent(new DragCanvasState(), event);
} else {
this.transitionWithEvent(new MoveItemsState(), event);
}
}
})
);
}
}
DefaultDiagramState.ts
import { MouseEvent } from 'react';
import { SelectingState, State, Action, InputType, ActionEvent, DragCanvasState } from '@projectstorm/react-canvas-core';
import { DiagramEngine, PortModel } from '@projectstorm/react-diagrams';
import { DragNewLinkState } from './DragNewLinkState';
import { DragDiagramItemsState } from './DragDiagramItemsState';
export class DefaultDiagramState extends State<DiagramEngine> {
dragCanvas: DragCanvasState;
dragNewLink: DragNewLinkState;
dragItems: DragDiagramItemsState;
constructor() {
super({
name: 'default-diagrams'
});
this.childStates = [new SelectingState()];
this.dragCanvas = new DragCanvasState();
this.dragNewLink = new DragNewLinkState({ allowLooseLinks: false }); // Do not allow dangling links
this.dragItems = new DragDiagramItemsState();
// determine what was clicked on
this.registerAction(
new Action({
type: InputType.MOUSE_DOWN,
fire: event => {
const element = this.engine.getActionEventBus().getModelForEvent(event as ActionEvent<MouseEvent<Element, globalThis.MouseEvent>>);
// the canvas was clicked on, transition to the dragging canvas state
if (!element) {
this.transitionWithEvent(this.dragCanvas, event);
} else {
// initiate dragging a new link
if (element instanceof PortModel) {
this.transitionWithEvent(this.dragNewLink, event);
}
// move the items (and potentially link points)
else {
this.transitionWithEvent(this.dragItems, event);
}
}
}
})
);
// touch drags the canvas
this.registerAction(
new Action({
type: InputType.TOUCH_START,
fire: event => {
this.transitionWithEvent(this.dragCanvas, event);
}
})
);
}
}
I then created a widget to display the nodes. In my case this widget display a single in-port at the top, followed by a header, followed by a form containing user input elements (this has the data-no-drag marker on it), followed by zero or more out-ports. Rough skeleton of this code is:
<div>
<div>...elements to render the single in-port go here...</div>
<div>...elements to render the node header go here...</div>
<div data-no-drag>...elements to render the user input elements go here...</div>
<div>...elements to render zero or more out-ports go here...</div>
</div>
Hope this helps.
Greatly appreciated!
Hi, I am using v6.2.0 of this library and have customised my nodes to allow users to set certain attributes. Here is an image of such a setup:
The white rectangles are customised nodes. The empty circle above a node represents a customised input port and the empty circles below a node represent customised output ports. If the user clicks inside the Admin telephone number field in the above image, then, as soon as they try to select the existing text inside that field (+44 in this case) by pressing the mouse down and then dragging to select the +44, it seems that the library takes over that event and start moving the actual node itself. Is there a simple way to disable this behaviour while the user is in any one of these fields of a custom node?