flow-graph-designer
Version:
A React component capable of flow graph designer.
833 lines (775 loc) • 30.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _propTypes = require("prop-types");
var _propTypes2 = _interopRequireDefault(_propTypes);
var _react = require("react");
var _react2 = _interopRequireDefault(_react);
var _reactDom = require("react-dom");
var _constants = require("../constants");
var _utils = require("../utils");
var _recycle = require("./recycle");
var _recycle2 = _interopRequireDefault(_recycle);
var _workspace = require("./workspace.less");
var _workspace2 = _interopRequireDefault(_workspace);
var _stateshot = require("stateshot");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var UNEXPAND_STATE_ID_PREFIX = "unexpand-state-id-";
var Workspace = function (_React$Component) {
_inherits(Workspace, _React$Component);
function Workspace(props) {
_classCallCheck(this, Workspace);
var _this = _possibleConstructorReturn(this, (Workspace.__proto__ || Object.getPrototypeOf(Workspace)).call(this, props));
_this.state = {};
_this.handleDragOver = _this.handleDragOver.bind(_this);
_this.handleDrop = _this.handleDrop.bind(_this);
_this.renderLine = _this.renderLine.bind(_this);
_this.renderNode = _this.renderNode.bind(_this);
_this.handlePaste = _this.handlePaste.bind(_this);
_this.handleCopy = _this.handleCopy.bind(_this);
_this.handleFocus = _this.handleFocus.bind(_this);
_this.handleBlur = _this.handleBlur.bind(_this);
_this.handleKeyUp = _this.handleKeyUp.bind(_this);
_this.handleClearState = _this.handleClearState.bind(_this);
_this.handleUpdateData = _this.handleUpdateData.bind(_this);
_this.history = new _stateshot.History();
return _this;
}
_createClass(Workspace, [{
key: "handleUpdateData",
value: function handleUpdateData(data) {
this.setState({ data: data });
this.history.pushSync(data);
}
}, {
key: "handleKeyUp",
value: function handleKeyUp(event) {
var _this2 = this;
if (event.key === "Delete") {
var _state = this.state,
data = _state.data,
currentId = _state.currentId;
if (!currentId) {
return;
}
var sourceParentNode = (0, _utils.getParentNodeById)(data, currentId);
var sourceIndex = sourceParentNode.children.findIndex(function (x) {
return x.id === currentId;
});
var _getNewFlowByDel = (0, _utils.getNewFlowByDel)({
config: data,
sourceId: currentId
}),
flow = _getNewFlowByDel.flow,
nodes = _getNewFlowByDel.nodes;
if (this.props.onChange && typeof this.props.onChange === "function") {
this.props.onChange({
data: flow,
detail: {
action: "del",
position: { id: sourceParentNode.id, index: sourceIndex },
nodes: nodes
}
});
}
this.handleClearState();
this.setState({ currentId: null });
this.handleUpdateData(flow);
return;
}
if (event.ctrlKey) {
switch (event.key) {
case "z":
(function () {
var data = _this2.history.undo().get();
_this2.setState({ data: data });
if (_this2.props.onChange && typeof _this2.props.onChange === "function") {
_this2.props.onChange({
data: data,
detail: {
action: "undo"
}
});
}
})();
break;
case "y":
(function () {
var data = _this2.history.redo().get();
_this2.setState({ data: data });
if (_this2.props.onChange && typeof _this2.props.onChange === "function") {
_this2.props.onChange({
data: data,
detail: {
action: "undo"
}
});
}
})();
break;
}
}
}
}, {
key: "handleCopy",
value: function handleCopy(event) {
var _state2 = this.state,
data = _state2.data,
currentId = _state2.currentId;
var node = (0, _utils.getNodeById)(data, currentId);
var text = JSON.stringify(node);
if (text) {
event.clipboardData.setData("text/plain", "flow-graph-designer:" + text);
}
event.preventDefault();
}
}, {
key: "handlePaste",
value: function handlePaste(event) {
var clipboardData = event.clipboardData;
if (!(clipboardData && clipboardData.items)) {
return;
}
var text = clipboardData.getData("text/plain");
if (text.startsWith("flow-graph-designer:")) {
var _state3 = this.state,
prevData = _state3.data,
currentId = _state3.currentId;
var nodeStr = text.substring("flow-graph-designer:".length);
var node = function () {
try {
var value = JSON.parse(nodeStr);
return value;
} catch (e) {
return null;
}
}();
if (!node) {
return;
}
var _ref = function () {
var unPasteResult = { containerId: null, containerIndex: null };
if (!currentId && node.type !== "case") {
// 如果没有当前节点,粘贴到根节点内部最后面的位置
return {
containerId: "root",
containerIndex: prevData.children.length
};
}
var currentNode = (0, _utils.getNodeById)(prevData, currentId);
// 粘贴到内的情况
if (currentNode.type === "loop" && node.type !== "case" || currentNode.type === "case" && node.type !== "case" || currentNode.type === "switch" && node.type === "case") {
return {
containerId: currentId,
containerIndex: currentNode.children.length
};
}
if (node.type === "case" && currentNode.type !== "case") {
return unPasteResult;
}
var containerNode = (0, _utils.getParentNodeById)(prevData, currentId);
var containerIndex = 1 + containerNode.children.findIndex(function (x) {
return x.id === currentId;
});
return { containerId: containerNode.id, containerIndex: containerIndex };
}(),
containerId = _ref.containerId,
containerIndex = _ref.containerIndex;
if (!containerId) {
return;
}
var _getNewFlowByPaste = (0, _utils.getNewFlowByPaste)({
config: prevData,
sourceNode: node,
containerId: containerId,
containerIndex: containerIndex
}),
data = _getNewFlowByPaste.data,
copyDetail = _getNewFlowByPaste.copyDetail;
var sourceParentNode = (0, _utils.getParentNodeById)(data, node.id);
var sourceIndex = sourceParentNode.children.findIndex(function (x) {
return x.id === node.id;
});
if (this.props.onChange && typeof this.props.onChange === "function") {
this.props.onChange({
data: data,
detail: {
action: "paste",
position: { id: sourceParentNode.id, index: sourceIndex },
position2: { id: containerId, index: containerIndex },
nodes: copyDetail.map(function (x) {
return x.from;
}),
nodes2: copyDetail.map(function (x) {
return x.to;
})
}
});
}
this.handleClearState();
this.handleUpdateData(data);
return;
}
}
}, {
key: "handleFocus",
value: function handleFocus() {
document.addEventListener("copy", this.handleCopy);
document.addEventListener("paste", this.handlePaste);
document.addEventListener("keyup", this.handleKeyUp);
}
}, {
key: "handleBlur",
value: function handleBlur() {
document.removeEventListener("copy", this.handleCopy);
document.removeEventListener("paste", this.handlePaste);
document.removeEventListener("keyup", this.handleKeyUp);
}
}, {
key: "handleClick",
value: function handleClick(id) {
var _this3 = this;
return function (e) {
e.stopPropagation();
_this3.setState({
currentId: id
});
if (_this3.props.onClick && typeof _this3.props.onClick === "function") {
_this3.props.onClick(id);
}
};
}
}, {
key: "handleClickExpandIcon",
value: function handleClickExpandIcon(id) {
var _this4 = this;
return function (e) {
e.stopPropagation();
var stateId = "" + UNEXPAND_STATE_ID_PREFIX + id;
_this4.setState(_defineProperty({}, stateId, !_this4.state[stateId]));
};
}
}, {
key: "handleDragStart",
value: function handleDragStart(id) {
var _this5 = this;
return function (e) {
e.dataTransfer.setData("dragId", id);
e.dataTransfer.setDragImage(e.target, 10, 10);
e.stopPropagation();
var dragNode = (0, _utils.getNodeById)(_this5.state.data, id);
var isDragCase = dragNode.type === "case";
_this5.setState({
dragId: id,
isDragCase: isDragCase
});
};
}
}, {
key: "handleDragOver",
value: function handleDragOver(_ref2) {
var _this6 = this;
var containerId = _ref2.containerId,
containerIndex = _ref2.containerIndex;
return function (e) {
e.preventDefault();
e.stopPropagation();
if (_this6.state.containerId !== containerId || _this6.state.containerInedx !== containerIndex) {
_this6.setState({
containerId: containerId,
containerIndex: containerIndex
});
}
};
}
}, {
key: "handleDragLeave",
value: function handleDragLeave(_ref3) {
var _this7 = this;
var containerId = _ref3.containerId,
containerIndex = _ref3.containerIndex;
return function (e) {
e.stopPropagation();
if (_this7.state.containerId === containerId && _this7.state.containerIndex === containerIndex) {
_this7.setState({
containerId: null,
containerIndex: null
});
}
};
}
}, {
key: "handleClearState",
value: function handleClearState() {
this.setState({
containerId: null,
containerIndex: null,
dragId: null,
isDragCase: false
});
}
}, {
key: "handleDrop",
value: function handleDrop(e) {
e.stopPropagation();
var sourceId = e.dataTransfer.getData("dragId");
var nodeName = e.dataTransfer.getData("nodeName");
var method = e.dataTransfer.getData("method");
var _state4 = this.state,
containerId = _state4.containerId,
containerIndex = _state4.containerIndex;
// 如果是从图标栏拖拽节点到垃圾箱,取消拖拽动作并返回
if (method === "new" && containerId === "recycle") {
this.handleClearState();
return;
}
// 创建新的节点
if (method === "new") {
var type = e.dataTransfer.getData("type");
var _action = e.dataTransfer.getData("action");
var node = (0, _utils.getNewNode)(type, _action, nodeName, (0, _utils.getNewIdFunc)(this.state.data));
// 在流程配置中增加新建的节点
var _getNewFlowByAdd = (0, _utils.getNewFlowByAdd)({
config: this.state.data,
node: node,
containerId: containerId,
containerIndex: containerIndex
}),
flow = _getNewFlowByAdd.flow,
nodes = _getNewFlowByAdd.nodes;
if (this.props.onChange && typeof this.props.onChange === "function") {
this.props.onChange({
data: flow,
detail: {
action: "add",
position: { id: containerId, index: containerIndex },
nodes: nodes
}
});
}
this.handleClearState();
this.handleUpdateData(flow);
return;
}
var prevData = this.state.data;
var sourceParentNode = (0, _utils.getParentNodeById)(prevData, sourceId);
var sourceIndex = sourceParentNode.children.findIndex(function (x) {
return x.id === sourceId;
});
if (containerId === "recycle") {
var _getNewFlowByDel2 = (0, _utils.getNewFlowByDel)({
config: this.state.data,
sourceId: sourceId
}),
_flow = _getNewFlowByDel2.flow,
_nodes = _getNewFlowByDel2.nodes;
if (this.props.onChange && typeof this.props.onChange === "function") {
this.props.onChange({
data: _flow,
detail: {
action: "del",
position: { id: sourceParentNode.id, index: sourceIndex },
nodes: _nodes
}
});
}
this.handleClearState();
this.setState({ currentId: null });
this.handleUpdateData(_flow);
return;
}
var action = e.nativeEvent.ctrlKey || e.nativeEvent.metaKey ? "copy" : "move";
if (action === "move") {
// 如果新的位置和当前位置没有变化,取消行动
if (sourceParentNode.id === containerId && (containerIndex === sourceIndex || containerIndex === sourceIndex + 1)) {
this.handleClearState();
return;
}
}
if (action === "copy") {
var _getNewFlowByCopy = (0, _utils.getNewFlowByCopy)({
config: prevData,
sourceId: sourceId,
containerId: containerId,
containerIndex: containerIndex
}),
data = _getNewFlowByCopy.data,
copyDetail = _getNewFlowByCopy.copyDetail;
if (this.props.onChange && typeof this.props.onChange === "function") {
this.props.onChange({
data: data,
detail: {
action: "copy",
position: { id: sourceParentNode.id, index: sourceIndex },
position2: { id: containerId, index: containerIndex },
nodes: copyDetail.map(function (x) {
return x.from;
}),
nodes2: copyDetail.map(function (x) {
return x.to;
})
}
});
}
this.handleClearState();
this.handleUpdateData(data);
return;
} else if (action === "move") {
var _getNewFlowByMove = (0, _utils.getNewFlowByMove)({
config: prevData,
sourceId: sourceId,
containerId: containerId,
containerIndex: containerIndex
}),
_flow2 = _getNewFlowByMove.flow,
_nodes2 = _getNewFlowByMove.nodes;
if (_flow2 === prevData) {
this.handleClearState();
return;
}
if (this.props.onChange && typeof this.props.onChange === "function") {
this.props.onChange({
data: _flow2,
detail: {
action: "move",
position: { id: sourceParentNode.id, index: sourceIndex },
position2: { id: containerId, index: containerIndex },
nodes: _nodes2
}
});
}
this.handleClearState();
this.handleUpdateData(_flow2);
return;
}
}
}, {
key: "renderLine",
value: function renderLine(_ref4) {
var containerId = _ref4.containerId,
containerIndex = _ref4.containerIndex,
hasArrow = _ref4.hasArrow,
style = _ref4.style;
var arrowClassName = hasArrow ? "arrow-line" : "";
var dragoverClassName = this.state.containerId === containerId && this.state.containerIndex === containerIndex ? "dragover" : "";
return _react2.default.createElement(
"div",
{
className: "flow-line " + arrowClassName + " " + dragoverClassName,
key: "line-" + containerId + ":{containerIndex}",
style: style,
onDragOver: this.handleDragOver({ containerId: containerId, containerIndex: containerIndex }),
onDragLeave: this.handleDragLeave({ containerId: containerId, containerIndex: containerIndex }),
onDrop: this.handleDrop },
_react2.default.createElement("div", { className: "line" }),
_react2.default.createElement("div", { className: "rect" }),
_react2.default.createElement("div", { className: "line" }),
hasArrow && _react2.default.createElement("div", { className: "arrow" })
);
}
}, {
key: "renderHalfHeightLine",
value: function renderHalfHeightLine() {
return _react2.default.createElement(
"div",
{ className: "flow-line" },
_react2.default.createElement("div", { className: "line" })
);
}
}, {
key: "renderNode",
value: function renderNode(node) {
var _this8 = this;
var unexpandState = this.state["" + UNEXPAND_STATE_ID_PREFIX + node.id];
if (node.type === "loop") {
return _react2.default.createElement(
"div",
{
key: node.id,
onClick: this.handleClick(node.id),
className: (unexpandState ? "unexpand" : "") + " flow-node loop " + (this.state.dragId === node.id ? "draged" : "") + " " + (this.state.currentId === node.id ? "clicked" : ""),
draggable: "true",
onDragStart: this.handleDragStart(node.id) },
_react2.default.createElement(
"div",
{ className: "title", onClick: this.handleClickExpandIcon(node.id) },
_react2.default.createElement(
"div",
null,
_react2.default.createElement("img", { className: "expand-icon", src: _constants.image.expand }),
_react2.default.createElement("img", { className: "unexpand-icon", src: _constants.image.unexpand }),
_react2.default.createElement(
"span",
{ style: { verticalAlign: "top" } },
node.name
),
_react2.default.createElement("img", {
src: _constants.image.loop,
style: {
width: "18px",
height: "18px",
marginLeft: "4px",
marginTop: "2px"
}
})
)
),
this.renderLine({ containerId: node.id, containerIndex: 0 }),
_react2.default.createElement(
"div",
{ className: "flow-body" },
node.children.map(function (x, index) {
return [_this8.renderNode(x), _this8.renderLine({
containerId: node.id,
containerIndex: index + 1,
hasArrow: true
})];
})
)
);
} else if (node.type === "switch") {
return _react2.default.createElement(
"div",
{
key: node.id,
className: (unexpandState ? "unexpand" : "") + " flow-node switch " + (this.state.dragId === node.id ? "draged" : "") + " " + (this.state.currentId === node.id ? "clicked" : ""),
draggable: "true",
onClick: this.handleClick(node.id),
onDragStart: this.handleDragStart(node.id) },
_react2.default.createElement(
"div",
{ className: "title", onClick: this.handleClickExpandIcon(node.id) },
_react2.default.createElement(
"div",
null,
_react2.default.createElement("img", { className: "expand-icon", src: _constants.image.expand }),
_react2.default.createElement("img", { className: "unexpand-icon", src: _constants.image.unexpand }),
_react2.default.createElement(
"span",
{ style: { verticalAlign: "top" } },
node.name
),
_react2.default.createElement("img", {
src: _constants.image.switch,
style: {
width: "18px",
height: "18px",
marginLeft: "4px",
marginTop: "3px"
}
})
)
),
this.renderHalfHeightLine(),
_react2.default.createElement(
"div",
{ className: "switch-body" },
node.children.map(function (x, index) {
return [_react2.default.createElement(
"div",
{ className: "flow-node case", key: "flow-node-" + x.id },
_react2.default.createElement(
"div",
{ className: "flow-node-line-up" },
_react2.default.createElement("div", { className: "line left-div" }),
_react2.default.createElement("div", { className: "line right-div" })
),
_react2.default.createElement(
"div",
{ className: "flow-node-rect-wrap" },
_react2.default.createElement(
"div",
{
className: "flow-node-rect " + (_this8.state.dragId === x.id ? "draged" : "") + " " + (_this8.state.currentId === x.id ? "clicked" : ""),
draggable: "true",
onClick: _this8.handleClick(x.id),
onDragStart: _this8.handleDragStart(x.id),
onDragOver: _this8.handleDragOver({
containerId: node.id,
containerIndex: index + 1,
id: x.id
}),
onDragLeave: _this8.handleDragLeave({
containerId: node.id,
containerIndex: index + 1
}),
onDrop: _this8.handleDrop },
_react2.default.createElement(
"div",
{ className: "title" },
_react2.default.createElement(
"div",
null,
x.name
),
_react2.default.createElement("img", {
src: _constants.image.case,
style: { width: "18px", height: "18px" }
})
),
_this8.renderLine({
containerId: x.id,
containerIndex: 0,
style: {
flexGrow: x.children && x.children.length ? 0 : 1
}
}),
_react2.default.createElement(
"div",
{ className: "flow-body" },
x.children && x.children.map(function (y, yIndex) {
return [_this8.renderNode(y), _this8.renderLine({
containerId: x.id,
containerIndex: yIndex + 1,
hasArrow: true
})];
})
)
),
_this8.state.containerId === node.id && _this8.state.containerIndex === index + 1 && _this8.state.isDragCase && _this8.state.dragId !== x.id && _react2.default.createElement("div", { className: "insert-rect-space" })
),
_react2.default.createElement(
"div",
{ className: "flow-node-line-down" },
_react2.default.createElement("div", { className: "line left-div" }),
_react2.default.createElement("div", { className: "line right-div" })
)
)];
})
),
this.renderHalfHeightLine()
);
}
var color = (0, _utils.getColorByNode)(node, this.props.template);
return _react2.default.createElement(
"div",
{
key: node.id,
className: "flow-node normal " + (this.state.dragId === node.id ? "draged" : "") + " " + (this.state.currentId === node.id ? "clicked" : ""),
style: {
backgroundColor: color,
boxShadow: "0 0 0 2px white, 0 0 0 3px " + color
},
onClick: this.handleClick(node.id),
draggable: "true",
onDragStart: this.handleDragStart(node.id) },
node.name || "节点"
);
}
}, {
key: "render",
value: function render() {
var _this9 = this;
if (!this.state._has_getDerivedStateFromProps_func) {
console.error("Component flow-graph-designer need react with version 16.4 or above.");
return null;
}
if (!this.state.data) {
return null;
}
var theme = this.props.theme || "theme-1";
return _react2.default.createElement(
"div",
{
className: "flow-designer-workspace-wrap " + _workspace2.default.mainClass,
style: this.props.style || {},
ref: function ref(_ref5) {
_this9.ref = _ref5;
},
tabIndex: 0,
onClick: this.handleClearState,
onFocus: this.handleFocus,
onBlur: this.handleBlur },
window.recycleWrap && (0, _reactDom.createPortal)(_react2.default.createElement(_recycle2.default, {
visible: !!this.state.dragId,
onDragOver: this.handleDragOver({
containerId: "recycle",
containerIndex: null
}),
onDragLeave: this.handleDragLeave({
containerId: "recycle",
containerIndex: null
}),
onDrop: this.handleDrop
}), window.recycleWrap),
_react2.default.createElement(
"div",
{ className: "main " + theme },
_react2.default.createElement(
"div",
{ className: "flow-body root" },
_react2.default.createElement(
"div",
{ className: "node-begin" },
_react2.default.createElement("img", {
src: _constants.image.begin,
style: { width: "32px", height: "32px" }
})
),
this.renderLine({ containerId: "root", containerIndex: 0 }),
this.state.data.children.map(function (x, index) {
var hasArrow = index !== _this9.state.data.children.length - 1;
return [_this9.renderNode(x), _this9.renderLine({
containerId: "root",
containerIndex: index + 1,
hasArrow: hasArrow
})];
}),
_react2.default.createElement(
"div",
{ className: "node-end" },
_react2.default.createElement("img", { src: _constants.image.end, style: { width: "32px", height: "32px" } })
)
)
)
);
}
}], [{
key: "getDerivedStateFromProps",
value: function getDerivedStateFromProps(nextProps, prevState) {
// render 函数将根据 state 中的 data 进行渲染
// state 中的 data 除了会被 props 更新外,也会被用户的拖拽操作设置
var data = function (_data) {
if (!_data) {
return { id: "root", children: [] };
}
if (!_data.children) {
return _extends({}, _data, {
children: []
});
}
return _data;
}(nextProps.data);
var dataFromProps = JSON.stringify(data);
if (!prevState.dataFromProps || dataFromProps !== prevState.dataFromProps) {
// 如果 props 中的 data 发生了变化,修改 state 中的 data
return {
_has_getDerivedStateFromProps_func: true,
dataFromProps: dataFromProps, // 记录每次从 props 获取到的 data 对象
data: data
};
}
return null;
}
}]);
return Workspace;
}(_react2.default.Component);
Workspace.propTypes = {
data: _propTypes2.default.shape({
id: _propTypes2.default.string,
children: _propTypes2.default.array
}),
template: _propTypes2.default.object,
theme: _propTypes2.default.string,
style: _propTypes2.default.object,
onClick: _propTypes2.default.func,
onChange: _propTypes2.default.func
};
exports.default = Workspace;