diff --git a/packages/diagrams-demo-gallery/demos/demo-alternative-linking/DefaultState.ts b/packages/diagrams-demo-gallery/demos/demo-alternative-linking/DefaultState.ts index fe88ef966..024e38276 100644 --- a/packages/diagrams-demo-gallery/demos/demo-alternative-linking/DefaultState.ts +++ b/packages/diagrams-demo-gallery/demos/demo-alternative-linking/DefaultState.ts @@ -1,4 +1,4 @@ -import { MouseEvent } from 'react'; +import { MouseEvent, TouchEvent } from 'react'; import { SelectingState, State, @@ -45,6 +45,28 @@ export class DefaultState extends State { }) ); + this.registerAction( + new Action({ + type: InputType.TOUCH_START, + fire: (event: ActionEvent) => { + const element = this.engine.getActionEventBus().getModelForEvent(event); + + // the canvas was clicked on, transition to the dragging canvas state + if (!element) { + this.transitionWithEvent(this.dragCanvas, event); + } + // initiate dragging a new link + else if (element instanceof PortModel) { + return; + } + // move the items (and potentially link points) + else { + this.transitionWithEvent(this.dragItems, event); + } + } + }) + ); + this.registerAction( new Action({ type: InputType.MOUSE_UP, diff --git a/packages/react-canvas-core/src/CanvasEngine.ts b/packages/react-canvas-core/src/CanvasEngine.ts index 214a15c76..86a64a6dd 100644 --- a/packages/react-canvas-core/src/CanvasEngine.ts +++ b/packages/react-canvas-core/src/CanvasEngine.ts @@ -3,7 +3,7 @@ import { FactoryBank } from './core/FactoryBank'; import { AbstractReactFactory } from './core/AbstractReactFactory'; import { LayerModel } from './entities/layer/LayerModel'; import { BaseListener, BaseObserver } from './core/BaseObserver'; -import { MouseEvent } from 'react'; +import { MouseEvent, TouchEvent } from 'react'; import { BaseModel } from './core-models/BaseModel'; import { Point } from '@projectstorm/geometry'; import { ActionEventBus } from './core-actions/ActionEventBus'; @@ -149,7 +149,7 @@ export class CanvasEngine< return this.canvas; } - getMouseElement(event: MouseEvent): BaseModel { + getMouseElement(event: MouseEvent | TouchEvent): BaseModel { return null; } diff --git a/packages/react-canvas-core/src/core-actions/Action.ts b/packages/react-canvas-core/src/core-actions/Action.ts index 931121f18..56ae3fa68 100644 --- a/packages/react-canvas-core/src/core-actions/Action.ts +++ b/packages/react-canvas-core/src/core-actions/Action.ts @@ -1,4 +1,4 @@ -import { MouseEvent, KeyboardEvent, WheelEvent, SyntheticEvent } from 'react'; +import { MouseEvent, KeyboardEvent, WheelEvent, TouchEvent, SyntheticEvent } from 'react'; import { Toolkit } from '../Toolkit'; import { CanvasEngine } from '../CanvasEngine'; import { BaseModel } from '../core-models/BaseModel'; @@ -9,7 +9,10 @@ export enum InputType { MOUSE_MOVE = 'mouse-move', MOUSE_WHEEL = 'mouse-wheel', KEY_DOWN = 'key-down', - KEY_UP = 'key-up' + KEY_UP = 'key-up', + TOUCH_START = 'touch-start', + TOUCH_MOVE = 'touch-move', + TOUCH_END = 'touch-end' } export interface Mapping { @@ -19,6 +22,9 @@ export interface Mapping { [InputType.MOUSE_WHEEL]: WheelEvent; [InputType.KEY_DOWN]: KeyboardEvent; [InputType.KEY_UP]: KeyboardEvent; + [InputType.TOUCH_START]: TouchEvent; + [InputType.TOUCH_MOVE]: TouchEvent; + [InputType.TOUCH_END]: TouchEvent; } export interface ActionEvent { diff --git a/packages/react-canvas-core/src/core-actions/ActionEventBus.ts b/packages/react-canvas-core/src/core-actions/ActionEventBus.ts index f26022259..868afa941 100644 --- a/packages/react-canvas-core/src/core-actions/ActionEventBus.ts +++ b/packages/react-canvas-core/src/core-actions/ActionEventBus.ts @@ -1,5 +1,5 @@ import { Action, ActionEvent, InputType } from './Action'; -import { KeyboardEvent, MouseEvent } from 'react'; +import { KeyboardEvent, MouseEvent, TouchEvent } from 'react'; import * as _ from 'lodash'; import { CanvasEngine } from '../CanvasEngine'; import { BaseModel } from '../core-models/BaseModel'; @@ -38,7 +38,7 @@ export class ActionEventBus { }); } - getModelForEvent(actionEvent: ActionEvent): BaseModel { + getModelForEvent(actionEvent: ActionEvent): BaseModel { if (actionEvent.model) { return actionEvent.model; } @@ -49,8 +49,12 @@ export class ActionEventBus { const { event } = actionEvent; if (event.type === 'mousedown') { return this.getActionsForType(InputType.MOUSE_DOWN); + } else if (event.type === 'touchstart') { + return this.getActionsForType(InputType.TOUCH_START); } else if (event.type === 'mouseup') { return this.getActionsForType(InputType.MOUSE_UP); + } else if (event.type === 'touchend') { + return this.getActionsForType(InputType.TOUCH_END); } else if (event.type === 'keydown') { // store the recorded key this.keys[(event as KeyboardEvent).key.toLowerCase()] = true; @@ -61,6 +65,8 @@ export class ActionEventBus { return this.getActionsForType(InputType.KEY_UP); } else if (event.type === 'mousemove') { return this.getActionsForType(InputType.MOUSE_MOVE); + } else if (event.type === 'touchmove') { + return this.getActionsForType(InputType.TOUCH_MOVE); } else if (event.type === 'wheel') { return this.getActionsForType(InputType.MOUSE_WHEEL); } diff --git a/packages/react-canvas-core/src/core-state/AbstractDisplacementState.ts b/packages/react-canvas-core/src/core-state/AbstractDisplacementState.ts index 4925a73ee..2c2c0ade5 100644 --- a/packages/react-canvas-core/src/core-state/AbstractDisplacementState.ts +++ b/packages/react-canvas-core/src/core-state/AbstractDisplacementState.ts @@ -7,7 +7,7 @@ export interface AbstractDisplacementStateEvent { displacementY: number; virtualDisplacementX: number; virtualDisplacementY: number; - event: React.MouseEvent; + event: React.MouseEvent | React.TouchEvent; } export abstract class AbstractDisplacementState extends State { @@ -30,6 +30,21 @@ export abstract class AbstractDisplacementState) => { + this.initialX = actionEvent.event.touches[0].clientX; + this.initialY = actionEvent.event.touches[0].clientY; + const rel = this.engine.getRelativePoint( + actionEvent.event.touches[0].clientX, + actionEvent.event.touches[0].clientY + ); + this.initialXRelative = rel.x; + this.initialYRelative = rel.y; + } + }) + ); this.registerAction( new Action({ type: InputType.MOUSE_MOVE, @@ -45,6 +60,23 @@ export abstract class AbstractDisplacementState) => { + const { event } = actionEvent; + this.fireMouseMoved({ + displacementX: event.touches[0].clientX - this.initialX, + displacementY: event.touches[0].clientY - this.initialY, + virtualDisplacementX: + (event.touches[0].clientX - this.initialX) / (this.engine.getModel().getZoomLevel() / 100.0), + virtualDisplacementY: + (event.touches[0].clientY - this.initialY) / (this.engine.getModel().getZoomLevel() / 100.0), + event: event + }); + } + }) + ); this.registerAction( new Action({ type: InputType.MOUSE_UP, @@ -54,6 +86,15 @@ export abstract class AbstractDisplacementState) => { + // when the mouse if up, we eject this state + this.eject(); + } + }) + ); } abstract fireMouseMoved(event: AbstractDisplacementStateEvent); diff --git a/packages/react-canvas-core/src/entities/canvas/CanvasWidget.tsx b/packages/react-canvas-core/src/entities/canvas/CanvasWidget.tsx index 3d6911281..78b8be477 100644 --- a/packages/react-canvas-core/src/entities/canvas/CanvasWidget.tsx +++ b/packages/react-canvas-core/src/entities/canvas/CanvasWidget.tsx @@ -90,6 +90,18 @@ export class CanvasWidget extends React.Component { }} onMouseMove={event => { this.props.engine.getActionEventBus().fireAction({ event }); + }} + onTouchMove={event => { + this.props.engine.getActionEventBus().fireAction({ event }); + }} + onTouchStart={event => { + this.props.engine.getActionEventBus().fireAction({ event }); + }} + onTouchMove={event => { + this.props.engine.getActionEventBus().fireAction({ event }); + }} + onTouchEnd={event => { + this.props.engine.getActionEventBus().fireAction({ event }); }}> {model.getLayers().map(layer => { return ( diff --git a/packages/react-canvas-core/src/states/MoveItemsState.ts b/packages/react-canvas-core/src/states/MoveItemsState.ts index b238709f4..6001c5d89 100644 --- a/packages/react-canvas-core/src/states/MoveItemsState.ts +++ b/packages/react-canvas-core/src/states/MoveItemsState.ts @@ -31,6 +31,19 @@ export class MoveItemsState extends Abstr } }) ); + this.registerAction( + new Action({ + type: InputType.TOUCH_START, + fire: (event: ActionEvent) => { + const element = this.engine.getActionEventBus().getModelForEvent(event); + if (!element.isSelected()) { + this.engine.getModel().clearSelection(); + } + element.setSelected(true); + this.engine.repaintCanvas(); + } + }) + ); } activated(previous: State) { diff --git a/packages/react-diagrams-core/src/DiagramEngine.ts b/packages/react-diagrams-core/src/DiagramEngine.ts index a286b5936..458255cda 100644 --- a/packages/react-diagrams-core/src/DiagramEngine.ts +++ b/packages/react-diagrams-core/src/DiagramEngine.ts @@ -3,7 +3,7 @@ import { PortModel } from './entities/port/PortModel'; import { LinkModel } from './entities/link/LinkModel'; import { LabelModel } from './entities/label/LabelModel'; import { Point, Rectangle } from '@projectstorm/geometry'; -import { MouseEvent } from 'react'; +import { MouseEvent, TouchEvent } from 'react'; import { AbstractModelFactory, AbstractReactFactory, @@ -58,7 +58,21 @@ export class DiagramEngine extends CanvasEngine 0 ? event.touches[0].clientX : event.changedTouches[0].clientX; + let y = event.touches.length > 0 ? event.touches[0].clientY : event.changedTouches[0].clientY; + + const target = document.elementFromPoint(x, y); + + return this.getElement(target); + } + + getElement(target: Element): BaseModel { + const diagramModel = this.model; //is it a port var element = Toolkit.closest(target, '.port[data-name]'); diff --git a/packages/react-diagrams-core/src/states/DefaultDiagramState.ts b/packages/react-diagrams-core/src/states/DefaultDiagramState.ts index adadd2f37..589baf42a 100644 --- a/packages/react-diagrams-core/src/states/DefaultDiagramState.ts +++ b/packages/react-diagrams-core/src/states/DefaultDiagramState.ts @@ -1,4 +1,4 @@ -import { MouseEvent } from 'react'; +import { MouseEvent, TouchEvent } from 'react'; import { SelectingState, State, @@ -48,5 +48,25 @@ export class DefaultDiagramState extends State { } }) ); + this.registerAction( + new Action({ + type: InputType.TOUCH_START, + fire: (event: ActionEvent) => { + const element = this.engine.getActionEventBus().getModelForEvent(event); + // the canvas was clicked on, transition to the dragging canvas state + if (!element) { + this.transitionWithEvent(this.dragCanvas, event); + } + // initiate dragging a new link + else if (element instanceof PortModel) { + this.transitionWithEvent(this.dragNewLink, event); + } + // move the items (and potentially link points) + else { + this.transitionWithEvent(this.dragItems, event); + } + } + }) + ); } } diff --git a/packages/react-diagrams-core/src/states/DragDiagramItemsState.ts b/packages/react-diagrams-core/src/states/DragDiagramItemsState.ts index 13c91bfb1..118cb5c88 100644 --- a/packages/react-diagrams-core/src/states/DragDiagramItemsState.ts +++ b/packages/react-diagrams-core/src/states/DragDiagramItemsState.ts @@ -3,7 +3,7 @@ import * as _ from 'lodash'; import { PointModel } from '../entities/link/PointModel'; import { DiagramEngine } from '../DiagramEngine'; import { PortModel } from '../entities/port/PortModel'; -import { MouseEvent } from 'react'; +import { MouseEvent, TouchEvent } from 'react'; import { LinkModel } from '../entities/link/LinkModel'; export class DragDiagramItemsState extends MoveItemsState { @@ -34,5 +34,30 @@ export class DragDiagramItemsState extends MoveItemsState { } }) ); + + this.registerAction( + new Action({ + type: InputType.TOUCH_END, + fire: (event: ActionEvent) => { + const item = this.engine.getMouseElement(event.event); + if (item instanceof PortModel) { + _.forEach(this.initialPositions, position => { + if (position.item instanceof PointModel) { + const link = position.item.getParent(); + // 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(); + } + } + }); + } + } + }) + ); } } diff --git a/packages/react-diagrams-core/src/states/DragNewLinkState.ts b/packages/react-diagrams-core/src/states/DragNewLinkState.ts index cbe87cea0..359c0aa45 100644 --- a/packages/react-diagrams-core/src/states/DragNewLinkState.ts +++ b/packages/react-diagrams-core/src/states/DragNewLinkState.ts @@ -6,7 +6,7 @@ import { InputType } from '@projectstorm/react-canvas-core'; import { PortModel } from '../entities/port/PortModel'; -import { MouseEvent } from 'react'; +import { MouseEvent, TouchEvent } from 'react'; import { LinkModel } from '../entities/link/LinkModel'; import { DiagramEngine } from '../DiagramEngine'; @@ -60,6 +60,31 @@ export class DragNewLinkState extends AbstractDisplacementState { }) ); + this.registerAction( + new Action({ + type: InputType.TOUCH_START, + fire: (event: ActionEvent) => { + this.port = this.engine.getTouchElement(event.event) as PortModel; + + if (!(this.port instanceof PortModel) || (!this.config.allowLinksFromLockedPorts && this.port.isLocked())) { + this.eject(); + return; + } + this.link = this.port.createLinkModel(); + + // if no link is given, just eject the state + if (!this.link) { + this.eject(); + return; + } + this.link.setSelected(true); + this.link.setSourcePort(this.port); + this.engine.getModel().addLink(this.link); + this.port.reportPosition(); + } + }) + ); + this.registerAction( new Action({ type: InputType.MOUSE_UP, @@ -83,23 +108,65 @@ export class DragNewLinkState extends AbstractDisplacementState { } }) ); + + this.registerAction( + new Action({ + type: InputType.TOUCH_END, + fire: (event: ActionEvent) => { + const model = this.engine.getTouchElement(event.event); + + // check to see if we connected to a new port + if (model instanceof PortModel) { + if (this.port.canLinkToPort(model)) { + this.link.setTargetPort(model); + model.reportPosition(); + this.engine.repaintCanvas(); + return; + } + } + + if (this.isNearbySourcePortMobile(event.event) || !this.config.allowLooseLinks) { + this.link.remove(); + this.engine.repaintCanvas(); + } + } + }) + ); } /** * Checks whether the mouse event appears to happen in proximity of the link's source port * @param event */ - isNearbySourcePort({ clientX, clientY }: MouseEvent): boolean { + isNearbySourcePort(event: MouseEvent): boolean { const sourcePort = this.link.getSourcePort(); const sourcePortPosition = this.link.getSourcePort().getPosition(); return ( - clientX >= sourcePortPosition.x && - clientX <= sourcePortPosition.x + sourcePort.width && - (clientY >= sourcePortPosition.y && clientY <= sourcePortPosition.y + sourcePort.height) + event.clientX >= sourcePortPosition.x && + event.clientX <= sourcePortPosition.x + sourcePort.width && + (event.clientY >= sourcePortPosition.y && event.clientY <= sourcePortPosition.y + sourcePort.height) ); } + /** + * Checks whether the touch event appears to happen in proximity of the link's source port + * @param event + */ + isNearbySourcePortMobile(event: TouchEvent): boolean { + const sourcePort = this.link.getSourcePort(); + const sourcePortPosition = this.link.getSourcePort().getPosition(); + + if (event.touches && event.touches[0]) { + return ( + event.touches[0].clientX >= sourcePortPosition.x && + event.touches[0].clientX <= sourcePortPosition.x + sourcePort.width && + (event.touches[0].clientY >= sourcePortPosition.y && + event.touches[0].clientY <= sourcePortPosition.y + sourcePort.height) + ); + } + } + /** * Calculates the link's far-end point position on mouse move. * In order to be as precise as possible the mouse initialXRelative & initialYRelative are taken into account as well