mirror of
https://github.com/projectstorm/react-diagrams.git
synced 2025-08-26 07:51:10 +08:00
break it up further into canvas
This commit is contained in:
@ -1,43 +0,0 @@
|
||||
import './sass/main.scss';
|
||||
|
||||
export * from './src/core-actions/AbstractAction';
|
||||
export * from './src/core-actions/AbstractActionFactory';
|
||||
export * from './src/core-actions/AbstractMouseAction';
|
||||
|
||||
export * from './src/actions/move-canvas/MoveCanvasActionFactory';
|
||||
export * from './src/actions/move-canvas/MoveCanvasAction';
|
||||
|
||||
export * from './src/actions/selecting-items/SelectingAction';
|
||||
export * from './src/actions/selecting-items/SelectingItemsActionFactory';
|
||||
|
||||
export * from './src/actions/move-items/MoveItemsAction';
|
||||
export * from './src/actions/move-items/MoveItemsActionFactory';
|
||||
|
||||
export * from './src/core/BaseObserver';
|
||||
export * from './src/core/FactoryBank';
|
||||
export * from './src/core/AbstractFactory';
|
||||
export * from './src/core/AbstractModelFactory';
|
||||
export * from './src/core/AbstractReactFactory';
|
||||
|
||||
export * from './src/core-models/BaseModel';
|
||||
export * from './src/core-models/BasePositionModel';
|
||||
export * from './src/core-models/BaseEntity';
|
||||
|
||||
export * from './src/models/DiagramModel';
|
||||
export * from './src/models/LabelModel';
|
||||
export * from './src/models/LinkModel';
|
||||
export * from './src/models/PointModel';
|
||||
export * from './src/models/PortModel';
|
||||
export * from './src/models/SelectionModel';
|
||||
export * from './src/models/NodeModel';
|
||||
|
||||
export * from './src/widgets/BaseWidget';
|
||||
export * from './src/widgets/DiagramWidget';
|
||||
export * from './src/widgets/layers/LinkLayerWidget';
|
||||
export * from './src/widgets/layers/NodeLayerWidget';
|
||||
export * from './src/widgets/LinkWidget';
|
||||
export * from './src/widgets/NodeWidget';
|
||||
export * from './src/widgets/PortWidget';
|
||||
|
||||
export * from './src/DiagramEngine';
|
||||
export * from './src/Toolkit';
|
@ -1,13 +0,0 @@
|
||||
.srd-diagram {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
cursor: move;
|
||||
overflow: hidden;
|
||||
|
||||
&__selector {
|
||||
position: absolute;
|
||||
background-color: rgba(0, 192, 255, 0.2);
|
||||
border: solid 2px rgb(0, 192, 255);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
.srd-link-layer {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transform-origin: 0 0;
|
||||
overflow: visible !important;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
.srd-node-layer {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
transform-origin: 0 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
.srd-node {
|
||||
position: absolute;
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Chrome/Safari/Opera */
|
||||
user-select: none;
|
||||
cursor: move;
|
||||
pointer-events: all;
|
||||
|
||||
&--selected {
|
||||
> * {
|
||||
border-color: rgb(0, 192, 255) !important;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
.srd-port {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: rgba(white, 0.1);
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
background: rgb(192, 255, 0);
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
@import 'DiagramWidget';
|
||||
@import 'LinkLayerWidget';
|
||||
@import 'NodeLayerWidget';
|
||||
@import 'NodeWidget';
|
||||
@import 'PortWidget';
|
@ -1,28 +0,0 @@
|
||||
import { AbstractMouseAction } from '../../core-actions/AbstractMouseAction';
|
||||
import { MouseEvent } from 'react';
|
||||
import { DiagramEngine } from '../../DiagramEngine';
|
||||
|
||||
export class MoveCanvasAction extends AbstractMouseAction {
|
||||
initialOffsetX: number;
|
||||
initialOffsetY: number;
|
||||
|
||||
constructor(mouseX: number, mouseY: number, engine: DiagramEngine) {
|
||||
super(mouseX, mouseY, engine);
|
||||
this.initialOffsetX = this.model.getOffsetX();
|
||||
this.initialOffsetY = this.model.getOffsetY();
|
||||
}
|
||||
|
||||
fireMouseMove(event: MouseEvent) {
|
||||
//translate the actual canvas
|
||||
this.model.setOffset(
|
||||
this.initialOffsetX + (event.clientX - this.mouseX),
|
||||
this.initialOffsetY + (event.clientY - this.mouseY)
|
||||
);
|
||||
}
|
||||
|
||||
fireMouseUp(event) {}
|
||||
|
||||
fireMouseDown(event) {
|
||||
this.model.clearSelection();
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import { AbstractActionFactory, ActionFactoryActivationEvent } from '../../core-actions/AbstractActionFactory';
|
||||
import { MoveCanvasAction } from './MoveCanvasAction';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
export class MoveCanvasActionFactory extends AbstractActionFactory<MoveCanvasAction> {
|
||||
constructor() {
|
||||
super('move-canvas');
|
||||
}
|
||||
|
||||
generateAction(event: MouseEvent): MoveCanvasAction {
|
||||
return new MoveCanvasAction(event.clientX, event.clientY, this.engine);
|
||||
}
|
||||
|
||||
activate(event: ActionFactoryActivationEvent): boolean {
|
||||
return !event.selectedEntity && !event.mouseEvent.shiftKey;
|
||||
}
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
import { AbstractMouseAction } from '../../core-actions/AbstractMouseAction';
|
||||
import { SelectionModel } from '../../models/SelectionModel';
|
||||
import { PointModel } from '../../models/PointModel';
|
||||
import { NodeModel } from '../../models/NodeModel';
|
||||
import { DiagramEngine } from '../../DiagramEngine';
|
||||
import { BasePositionModel } from '../../core-models/BasePositionModel';
|
||||
import * as _ from 'lodash';
|
||||
import { PortModel } from '../../models/PortModel';
|
||||
import { LinkModel } from '../../models/LinkModel';
|
||||
import { MouseEvent } from 'react';
|
||||
import { ActionFactoryActivationEvent } from '../../core-actions/AbstractActionFactory';
|
||||
|
||||
export class MoveItemsAction extends AbstractMouseAction {
|
||||
selectionModels: SelectionModel[];
|
||||
moved: boolean;
|
||||
allowLooseLinks: boolean;
|
||||
|
||||
constructor(mouseX: number, mouseY: number, diagramEngine: DiagramEngine, allowLooseLinks: boolean) {
|
||||
super(mouseX, mouseY, diagramEngine);
|
||||
this.allowLooseLinks = allowLooseLinks;
|
||||
this.moved = false;
|
||||
}
|
||||
|
||||
fireMouseMove(event: MouseEvent) {
|
||||
let amountX = event.clientX - this.mouseX;
|
||||
let amountY = event.clientY - this.mouseY;
|
||||
let amountZoom = this.model.getZoomLevel() / 100;
|
||||
|
||||
_.forEach(this.selectionModels, model => {
|
||||
// in this case we need to also work out the relative grid position
|
||||
if (model.model instanceof NodeModel || (model.model instanceof PointModel && !model.model.isConnectedToPort())) {
|
||||
model.model.setPosition(
|
||||
this.model.getGridPosition(model.initialX + amountX / amountZoom),
|
||||
this.model.getGridPosition(model.initialY + amountY / amountZoom)
|
||||
);
|
||||
} else if (model.model instanceof PointModel) {
|
||||
// we want points that are connected to ports, to not necessarily snap to grid
|
||||
// this stuff needs to be pixel perfect, dont touch it
|
||||
model.model.setPosition(
|
||||
model.initialX + this.model.getGridPosition(amountX / amountZoom),
|
||||
model.initialY + this.model.getGridPosition(amountY / amountZoom)
|
||||
);
|
||||
}
|
||||
});
|
||||
this.moved = true;
|
||||
}
|
||||
|
||||
fireMouseUp(event: MouseEvent) {
|
||||
const element = this.engine.getMouseElement(event);
|
||||
_.forEach(this.selectionModels, model => {
|
||||
//only care about points connecting to things
|
||||
if (!(model.model instanceof PointModel)) {
|
||||
return;
|
||||
}
|
||||
if (element && element.model instanceof PortModel && !this.engine.isModelLocked(element.model)) {
|
||||
let link = model.model.getLink();
|
||||
|
||||
//if this was a valid link already and we are adding a node in the middle, create 2 links from the original
|
||||
if (link.getTargetPort()) {
|
||||
if (link.getTargetPort() !== element.model && link.getSourcePort() !== element.model) {
|
||||
const targetPort = link.getTargetPort();
|
||||
let newLink = link.clone({});
|
||||
newLink.setSourcePort(element.model);
|
||||
newLink.setTargetPort(targetPort);
|
||||
link.setTargetPort(element.model);
|
||||
link.getLastPoint().setPosition(this.engine.getPortCenter(element.model));
|
||||
targetPort.removeLink(link);
|
||||
newLink.removePointsBefore(newLink.getPoints()[link.getPointIndex(model.model)]);
|
||||
link.removePointsAfter(model.model);
|
||||
this.engine.getDiagramModel().addLink(newLink);
|
||||
//if we are connecting to the same target or source, remove tweener points
|
||||
} else if (link.getTargetPort() === element.model) {
|
||||
link.removePointsAfter(model.model);
|
||||
} else if (link.getSourcePort() === element.model) {
|
||||
link.removePointsBefore(model.model);
|
||||
}
|
||||
}
|
||||
|
||||
// set the target port
|
||||
else {
|
||||
link.setTargetPort(element.model);
|
||||
link.getLastPoint().setPosition(this.engine.getPortCenter(element.model));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//check for / remove any loose links in any models which have been moved
|
||||
if (!this.allowLooseLinks && this.moved) {
|
||||
_.forEach(this.selectionModels, model => {
|
||||
//only care about points connecting to things
|
||||
if (!(model.model instanceof PointModel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedPoint: PointModel = model.model;
|
||||
let link: LinkModel = selectedPoint.getLink();
|
||||
if (link.getSourcePort() === null || link.getTargetPort() === null) {
|
||||
link.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//remove any invalid links
|
||||
_.forEach(this.selectionModels, model => {
|
||||
//only care about points connecting to things
|
||||
if (!(model.model instanceof PointModel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let link: LinkModel = model.model.getLink();
|
||||
let sourcePort: PortModel = link.getSourcePort();
|
||||
let targetPort: PortModel = link.getTargetPort();
|
||||
if (sourcePort !== null && targetPort !== null) {
|
||||
if (!sourcePort.canLinkToPort(targetPort)) {
|
||||
//link not allowed
|
||||
link.remove();
|
||||
} else if (
|
||||
_.some(
|
||||
_.values(targetPort.getLinks()),
|
||||
(l: LinkModel) => l !== link && (l.getSourcePort() === sourcePort || l.getTargetPort() === sourcePort)
|
||||
)
|
||||
) {
|
||||
//link is a duplicate
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fireMouseDown(event: ActionFactoryActivationEvent) {
|
||||
// clear selection first?
|
||||
if (!event.selectedModel.isSelected()) {
|
||||
this.model.clearSelection();
|
||||
}
|
||||
|
||||
if (event.selectedModel instanceof PortModel) {
|
||||
//its a port element, we want to drag a link
|
||||
if (!this.engine.isModelLocked(event.selectedModel)) {
|
||||
const portCenter = this.engine.getPortCenter(event.selectedModel);
|
||||
const sourcePort = event.selectedModel;
|
||||
const link = sourcePort.createLinkModel();
|
||||
link.setSourcePort(sourcePort);
|
||||
|
||||
if (link) {
|
||||
link.removeMiddlePoints();
|
||||
if (link.getSourcePort() !== sourcePort) {
|
||||
link.setSourcePort(sourcePort);
|
||||
}
|
||||
link.setTargetPort(null);
|
||||
|
||||
link.getFirstPoint().setPosition(portCenter);
|
||||
link.getLastPoint().setPosition(portCenter);
|
||||
|
||||
this.model.clearSelection();
|
||||
link.getLastPoint().setSelected(true);
|
||||
this.model.addLink(link);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
event.selectedModel.setSelected(true);
|
||||
}
|
||||
const selectedItems = this.model.getSelectedItems().filter(item => {
|
||||
if (!(item instanceof BasePositionModel)) {
|
||||
return false;
|
||||
}
|
||||
return !this.engine.isModelLocked(item);
|
||||
});
|
||||
|
||||
this.selectionModels = selectedItems.map((item: PointModel | NodeModel) => {
|
||||
return {
|
||||
model: item,
|
||||
initialX: item.getX(),
|
||||
initialY: item.getY()
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { AbstractActionFactory, ActionFactoryActivationEvent } from '../../core-actions/AbstractActionFactory';
|
||||
import { MouseEvent } from 'react';
|
||||
import { MoveItemsAction } from './MoveItemsAction';
|
||||
|
||||
export interface MoveItemsActionFactoryOptions {
|
||||
allowLooseLinks?: boolean;
|
||||
}
|
||||
|
||||
export class MoveItemsActionFactory extends AbstractActionFactory<MoveItemsAction> {
|
||||
options: MoveItemsActionFactoryOptions;
|
||||
|
||||
static NAME = 'move-items';
|
||||
|
||||
constructor(options: MoveItemsActionFactoryOptions = {}) {
|
||||
super(MoveItemsActionFactory.NAME);
|
||||
this.options = {
|
||||
...options,
|
||||
allowLooseLinks: options.allowLooseLinks == null ? true : options.allowLooseLinks
|
||||
};
|
||||
}
|
||||
|
||||
generateAction(event: MouseEvent): MoveItemsAction {
|
||||
return new MoveItemsAction(event.clientX, event.clientY, this.engine, this.options.allowLooseLinks);
|
||||
}
|
||||
|
||||
activate(event: ActionFactoryActivationEvent): boolean {
|
||||
if (event.selectedModel) {
|
||||
return !this.engine.isModelLocked(event.selectedModel);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
import { AbstractMouseAction } from '../../core-actions/AbstractMouseAction';
|
||||
import { DiagramModel } from '../../models/DiagramModel';
|
||||
import * as _ from 'lodash';
|
||||
import { DiagramEngine } from '../../DiagramEngine';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
export class SelectingAction extends AbstractMouseAction {
|
||||
mouseX2: number;
|
||||
mouseY2: number;
|
||||
|
||||
constructor(mouseX: number, mouseY: number, engine: DiagramEngine) {
|
||||
super(mouseX, mouseY, engine);
|
||||
this.mouseX2 = mouseX;
|
||||
this.mouseY2 = mouseY;
|
||||
}
|
||||
|
||||
getBoxDimensions() {
|
||||
return {
|
||||
left: this.mouseX2 > this.mouseX ? this.mouseX : this.mouseX2,
|
||||
top: this.mouseY2 > this.mouseY ? this.mouseY : this.mouseY2,
|
||||
width: Math.abs(this.mouseX2 - this.mouseX),
|
||||
height: Math.abs(this.mouseY2 - this.mouseY),
|
||||
right: this.mouseX2 < this.mouseX ? this.mouseX : this.mouseX2,
|
||||
bottom: this.mouseY2 < this.mouseY ? this.mouseY : this.mouseY2
|
||||
};
|
||||
}
|
||||
|
||||
containsElement(x: number, y: number, diagramModel: DiagramModel): boolean {
|
||||
var z = diagramModel.getZoomLevel() / 100.0;
|
||||
let dimensions = this.getBoxDimensions();
|
||||
|
||||
return (
|
||||
x * z + diagramModel.getOffsetX() > dimensions.left &&
|
||||
x * z + diagramModel.getOffsetX() < dimensions.right &&
|
||||
y * z + diagramModel.getOffsetY() > dimensions.top &&
|
||||
y * z + diagramModel.getOffsetY() < dimensions.bottom
|
||||
);
|
||||
}
|
||||
|
||||
fireMouseMove(event: MouseEvent) {
|
||||
var relative = this.engine.getRelativePoint(event.clientX, event.clientY);
|
||||
|
||||
_.forEach(this.model.getNodes(), node => {
|
||||
// TODO use geometry instead
|
||||
if (this.containsElement(node.getX(), node.getY(), this.model)) {
|
||||
node.setSelected(true);
|
||||
}
|
||||
});
|
||||
|
||||
_.forEach(this.model.getLinks(), link => {
|
||||
var allSelected = true;
|
||||
_.forEach(link.getPoints(), point => {
|
||||
if (this.containsElement(point.getX(), point.getY(), this.model)) {
|
||||
point.setSelected(true);
|
||||
} else {
|
||||
allSelected = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (allSelected) {
|
||||
link.setSelected(true);
|
||||
}
|
||||
});
|
||||
|
||||
this.mouseX2 = relative.x;
|
||||
this.mouseY2 = relative.y;
|
||||
}
|
||||
|
||||
fireMouseUp(event) {}
|
||||
|
||||
fireMouseDown(event) {}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import { AbstractActionFactory, ActionFactoryActivationEvent } from '../../core-actions/AbstractActionFactory';
|
||||
import { MouseEvent } from 'react';
|
||||
import { SelectingAction } from './SelectingAction';
|
||||
|
||||
export class SelectingItemsActionFactory extends AbstractActionFactory<SelectingAction> {
|
||||
constructor() {
|
||||
super('select-items');
|
||||
}
|
||||
|
||||
generateAction(event: MouseEvent): SelectingAction {
|
||||
return new SelectingAction(event.clientX, event.clientY, this.engine);
|
||||
}
|
||||
|
||||
activate(event: ActionFactoryActivationEvent): boolean {
|
||||
return !event.selectedEntity && event.mouseEvent.shiftKey;
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import { DiagramEngine } from '../DiagramEngine';
|
||||
import { DiagramModel } from '../models/DiagramModel';
|
||||
|
||||
export class AbstractAction {
|
||||
engine: DiagramEngine;
|
||||
model: DiagramModel;
|
||||
|
||||
constructor(engine: DiagramEngine) {
|
||||
this.engine = engine;
|
||||
this.model = engine.getDiagramModel();
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { AbstractAction } from './AbstractAction';
|
||||
import { AbstractFactory } from '../core/AbstractFactory';
|
||||
import { MouseEvent } from 'react';
|
||||
import { BaseModel } from '../core-models/BaseModel';
|
||||
|
||||
export interface ActionFactoryActivationEvent {
|
||||
selectedModel: BaseModel;
|
||||
selectedEntity: HTMLElement;
|
||||
mouseEvent: MouseEvent;
|
||||
}
|
||||
|
||||
export abstract class AbstractActionFactory<T extends AbstractAction = AbstractAction> extends AbstractFactory {
|
||||
abstract activate(event: ActionFactoryActivationEvent): boolean;
|
||||
|
||||
abstract generateAction(event: MouseEvent): T;
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { AbstractAction } from './AbstractAction';
|
||||
import { DiagramEngine } from '../DiagramEngine';
|
||||
import { ActionFactoryActivationEvent } from './AbstractActionFactory';
|
||||
|
||||
export abstract class AbstractMouseAction extends AbstractAction {
|
||||
protected mouseX: number;
|
||||
protected mouseY: number;
|
||||
|
||||
constructor(mouseX: number, mouseY: number, engine: DiagramEngine) {
|
||||
super(engine);
|
||||
this.mouseX = mouseX;
|
||||
this.mouseY = mouseY;
|
||||
}
|
||||
|
||||
abstract fireMouseDown(event: ActionFactoryActivationEvent);
|
||||
|
||||
abstract fireMouseMove(event: MouseEvent);
|
||||
|
||||
abstract fireMouseUp(event: MouseEvent);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { AbstractFactory } from './AbstractFactory';
|
||||
import { BaseModel } from '../core-models/BaseModel';
|
||||
|
||||
export interface GenerateModelEvent {
|
||||
initialConfig?: any;
|
||||
}
|
||||
|
||||
export abstract class AbstractModelFactory<T extends BaseModel> extends AbstractFactory<T> {
|
||||
/**
|
||||
* Generates new models (the core factory pattern)
|
||||
*/
|
||||
abstract generateModel(event: GenerateModelEvent): T;
|
||||
}
|
@ -1,285 +0,0 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
BaseEntityEvent,
|
||||
BaseEntityGenerics,
|
||||
BaseEntityListener,
|
||||
BaseEntityOptions,
|
||||
BaseEntityType
|
||||
} from '../core-models/BaseEntity';
|
||||
import * as _ from 'lodash';
|
||||
import { DiagramEngine } from '../DiagramEngine';
|
||||
import { LinkModel } from './LinkModel';
|
||||
import { NodeModel } from './NodeModel';
|
||||
import { PortModel } from './PortModel';
|
||||
import { BaseModel } from '../core-models/BaseModel';
|
||||
import { PointModel } from './PointModel';
|
||||
|
||||
export interface DiagramListener extends BaseEntityListener {
|
||||
nodesUpdated?(event: BaseEntityEvent & { node: NodeModel; isCreated: boolean }): void;
|
||||
|
||||
linksUpdated?(event: BaseEntityEvent & { link: LinkModel; isCreated: boolean }): void;
|
||||
|
||||
offsetUpdated?(event: BaseEntityEvent<DiagramModel> & { offsetX: number; offsetY: number }): void;
|
||||
|
||||
zoomUpdated?(event: BaseEntityEvent<DiagramModel> & { zoom: number }): void;
|
||||
|
||||
gridUpdated?(event: BaseEntityEvent<DiagramModel> & { size: number }): void;
|
||||
}
|
||||
|
||||
export interface DiagramModelOptions extends BaseEntityOptions {
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
zoom?: number;
|
||||
gridSize?: number;
|
||||
}
|
||||
|
||||
export interface DiagramModelGenerics extends BaseEntityGenerics {
|
||||
LISTENER: DiagramListener;
|
||||
OPTIONS: DiagramModelOptions;
|
||||
}
|
||||
|
||||
export class DiagramModel<G extends DiagramModelGenerics = DiagramModelGenerics> extends BaseEntity<G> {
|
||||
//models
|
||||
protected links: { [s: string]: LinkModel };
|
||||
protected nodes: { [s: string]: NodeModel };
|
||||
|
||||
rendered: boolean;
|
||||
|
||||
constructor(options: G['OPTIONS'] = {}) {
|
||||
super({
|
||||
zoom: 100,
|
||||
gridSize: 0,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
...options
|
||||
});
|
||||
|
||||
this.links = {};
|
||||
this.nodes = {};
|
||||
this.rendered = false;
|
||||
}
|
||||
|
||||
setGridSize(size: number = 0) {
|
||||
this.options.gridSize = size;
|
||||
this.fireEvent({ size: size }, 'gridUpdated');
|
||||
}
|
||||
|
||||
getGridPosition(pos) {
|
||||
if (this.options.gridSize === 0) {
|
||||
return pos;
|
||||
}
|
||||
return this.options.gridSize * Math.floor((pos + this.options.gridSize / 2) / this.options.gridSize);
|
||||
}
|
||||
|
||||
deSerializeDiagram(object: any, diagramEngine: DiagramEngine) {
|
||||
this.deSerialize(object, diagramEngine);
|
||||
|
||||
this.options.offsetX = object.offsetX;
|
||||
this.options.offsetY = object.offsetY;
|
||||
this.options.zoom = object.zoom;
|
||||
this.options.gridSize = object.gridSize;
|
||||
|
||||
// deserialize nodes
|
||||
_.forEach(object.nodes, (node: any) => {
|
||||
let nodeOb = diagramEngine.getFactoryForNode(node.type).generateModel({ initialConfig: node });
|
||||
nodeOb.setParent(this);
|
||||
nodeOb.deSerialize(node, diagramEngine);
|
||||
this.addNode(nodeOb);
|
||||
});
|
||||
|
||||
// deserialze links
|
||||
_.forEach(object.links, (link: any) => {
|
||||
let linkOb = diagramEngine.getFactoryForLink(link.type).generateModel({ initialConfig: link });
|
||||
linkOb.setParent(this);
|
||||
linkOb.deSerialize(link, diagramEngine);
|
||||
this.addLink(linkOb);
|
||||
});
|
||||
}
|
||||
|
||||
serializeDiagram() {
|
||||
return {
|
||||
...this.serialize(),
|
||||
offsetX: this.options.offsetX,
|
||||
offsetY: this.options.offsetY,
|
||||
zoom: this.options.zoom,
|
||||
gridSize: this.options.gridSize,
|
||||
links: _.map(this.links, link => {
|
||||
return link.serialize();
|
||||
}),
|
||||
nodes: _.map(this.nodes, node => {
|
||||
return node.serialize();
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
clearSelection(ignore: BaseModel | null = null) {
|
||||
_.forEach(this.getSelectedItems(), element => {
|
||||
if (ignore && ignore.getID() === element.getID()) {
|
||||
return;
|
||||
}
|
||||
element.setSelected(false); //TODO dont fire the listener
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedItems(...filters: BaseEntityType[]): BaseModel[] {
|
||||
if (!Array.isArray(filters)) {
|
||||
filters = [filters];
|
||||
}
|
||||
var items = [];
|
||||
|
||||
// run through nodes
|
||||
items = items.concat(
|
||||
_.flatMap(this.nodes, node => {
|
||||
return node.getSelectedEntities();
|
||||
})
|
||||
);
|
||||
|
||||
// find all the links
|
||||
items = items.concat(
|
||||
_.flatMap(this.links, link => {
|
||||
return link.getSelectedEntities();
|
||||
})
|
||||
);
|
||||
|
||||
//find all points
|
||||
items = items.concat(
|
||||
_.flatMap(this.links, link => {
|
||||
return _.flatMap(link.getPoints(), point => {
|
||||
return point.getSelectedEntities();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
items = _.uniq(items);
|
||||
|
||||
if (filters.length > 0) {
|
||||
items = _.filter(_.uniq(items), (item: BaseModel<any>) => {
|
||||
if (_.includes(filters, 'node') && item instanceof NodeModel) {
|
||||
return true;
|
||||
}
|
||||
if (_.includes(filters, 'link') && item instanceof LinkModel) {
|
||||
return true;
|
||||
}
|
||||
if (_.includes(filters, 'port') && item instanceof PortModel) {
|
||||
return true;
|
||||
}
|
||||
if (_.includes(filters, 'point') && item instanceof PointModel) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
setZoomLevel(zoom: number) {
|
||||
this.options.zoom = zoom;
|
||||
this.fireEvent({ zoom }, 'zoomUpdated');
|
||||
}
|
||||
|
||||
setOffset(offsetX: number, offsetY: number) {
|
||||
this.options.offsetX = offsetX;
|
||||
this.options.offsetY = offsetY;
|
||||
this.fireEvent({ offsetX, offsetY }, 'offsetUpdated');
|
||||
}
|
||||
|
||||
setOffsetX(offsetX: number) {
|
||||
this.setOffset(offsetX, this.options.offsetY);
|
||||
}
|
||||
|
||||
setOffsetY(offsetY: number) {
|
||||
this.setOffset(this.options.offsetX, offsetY);
|
||||
}
|
||||
|
||||
getOffsetY() {
|
||||
return this.options.offsetY;
|
||||
}
|
||||
|
||||
getOffsetX() {
|
||||
return this.options.offsetX;
|
||||
}
|
||||
|
||||
getZoomLevel() {
|
||||
return this.options.zoom;
|
||||
}
|
||||
|
||||
getNode(node: string | NodeModel): NodeModel | null {
|
||||
if (node instanceof NodeModel) {
|
||||
return node;
|
||||
}
|
||||
if (!this.nodes[node]) {
|
||||
return null;
|
||||
}
|
||||
return this.nodes[node];
|
||||
}
|
||||
|
||||
getLink(link: string | LinkModel): LinkModel | null {
|
||||
if (link instanceof LinkModel) {
|
||||
return link;
|
||||
}
|
||||
if (!this.links[link]) {
|
||||
return null;
|
||||
}
|
||||
return this.links[link];
|
||||
}
|
||||
|
||||
addAll(...models: BaseModel[]): BaseModel[] {
|
||||
_.forEach(models, model => {
|
||||
if (model instanceof LinkModel) {
|
||||
this.addLink(model);
|
||||
} else if (model instanceof NodeModel) {
|
||||
this.addNode(model);
|
||||
}
|
||||
});
|
||||
return models;
|
||||
}
|
||||
|
||||
addLink(link: LinkModel): LinkModel {
|
||||
link.registerListener({
|
||||
entityRemoved: () => {
|
||||
this.removeLink(link);
|
||||
}
|
||||
});
|
||||
this.links[link.getID()] = link;
|
||||
this.fireEvent(
|
||||
{
|
||||
link,
|
||||
isCreated: true
|
||||
},
|
||||
'linksUpdated'
|
||||
);
|
||||
return link;
|
||||
}
|
||||
|
||||
addNode(node: NodeModel): NodeModel {
|
||||
node.registerListener({
|
||||
entityRemoved: () => {
|
||||
this.removeNode(node);
|
||||
}
|
||||
});
|
||||
this.nodes[node.getID()] = node;
|
||||
this.fireEvent({ node, isCreated: true }, 'nodesUpdated');
|
||||
return node;
|
||||
}
|
||||
|
||||
removeLink(link: LinkModel | string) {
|
||||
link = this.getLink(link);
|
||||
delete this.links[link.getID()];
|
||||
this.fireEvent({ link, isCreated: false }, 'linksUpdated');
|
||||
}
|
||||
|
||||
removeNode(node: NodeModel | string) {
|
||||
node = this.getNode(node);
|
||||
delete this.nodes[node.getID()];
|
||||
this.fireEvent({ node, isCreated: false }, 'nodesUpdated');
|
||||
}
|
||||
|
||||
getLinks(): { [s: string]: LinkModel } {
|
||||
return this.links;
|
||||
}
|
||||
|
||||
getNodes(): { [s: string]: NodeModel } {
|
||||
return this.nodes;
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { BaseModel } from '../core-models/BaseModel';
|
||||
|
||||
export interface SelectionModel {
|
||||
model: BaseModel;
|
||||
initialX: number;
|
||||
initialY: number;
|
||||
}
|
@ -1 +0,0 @@
|
||||
export class ActionManager {}
|
@ -1,41 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface BaseWidgetProps {
|
||||
/**
|
||||
* Override the base class name
|
||||
*/
|
||||
baseClass?: string;
|
||||
/**
|
||||
* append additional classes
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Additional props to add
|
||||
*/
|
||||
extraProps?: any;
|
||||
}
|
||||
|
||||
export class BaseWidget<P extends BaseWidgetProps = BaseWidgetProps, S = any> extends React.Component<P, S> {
|
||||
className: string;
|
||||
|
||||
constructor(name: string, props: P) {
|
||||
super(props);
|
||||
this.className = name;
|
||||
}
|
||||
|
||||
bem(selector: string): string {
|
||||
return (this.props.baseClass || this.className) + selector + ' ';
|
||||
}
|
||||
|
||||
getClassName(): string {
|
||||
return (this.props.baseClass || this.className) + ' ' + (this.props.className ? this.props.className + ' ' : '');
|
||||
}
|
||||
|
||||
getProps(): any {
|
||||
return {
|
||||
...((this.props.extraProps as any) || {}),
|
||||
className: this.getClassName()
|
||||
};
|
||||
}
|
||||
}
|
@ -1,288 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { DiagramEngine } from '../DiagramEngine';
|
||||
import * as _ from 'lodash';
|
||||
import { LinkLayerWidget } from './layers/LinkLayerWidget';
|
||||
import { NodeLayerWidget } from './layers/NodeLayerWidget';
|
||||
import { AbstractMouseAction } from '../core-actions/AbstractMouseAction';
|
||||
import { MoveItemsAction } from '../actions/move-items/MoveItemsAction';
|
||||
import { SelectingAction } from '../actions/selecting-items/SelectingAction';
|
||||
import { PointModel } from '../models/PointModel';
|
||||
import { BaseWidget, BaseWidgetProps } from './BaseWidget';
|
||||
import { MouseEvent } from 'react';
|
||||
import { ActionFactoryActivationEvent } from '../core-actions/AbstractActionFactory';
|
||||
import { AbstractAction } from '../core-actions/AbstractAction';
|
||||
import { MoveItemsActionFactory } from '../actions/move-items/MoveItemsActionFactory';
|
||||
|
||||
export interface DiagramProps extends BaseWidgetProps {
|
||||
diagramEngine: DiagramEngine;
|
||||
|
||||
// zoom
|
||||
allowCanvasZoom?: boolean;
|
||||
inverseZoom?: boolean;
|
||||
|
||||
actionStartedFiring?: (action: AbstractAction) => boolean;
|
||||
actionStillFiring?: (action: AbstractAction) => void;
|
||||
actionStoppedFiring?: (action: AbstractAction) => void;
|
||||
|
||||
deleteKeys?: number[];
|
||||
}
|
||||
|
||||
export interface DiagramState {
|
||||
action: AbstractAction;
|
||||
diagramEngineListener: any;
|
||||
}
|
||||
|
||||
export class DiagramWidget extends BaseWidget<DiagramProps, DiagramState> {
|
||||
onKeyUpPointer: (this: Window, ev: KeyboardEvent) => void = null;
|
||||
ref: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: DiagramProps) {
|
||||
super('srd-diagram', props);
|
||||
|
||||
this.ref = React.createRef();
|
||||
this.state = {
|
||||
action: null,
|
||||
diagramEngineListener: null
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.diagramEngine.deregisterListener(this.state.diagramEngineListener);
|
||||
this.props.diagramEngine.setCanvas(null);
|
||||
window.removeEventListener('keyup', this.onKeyUpPointer);
|
||||
window.removeEventListener('mouseUp', this.onMouseUp);
|
||||
window.removeEventListener('mouseMove', this.onMouseMove);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: DiagramProps) {
|
||||
if (this.props.diagramEngine !== nextProps.diagramEngine) {
|
||||
this.props.diagramEngine.deregisterListener(this.state.diagramEngineListener);
|
||||
const diagramEngineListener = nextProps.diagramEngine.registerListener({
|
||||
repaintCanvas: () => this.forceUpdate()
|
||||
});
|
||||
this.setState({ diagramEngineListener });
|
||||
}
|
||||
}
|
||||
|
||||
registerCanvas() {
|
||||
this.props.diagramEngine.setCanvas(this.ref.current);
|
||||
this.props.diagramEngine.iterateListeners(list => {
|
||||
list.rendered && list.rendered();
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.registerCanvas();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onKeyUpPointer = this.onKeyUp.bind(this);
|
||||
|
||||
//add a keyboard listener
|
||||
this.setState({
|
||||
diagramEngineListener: this.props.diagramEngine.registerListener({
|
||||
repaintCanvas: () => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
window.addEventListener('keyup', this.onKeyUpPointer, false);
|
||||
|
||||
// dont focus the window when in test mode - jsdom fails
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
window.focus();
|
||||
}
|
||||
|
||||
this.registerCanvas();
|
||||
}
|
||||
|
||||
fireAction() {
|
||||
if (this.state.action && this.props.actionStillFiring) {
|
||||
this.props.actionStillFiring(this.state.action);
|
||||
}
|
||||
}
|
||||
|
||||
stopFiringAction(shouldSkipEvent?: boolean) {
|
||||
if (this.props.actionStoppedFiring && !shouldSkipEvent) {
|
||||
this.props.actionStoppedFiring(this.state.action);
|
||||
}
|
||||
this.setState({ action: null });
|
||||
}
|
||||
|
||||
startFiringAction(action: AbstractAction) {
|
||||
var setState = true;
|
||||
if (this.props.actionStartedFiring) {
|
||||
setState = this.props.actionStartedFiring(action);
|
||||
}
|
||||
if (setState) {
|
||||
this.setState({ action: action });
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp = event => {
|
||||
if (this.state.action && this.state.action instanceof AbstractMouseAction) {
|
||||
this.state.action.fireMouseUp(event);
|
||||
}
|
||||
this.stopFiringAction();
|
||||
document.removeEventListener('mousemove', this.onMouseMove);
|
||||
document.removeEventListener('mouseup', this.onMouseUp);
|
||||
};
|
||||
|
||||
onMouseMove = event => {
|
||||
//select items so draw a bounding box
|
||||
if (this.state.action) {
|
||||
if (this.state.action && this.state.action instanceof AbstractMouseAction) {
|
||||
this.state.action.fireMouseMove(event);
|
||||
}
|
||||
this.fireAction();
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
onKeyUp = event => {
|
||||
//delete all selected
|
||||
if ((this.props.deleteKeys || [46, 8]).indexOf(event.keyCode) !== -1) {
|
||||
_.forEach(this.props.diagramEngine.getDiagramModel().getSelectedItems(), element => {
|
||||
//only delete items which are not locked
|
||||
if (!this.props.diagramEngine.isModelLocked(element)) {
|
||||
element.remove();
|
||||
}
|
||||
});
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
drawSelectionBox() {
|
||||
let dimensions = (this.state.action as SelectingAction).getBoxDimensions();
|
||||
return (
|
||||
<div
|
||||
className={this.bem('__selector')}
|
||||
style={{
|
||||
top: dimensions.top,
|
||||
left: dimensions.left,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getActionForEvent(event: MouseEvent): AbstractAction {
|
||||
event.persist();
|
||||
const { diagramEngine } = this.props;
|
||||
const model = diagramEngine.getMouseElement(event);
|
||||
|
||||
const activateEvent: ActionFactoryActivationEvent = {
|
||||
selectedModel: model && model.model,
|
||||
selectedEntity: model && (model.element as HTMLElement),
|
||||
mouseEvent: event
|
||||
};
|
||||
|
||||
for (let factory of diagramEngine.getActionFactories().getFactories()) {
|
||||
if (factory.activate(activateEvent)) {
|
||||
return factory.generateAction(event);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const diagramEngine = this.props.diagramEngine;
|
||||
const diagramModel = diagramEngine.getDiagramModel();
|
||||
|
||||
return (
|
||||
<div
|
||||
{...this.getProps()}
|
||||
ref={this.ref}
|
||||
onWheel={event => {
|
||||
const allow = this.props.allowCanvasZoom == null ? true : this.props.allowCanvasZoom;
|
||||
if (allow) {
|
||||
event.stopPropagation();
|
||||
const oldZoomFactor = diagramModel.getZoomLevel() / 100;
|
||||
let scrollDelta = this.props.inverseZoom ? -event.deltaY : event.deltaY;
|
||||
//check if it is pinch gesture
|
||||
if (event.ctrlKey && scrollDelta % 1 !== 0) {
|
||||
/*Chrome and Firefox sends wheel event with deltaY that
|
||||
have fractional part, also `ctrlKey` prop of the event is true
|
||||
though ctrl isn't pressed
|
||||
*/
|
||||
scrollDelta /= 3;
|
||||
} else {
|
||||
scrollDelta /= 60;
|
||||
}
|
||||
if (diagramModel.getZoomLevel() + scrollDelta > 10) {
|
||||
diagramModel.setZoomLevel(diagramModel.getZoomLevel() + scrollDelta);
|
||||
}
|
||||
|
||||
const zoomFactor = diagramModel.getZoomLevel() / 100;
|
||||
|
||||
const boundingRect = event.currentTarget.getBoundingClientRect();
|
||||
const clientWidth = boundingRect.width;
|
||||
const clientHeight = boundingRect.height;
|
||||
// compute difference between rect before and after scroll
|
||||
const widthDiff = clientWidth * zoomFactor - clientWidth * oldZoomFactor;
|
||||
const heightDiff = clientHeight * zoomFactor - clientHeight * oldZoomFactor;
|
||||
// compute mouse coords relative to canvas
|
||||
const clientX = event.clientX - boundingRect.left;
|
||||
const clientY = event.clientY - boundingRect.top;
|
||||
|
||||
// compute width and height increment factor
|
||||
const xFactor = (clientX - diagramModel.getOffsetX()) / oldZoomFactor / clientWidth;
|
||||
const yFactor = (clientY - diagramModel.getOffsetY()) / oldZoomFactor / clientHeight;
|
||||
|
||||
diagramModel.setOffset(
|
||||
diagramModel.getOffsetX() - widthDiff * xFactor,
|
||||
diagramModel.getOffsetY() - heightDiff * yFactor
|
||||
);
|
||||
|
||||
this.forceUpdate();
|
||||
}
|
||||
}}
|
||||
onMouseDown={event => {
|
||||
// try and get an action for this event
|
||||
const action = this.getActionForEvent(event);
|
||||
if (action) {
|
||||
if (action instanceof AbstractMouseAction) {
|
||||
const selected = diagramEngine.getMouseElement(event);
|
||||
action.fireMouseDown({
|
||||
mouseEvent: event,
|
||||
selectedEntity: selected && (selected.element as HTMLElement),
|
||||
selectedModel: selected && selected.model
|
||||
});
|
||||
}
|
||||
this.startFiringAction(action);
|
||||
}
|
||||
document.addEventListener('mousemove', this.onMouseMove);
|
||||
document.addEventListener('mouseup', this.onMouseUp);
|
||||
}}>
|
||||
<LinkLayerWidget
|
||||
diagramEngine={diagramEngine}
|
||||
pointAdded={(point: PointModel, event) => {
|
||||
document.addEventListener('mousemove', this.onMouseMove);
|
||||
document.addEventListener('mouseup', this.onMouseUp);
|
||||
event.stopPropagation();
|
||||
|
||||
// TODO implement this better and more generic
|
||||
let action: MoveItemsAction = null;
|
||||
let fac: MoveItemsActionFactory = null;
|
||||
try {
|
||||
fac = diagramEngine.getActionFactories().getFactory<MoveItemsActionFactory>(MoveItemsActionFactory.NAME);
|
||||
} catch (e) {}
|
||||
if (fac) {
|
||||
action = fac.generateAction(event);
|
||||
action.fireMouseDown({
|
||||
selectedModel: point,
|
||||
selectedEntity: event.target as HTMLElement,
|
||||
mouseEvent: event
|
||||
});
|
||||
this.startFiringAction(action);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<NodeLayerWidget diagramEngine={diagramEngine} />
|
||||
{this.state.action instanceof SelectingAction && this.drawSelectionBox()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { DiagramEngine } from '../../DiagramEngine';
|
||||
import { LinkWidget } from '../LinkWidget';
|
||||
import * as _ from 'lodash';
|
||||
import { PointModel } from '../../models/PointModel';
|
||||
import { BaseWidget, BaseWidgetProps } from '../BaseWidget';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
export interface LinkLayerProps extends BaseWidgetProps {
|
||||
diagramEngine: DiagramEngine;
|
||||
pointAdded: (point: PointModel, event: MouseEvent) => any;
|
||||
}
|
||||
|
||||
export class LinkLayerWidget extends BaseWidget<LinkLayerProps> {
|
||||
constructor(props: LinkLayerProps) {
|
||||
super('srd-link-layer', props);
|
||||
}
|
||||
|
||||
render() {
|
||||
var diagramModel = this.props.diagramEngine.getDiagramModel();
|
||||
return (
|
||||
<svg
|
||||
{...this.getProps()}
|
||||
style={{
|
||||
transform:
|
||||
'translate(' +
|
||||
diagramModel.getOffsetX() +
|
||||
'px,' +
|
||||
diagramModel.getOffsetY() +
|
||||
'px) scale(' +
|
||||
diagramModel.getZoomLevel() / 100.0 +
|
||||
')'
|
||||
}}>
|
||||
{//only perform these actions when we have a diagram
|
||||
_.map(diagramModel.getLinks(), link => {
|
||||
return (
|
||||
<LinkWidget
|
||||
pointAdded={this.props.pointAdded}
|
||||
key={link.getID()}
|
||||
link={link}
|
||||
diagramEngine={this.props.diagramEngine}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { DiagramEngine } from '../../DiagramEngine';
|
||||
import * as _ from 'lodash';
|
||||
import { NodeWidget } from '../NodeWidget';
|
||||
import { NodeModel } from '../../models/NodeModel';
|
||||
import { BaseWidget, BaseWidgetProps } from '../BaseWidget';
|
||||
|
||||
export interface NodeLayerProps extends BaseWidgetProps {
|
||||
diagramEngine: DiagramEngine;
|
||||
}
|
||||
|
||||
export class NodeLayerWidget extends BaseWidget<NodeLayerProps> {
|
||||
constructor(props: NodeLayerProps) {
|
||||
super('srd-node-layer', props);
|
||||
}
|
||||
|
||||
render() {
|
||||
var diagramModel = this.props.diagramEngine.getDiagramModel();
|
||||
return (
|
||||
<div
|
||||
{...this.getProps()}
|
||||
style={{
|
||||
transform:
|
||||
'translate(' +
|
||||
diagramModel.getOffsetX() +
|
||||
'px,' +
|
||||
diagramModel.getOffsetY() +
|
||||
'px) scale(' +
|
||||
diagramModel.getZoomLevel() / 100.0 +
|
||||
')'
|
||||
}}>
|
||||
{_.map(diagramModel.getNodes(), (node: NodeModel) => {
|
||||
return <NodeWidget key={node.getID()} diagramEngine={this.props.diagramEngine} node={node} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
const config = require('../webpack.shared')(__dirname);
|
||||
module.exports = {
|
||||
...config,
|
||||
output: {
|
||||
...config.output,
|
||||
library: 'projectstorm/react-diagrams-core'
|
||||
}
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { DiagramEngine, DiagramWidget } from '@projectstorm/react-diagrams';
|
||||
|
||||
export interface BodyWidgetProps {
|
||||
engine: DiagramEngine;
|
||||
}
|
||||
|
||||
export class BodyWidget extends React.Component<BodyWidgetProps> {
|
||||
render() {
|
||||
return <DiagramWidget className="diagram-container" diagramEngine={this.props.engine} />;
|
||||
}
|
||||
}
|
14
package.json
14
package.json
@ -7,15 +7,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/projectstorm/react-diagrams.git"
|
||||
},
|
||||
"workspaces": [
|
||||
"lib-all",
|
||||
"lib-core",
|
||||
"lib-defaults",
|
||||
"lib-routing",
|
||||
"lib-geometry",
|
||||
"lib-demo-project",
|
||||
"lib-demo-gallery"
|
||||
],
|
||||
"workspaces": ["packages/*"],
|
||||
"keywords": [
|
||||
"web",
|
||||
"diagram",
|
||||
@ -33,11 +25,11 @@
|
||||
"watch": "yarn build:clean && lerna run watch --stream",
|
||||
"publish:dev": "yarn build:prod && lerna publish --force-publish --dist-tag=next",
|
||||
"publish:prod": "yarn build:prod && lerna publish --force-publish",
|
||||
"publish:storybook": "cd lib-demo-gallery && yarn storybook:build && ../node_modules/.bin/storybook-to-ghpages --existing-output-dir .out",
|
||||
"publish:storybook": "cd packages/diagrams-demo-gallery && yarn storybook:build && ../../node_modules/.bin/storybook-to-ghpages --existing-output-dir .out",
|
||||
"build:clean": "lerna run clean --stream",
|
||||
"test": "lerna run test --stream",
|
||||
"test:ci": "lerna run test --stream -- --runInBand --ci ",
|
||||
"pretty": "prettier --write \"lib-*/**/*.{ts,tsx,scss,js,jsx}\""
|
||||
"pretty": "prettier --write \"packages/**/*.{ts,tsx,scss,js,jsx}\""
|
||||
},
|
||||
"peerDependencies": {
|
||||
"closest": "^0.0.1",
|
||||
|
@ -8,10 +8,10 @@
|
||||
"url": "https://github.com/projectstorm/react-diagrams.git"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "../node_modules/.bin/start-storybook",
|
||||
"storybook:build": "../node_modules/.bin/build-storybook -c .storybook -o .out",
|
||||
"github": "../node_modules/.bin/storybook-to-ghpages",
|
||||
"test:run": "../node_modules/.bin/jest --no-cache",
|
||||
"start": "../../node_modules/.bin/start-storybook",
|
||||
"storybook:build": "../../node_modules/.bin/build-storybook -c .storybook -o .out",
|
||||
"github": "../../node_modules/.bin/storybook-to-ghpages",
|
||||
"test:run": "../../node_modules/.bin/jest --no-cache",
|
||||
"test": "yarn build && yarn test:run"
|
||||
},
|
||||
"keywords": [
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../tsconfig",
|
||||
"extends": "../../tsconfig",
|
||||
"include": [
|
||||
"./demos"
|
||||
]
|
@ -8,7 +8,7 @@
|
||||
"url": "https://github.com/projectstorm/react-diagrams.git"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "../node_modules/.bin/webpack-dev-server"
|
||||
"start": "../../node_modules/.bin/webpack-dev-server"
|
||||
},
|
||||
"keywords": [
|
||||
"web",
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 263 KiB |
13
packages/diagrams-demo-project/src/BodyWidget.tsx
Normal file
13
packages/diagrams-demo-project/src/BodyWidget.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { DiagramEngine } from '@projectstorm/react-diagrams';
|
||||
import {CanvasWidget} from "@projectstorm/react-canvas-core";
|
||||
|
||||
export interface BodyWidgetProps {
|
||||
engine: DiagramEngine;
|
||||
}
|
||||
|
||||
export class BodyWidget extends React.Component<BodyWidgetProps> {
|
||||
render() {
|
||||
return <CanvasWidget className="diagram-container" engine={this.props.engine} />;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { AbstractReactFactory } from '@projectstorm/react-diagrams';
|
||||
import { JSCustomNodeModel } from './JSCustomNodeModel';
|
||||
import { JSCustomNodeWidget } from './JSCustomNodeWidget';
|
||||
import {AbstractReactFactory} from "@projectstorm/react-canvas-core";
|
||||
|
||||
export class JSCustomNodeFactory extends AbstractReactFactory {
|
||||
constructor() {
|
@ -5,8 +5,12 @@ export class JSCustomNodeWidget extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="custom-node">
|
||||
<PortWidget engine={this.props.engine} port={this.props.node.getPort('in')} />
|
||||
<PortWidget engine={this.props.engine} port={this.props.node.getPort('out')} />
|
||||
<PortWidget engine={this.props.engine} port={this.props.node.getPort('in')}>
|
||||
<div className="circle-port"/>
|
||||
</PortWidget>
|
||||
<PortWidget engine={this.props.engine} port={this.props.node.getPort('out')}>
|
||||
<div className="circle-port"/>
|
||||
</PortWidget>
|
||||
<div className="custom-node-color" style={{ backgroundColor: this.props.node.color }} />
|
||||
</div>
|
||||
);
|
@ -1,9 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { AbstractReactFactory } from '@projectstorm/react-diagrams';
|
||||
import { TSCustomNodeModel } from './TSCustomNodeModel';
|
||||
import { TSCustomNodeWidget } from './TSCustomNodeWidget';
|
||||
import {AbstractReactFactory} from "@projectstorm/react-canvas-core";
|
||||
import {DiagramEngine} from "@projectstorm/react-diagrams-core";
|
||||
|
||||
export class TSCustomNodeFactory extends AbstractReactFactory<TSCustomNodeModel> {
|
||||
export class TSCustomNodeFactory extends AbstractReactFactory<TSCustomNodeModel, DiagramEngine> {
|
||||
constructor() {
|
||||
super('ts-custom-node');
|
||||
}
|
||||
@ -13,6 +14,6 @@ export class TSCustomNodeFactory extends AbstractReactFactory<TSCustomNodeModel>
|
||||
}
|
||||
|
||||
generateReactWidget(event): JSX.Element {
|
||||
return <TSCustomNodeWidget engine={this.engine} node={event.model} />;
|
||||
return <TSCustomNodeWidget engine={this.engine as DiagramEngine} node={event.model} />;
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { DiagramEngine, NodeModel, DefaultPortModel, BaseModelOptions } from '@projectstorm/react-diagrams';
|
||||
import { DiagramEngine, NodeModel, DefaultPortModel } from '@projectstorm/react-diagrams';
|
||||
import {BaseModelOptions} from "@projectstorm/react-canvas-core";
|
||||
|
||||
export interface TSCustomNodeModelOptions extends BaseModelOptions {
|
||||
color?: string;
|
@ -1,13 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import { DiagramEngine, PortWidget } from '@projectstorm/react-diagrams-core';
|
||||
import { TSCustomNodeModel } from './TSCustomNodeModel';
|
||||
import {DiagramEngine, PortWidget} from '@projectstorm/react-diagrams-core';
|
||||
import {TSCustomNodeModel} from './TSCustomNodeModel';
|
||||
|
||||
export interface TSCustomNodeWidgetProps {
|
||||
node: TSCustomNodeModel;
|
||||
engine: DiagramEngine;
|
||||
}
|
||||
|
||||
export interface TSCustomNodeWidgetState {}
|
||||
export interface TSCustomNodeWidgetState {
|
||||
}
|
||||
|
||||
export class TSCustomNodeWidget extends React.Component<TSCustomNodeWidgetProps, TSCustomNodeWidgetState> {
|
||||
constructor(props: TSCustomNodeWidgetProps) {
|
||||
@ -18,9 +19,13 @@ export class TSCustomNodeWidget extends React.Component<TSCustomNodeWidgetProps,
|
||||
render() {
|
||||
return (
|
||||
<div className="custom-node">
|
||||
<PortWidget engine={this.props.engine} port={this.props.node.getPort('in')} />
|
||||
<PortWidget engine={this.props.engine} port={this.props.node.getPort('out')} />
|
||||
<div className="custom-node-color" style={{ backgroundColor: this.props.node.color }} />
|
||||
<PortWidget engine={this.props.engine} port={this.props.node.getPort('in')}>
|
||||
<div className="circle-port"/>
|
||||
</PortWidget>
|
||||
<PortWidget engine={this.props.engine} port={this.props.node.getPort('out')}>
|
||||
<div className="circle-port"/>
|
||||
</PortWidget>
|
||||
<div className="custom-node-color" style={{backgroundColor: this.props.node.color}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -34,3 +34,16 @@ html, body, #application{
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.circle-port{
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: 2px;
|
||||
border-radius: 4px;
|
||||
background: darkgray;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.circle-port:hover{
|
||||
background: mediumpurple;
|
||||
}
|
@ -12,7 +12,7 @@ import { BodyWidget } from './BodyWidget';
|
||||
const engine = createEngine();
|
||||
|
||||
// register the two engines
|
||||
engine.getNodeFactories().registerFactory(new JSCustomNodeFactory());
|
||||
engine.getNodeFactories().registerFactory(new JSCustomNodeFactory() as any);
|
||||
engine.getNodeFactories().registerFactory(new TSCustomNodeFactory());
|
||||
|
||||
// create a diagram model
|
||||
@ -36,7 +36,7 @@ model.addAll(node1, node2, link1);
|
||||
//####################################################
|
||||
|
||||
// install the model into the engine
|
||||
engine.setDiagramModel(model);
|
||||
engine.setModel(model);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
ReactDOM.render(<BodyWidget engine={engine} />, document.querySelector('#application'));
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@projectstorm/react-diagrams-geometry",
|
||||
"name": "@projectstorm/geometry",
|
||||
"version": "6.0.0-alpha.4.2",
|
||||
"author": "dylanvorster",
|
||||
"repository": {
|
||||
@ -8,8 +8,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist",
|
||||
"build": "../node_modules/.bin/webpack",
|
||||
"build:prod": "NODE_ENV=production ../node_modules/.bin/webpack"
|
||||
"build": "../../node_modules/.bin/webpack",
|
||||
"build:prod": "NODE_ENV=production ../../node_modules/.bin/webpack"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../tsconfig",
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationDir": "dist/@types"
|
@ -1,4 +1,4 @@
|
||||
const config = require('../webpack.shared')(__dirname);
|
||||
const config = require('../../webpack.shared')(__dirname);
|
||||
module.exports = {
|
||||
...config,
|
||||
output: {
|
21
packages/react-canvas-core/index.ts
Normal file
21
packages/react-canvas-core/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export * from "./src/CanvasEngine"
|
||||
export * from "./src/Toolkit"
|
||||
export * from "./src/entities/canvas/CanvasModel"
|
||||
|
||||
export * from "./src/core/AbstractFactory"
|
||||
export * from "./src/core/AbstractModelFactory"
|
||||
export * from "./src/core/AbstractReactFactory"
|
||||
export * from "./src/core/BaseObserver"
|
||||
export * from "./src/core/FactoryBank"
|
||||
|
||||
export * from "./src/core-models/BaseEntity"
|
||||
export * from "./src/core-models/BaseModel"
|
||||
export * from "./src/core-models/BasePositionModel"
|
||||
|
||||
export * from "./src/entities/canvas/CanvasModel"
|
||||
export * from "./src/entities/canvas/CanvasWidget"
|
||||
|
||||
export * from "./src/entities/layer/LayerModel"
|
||||
|
||||
export * from "./src/widgets/PeformanceWidget"
|
||||
export * from "./src/entities/layer/TransformLayerWidget"
|
39
packages/react-canvas-core/package.json
Normal file
39
packages/react-canvas-core/package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@projectstorm/react-canvas-core",
|
||||
"version": "6.0.0-alpha.4.2",
|
||||
"author": "dylanvorster",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/projectstorm/react-diagrams.git"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist",
|
||||
"build": "../../node_modules/.bin/webpack",
|
||||
"build:prod": "NODE_ENV=production ../../node_modules/.bin/webpack"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"web",
|
||||
"diagram",
|
||||
"diagrams",
|
||||
"react",
|
||||
"typescript",
|
||||
"flowchart",
|
||||
"simple",
|
||||
"links",
|
||||
"nodes"
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/@types/index",
|
||||
"dependencies": {
|
||||
"@projectstorm/geometry": "^6.0.0-alpha.4.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"closest": "^0.0.1",
|
||||
"lodash": "4.*",
|
||||
"react": "16.*"
|
||||
},
|
||||
"gitHead": "bb878657ba0c2f81764f32901fd96158a0f8352e"
|
||||
}
|
120
packages/react-canvas-core/src/CanvasEngine.ts
Normal file
120
packages/react-canvas-core/src/CanvasEngine.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import {CanvasModel} from "./entities/canvas/CanvasModel";
|
||||
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 {BaseModel} from "./core-models/BaseModel";
|
||||
import {Point} from "@projectstorm/geometry";
|
||||
import {ActionEventBus} from "./core-actions/ActionEventBus";
|
||||
import {ZoomCanvasAction} from "./actions/ZoomCanvasAction";
|
||||
import {DeleteItemsAction} from "./actions/DeleteItemsAction";
|
||||
|
||||
export interface CanvasEngineListener extends BaseListener {
|
||||
canvasReady?(): void;
|
||||
|
||||
repaintCanvas?(): void;
|
||||
|
||||
rendered?(): void;
|
||||
}
|
||||
|
||||
export class CanvasEngine<L extends CanvasEngineListener = CanvasEngineListener, M extends CanvasModel = CanvasModel> extends BaseObserver<L> {
|
||||
|
||||
protected model: M;
|
||||
protected layerFactories: FactoryBank<AbstractReactFactory<LayerModel>>;
|
||||
protected canvas: HTMLDivElement;
|
||||
protected eventBus: ActionEventBus;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.model = null;
|
||||
this.eventBus = new ActionEventBus(this);
|
||||
this.layerFactories = new FactoryBank();
|
||||
|
||||
this.registerFactoryBank(this.layerFactories);
|
||||
this.eventBus.registerAction(new ZoomCanvasAction());
|
||||
this.eventBus.registerAction(new DeleteItemsAction());
|
||||
}
|
||||
|
||||
getRelativeMousePoint(event): Point {
|
||||
var point = this.getRelativePoint(event.clientX, event.clientY);
|
||||
return new Point(
|
||||
(point.x - this.model.getOffsetX()) / (this.model.getZoomLevel() / 100.0),
|
||||
(point.y - this.model.getOffsetY()) / (this.model.getZoomLevel() / 100.0)
|
||||
);
|
||||
}
|
||||
|
||||
getRelativePoint(x, y): Point {
|
||||
var canvasRect = this.canvas.getBoundingClientRect();
|
||||
return new Point(x - canvasRect.left, y - canvasRect.top);
|
||||
}
|
||||
|
||||
registerFactoryBank(factory: FactoryBank) {
|
||||
factory.registerListener({
|
||||
factoryAdded: event => {
|
||||
event.factory.setDiagramEngine(this);
|
||||
},
|
||||
factoryRemoved: event => {
|
||||
event.factory.setDiagramEngine(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getActionEventBus() {
|
||||
return this.eventBus;
|
||||
}
|
||||
|
||||
getLayerFactories() {
|
||||
return this.layerFactories;
|
||||
}
|
||||
|
||||
getFactoryForLayer<F extends AbstractReactFactory<LayerModel>>(layer: LayerModel) {
|
||||
if (typeof layer === 'string') {
|
||||
return this.layerFactories.getFactory(layer);
|
||||
}
|
||||
return this.layerFactories.getFactory(layer.getType());
|
||||
}
|
||||
|
||||
setModel(model: M) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
getModel(): M {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
repaintCanvas() {
|
||||
this.iterateListeners(listener => {
|
||||
if (listener.repaintCanvas) {
|
||||
listener.repaintCanvas();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setCanvas(canvas?: HTMLDivElement) {
|
||||
if (this.canvas !== canvas) {
|
||||
this.canvas = canvas;
|
||||
if (canvas) {
|
||||
this.fireEvent({}, 'canvasReady');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCanvas(){
|
||||
return this.canvas;
|
||||
}
|
||||
|
||||
getMouseElement(event: MouseEvent): { model: BaseModel; element: Element } {
|
||||
return null;
|
||||
}
|
||||
|
||||
zoomToFit() {
|
||||
const xFactor = this.canvas.clientWidth / this.canvas.scrollWidth;
|
||||
const yFactor = this.canvas.clientHeight / this.canvas.scrollHeight;
|
||||
const zoomFactor = xFactor < yFactor ? xFactor : yFactor;
|
||||
|
||||
this.model.setZoomLevel(this.model.getZoomLevel() * zoomFactor);
|
||||
this.model.setOffset(0, 0);
|
||||
this.repaintCanvas();
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import * as closest from 'closest';
|
||||
import { PointModel } from './models/PointModel';
|
||||
|
||||
export class Toolkit {
|
||||
static TESTING: boolean = false;
|
||||
@ -23,9 +22,6 @@ export class Toolkit {
|
||||
|
||||
/**
|
||||
* Finds the closest element as a polyfill
|
||||
*
|
||||
* @param {Element} element [description]
|
||||
* @param {string} selector [description]
|
||||
*/
|
||||
public static closest(element: Element, selector: string) {
|
||||
if (document.body.closest) {
|
||||
@ -33,8 +29,4 @@ export class Toolkit {
|
||||
}
|
||||
return closest(element, selector);
|
||||
}
|
||||
|
||||
public static generateLinePath(firstPoint: PointModel, lastPoint: PointModel): string {
|
||||
return `M${firstPoint.getX()},${firstPoint.getY()} L ${lastPoint.getX()},${lastPoint.getY()}`;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user