记一种拖拉拽编排流程的思路

记一种拖拉拽编排流程的思路

有这么一个场景,我们希望能在界面上编排流程,可以添加任意类型的节点,也可以编排节点之间的约束条件。拿采购流程举例,项目经理节点发起采购流程指向采购部门,如果金额在5W以下,采购部门直接评审结束;否则还要经过CEO审批。

想到了三种实现技术:

1、AntG6可视化组件

2、Angular官方material组件

3、原生Component

其中angular@angular/cdk/drag-drop组件用起来最方便。这里记录一下原生JS实现的思路。

原生Component实现思路

按示例,视图应该拆分成“网格画布”、“节点”、“连线”、“条件”四个组件。考虑可能需要添加各类组态效果——连线具有数据流向动画、点击会进行条件设置、各类节点约束条件多样等等,需要要利用继承、多态等特性。

基础类型只实现拖拉拽以及数据交换的API,其子类拓展各种交互效果。

所以基础类型的主要属性、函数大概就清晰了。

画布:具备节点集合和连线集合,以及节点和连线的渲染函数;当然,还有网格。

节点:具备坐标,以之为始的连线,以之为终的坐标;拖动开始、中、结束事件。

连线:起始节点坐标,终止节点坐标;条件。

条件:起始节点坐标,终止节点坐标。

为了达到添加实体自动渲染的效果,这里采用Proxy或者getter、setter做双向绑定。

连线采用SVGPATH实现,轨迹转换成贝塞尔曲线;条件采用SVGTEXT实现;连线上的数据流向采用SVGstroke-dasharray动画。

Demo代码

下面为代码草稿,效果图如下:

DragCanvas

class DragCanvasElement extends HTMLDivElement{
    constructor() {
        super();
        this.init();
    }

    init() {
        // var shadow = this.attachShadow({mode: 'open'});
        let canvas = document.createElement('div');
        let lines = document.createElementNS('http://www.w3.org/2000/svg','svg');
        canvas.setAttribute('class', 'canvas');
        lines.classList.add('point-line');
        lines.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
        lines.setAttribute('version', '1.1');
        lines.innerHTML = `
        <defs> 
            <marker markerWidth="5" markerHeight="5" viewBox="-6 -6 12 12" refX="5" refY="0" markerUnits="strokeWidth" orient="auto" id="marker">
                <polygon points="-2,0 -5,5 5,0 -5,-5" stroke="#4a90e2" fill="#4a90e2" stroke-width="1px"></polygon>
            </marker>
            <filter x="0" y="0" width="1" height="1" id="textBg">
                <feFlood flood-color="yellow"/>
                <feComposite in="SourceGraphic"/>
            </filter>
        </defs>`;// 连线的箭头,条件文本的背景
        canvas.appendChild(lines);
        this.append(canvas);
    }
}

class RuleDragCanvasElement extends DragCanvasElement {
    constructor() {
        super();
        this._ruleDragNodes = [];//节点集
        this._ruleDragLines = [];//线集合

        this.ruleDragNodes = this.watchRuleDragNodes();
        this.ruleDragLines = this.watchRuleDragLines();
    }

    // Proxy监听,画布里添加后增加对应的DOM元素
    watchRuleDragNodes() {
        let ctx = this;
        return new Proxy(this._ruleDragLines, {
            get(target, prop, receiver) {
                return Reflect.get(target, prop, receiver);
            },
            set(target, prop, value, receiver) {
                // do sth
                if(value.constructor == RuleDragNodeElement) {
                    ctx.append(value);
                }
                return Reflect.set(target,prop,value,receiver);
            }
        });
    }

    watchRuleDragLines() {
        let ctx = this;
        return new Proxy(this._ruleDragNodes, {
            get(target, prop, receiver) {
                return Reflect.get(target, prop, receiver);
            },
            set(target, prop, value, receiver) {
                // do sth
                if(value.constructor == RuleDragLineElement) {
                    ctx.querySelector('svg').append(value.querySelector('g'));
                }
                return Reflect.set(target,prop,value,receiver)
            }
        });
    }
}

customElements.define('rule-drag-canvas', RuleDragCanvasElement, { extends: 'div' });

DragNode

const DRAG_NODE_STYLE_RULES = `
.canvas-node {
	border-radius: 4px;
    width: 152px;
    height: 36px;
    position: absolute;
    top: 0;
    left: 0;
	background: #c2185b;
	text-align: center;
    line-height: 36px;
	user-select: none;
	color: #fff;
}
`;

class DragNodeElement extends HTMLElement {
    constructor() {
        super();
        this.id = new Date().getTime();
        this._point = {x:0, y:0}; //实际坐标,与movePoint区别;如拖到不合适的位置需要回到原来位置。
        this._movPoint = this.point; //拖动过程,以这个坐标显示
        this.events = {};
        this.relationLine = { //关联的线段
            prev: [],
            next: []
        }

        this._draggable = true;
        this._name = 'xxxx';
        this.init();
        this._ondragstart();
		this._ondrag();
		this._ondragend();
    }

    get draggable() {
        return this._draggable;
    }
    set draggable(v) {
        this._draggable = v;
    }

    get name() {
        return this._name;
    }
    set name(v) {
        this._name = v;
        if(this.shadowRoot) {
            this.shadowRoot.querySelector('.canvas-node').textContent = v;
        }
    }

    get point() {
        return this._point;
    }
    set point(v) {
        this._point = v;
        this.movPoint = v;
    }

    get movPoint() {
        return this._movPoint;
    }
    set movPoint(v) {
        this._movPoint = v;
        if(v) {
            this._getNodeEl().setAttribute('style', `transform: translate3d(${v.x}px, ${v.y}px, 0px)`);
        }
    }

    init() {
        let shadow = this.attachShadow({mode: 'open'});
        let style = document.createElement('style');
        style.textContent = DRAG_NODE_STYLE_RULES;
        shadow.appendChild(style);

        let node = document.createElement('div');
        node.classList.add('canvas-node');
        node.textContent = this.name
        node.setAttribute('draggable', this.draggable);
        shadow.appendChild(node);
    }

    onDragStarted(fn) {
		if(!this.events['dragStarted']) {
			this.events['dragStarted'] = [];
		}
		this.events['dragStarted'].push(fn);
	}
	
	onDragMoved(fn) {
		if(!this.events['dragMoved']) {
			this.events['dragMoved'] = [];
		}
		this.events['dragMoved'].push(fn);
	}
	
	onDragEnded(fn) {
		if(!this.events['dragEnded']) {
			this.events['dragEnded'] = [];
		}
		this.events['dragEnded'].push(fn);
	}

    _ondragstart() {
		let ctx = this;
		this.addEventListener('dragstart', function(e) {
			console.log('dragstart', e);
			e.dataTransfer.setDragImage(e.target.cloneNode(), 0, 0);
			ctx.startPos = {x: e.pageX, y: e.pageY};
			if(ctx.events['dragStarted']) {
				ctx.events['dragStarted'].forEach(fn=> fn.apply(ctx, arguments));
			}
		});
	}
	
	_ondrag() {
		let ctx = this;
		this.addEventListener('drag', function(e) {
			let movePos = {x: e.pageX, y: e.pageY};
			ctx.movPoint = {x: movePos.x - ctx.startPos.x + ctx.point.x, y: movePos.y - ctx.startPos.y + ctx.point.y };
            ctx.resetRelationLinePoint(ctx, ctx.movPoint);
			//边界判定 drag在鼠标划出浏览器就无法监听了。
			if(ctx.events['dragMoved']) {
				ctx.events['dragMoved'].forEach(fn=> fn.apply(ctx, arguments));
			}
		});
	}
	
	_ondragend() {
		let ctx = this;
		this.addEventListener('dragend', function(e) {
			console.log('dragEnd', e);
			let endPos = {x: e.pageX, y: e.pageY};
			ctx.movPoint = {x: endPos.x - ctx.startPos.x + ctx.point.x, y: endPos.y - ctx.startPos.y + ctx.point.y };
			ctx.point = ctx.movPoint;
            ctx.resetRelationLinePoint(ctx, ctx.point);
			if(ctx.events['dragEnded']) {
				ctx.events['dragEnded'].forEach(fn=> fn.apply(ctx, arguments));
			}
		});
	}

    _getNodeEl() {
        if(!this.shadowRoot) {
            return null;
        }
        return this.shadowRoot.querySelector('.canvas-node');
    }

    resetRelationLinePoint(ctx, point) {
        ctx.relationLine.prev.forEach(item=> {
            item.setStartPoint(point);
        });
        ctx.relationLine.next.forEach(item=> {
            item.setEndPoint(point);
        });
    }

}

class RuleDragNodeElement extends DragNodeElement {
    constructor() {
        super();
        
    }
}

customElements.define('rule-drag-node', RuleDragNodeElement);

DragLine

const DRAG_NODE_STYLE_RULES = `
.canvas-node {
	border-radius: 4px;
    width: 152px;
    height: 36px;
    position: absolute;
    top: 0;
    left: 0;
	background: #c2185b;
	text-align: center;
    line-height: 36px;
	user-select: none;
	color: #fff;
}
`;

class DragNodeElement extends HTMLElement {
    constructor() {
        super();
        this.id = new Date().getTime();
        this._point = {x:0, y:0}; //实际坐标,与movePoint区别;如拖到不合适的位置需要回到原来位置。
        this._movPoint = this.point; //拖动过程,以这个坐标显示
        this.events = {};
        this.relationLine = { //关联的线段
            prev: [],
            next: []
        }

        this._draggable = true;
        this._name = 'xxxx';
        this.init();
        this._ondragstart();
		this._ondrag();
		this._ondragend();
    }

    get draggable() {
        return this._draggable;
    }
    set draggable(v) {
        this._draggable = v;
    }

    get name() {
        return this._name;
    }
    set name(v) {
        this._name = v;
        if(this.shadowRoot) {
            this.shadowRoot.querySelector('.canvas-node').textContent = v;
        }
    }

    get point() {
        return this._point;
    }
    set point(v) {
        this._point = v;
        this.movPoint = v;
    }

    get movPoint() {
        return this._movPoint;
    }
    set movPoint(v) {
        this._movPoint = v;
        if(v) {
            this._getNodeEl().setAttribute('style', `transform: translate3d(${v.x}px, ${v.y}px, 0px)`);
        }
    }

    init() {
        let shadow = this.attachShadow({mode: 'open'});
        let style = document.createElement('style');
        style.textContent = DRAG_NODE_STYLE_RULES;
        shadow.appendChild(style);

        let node = document.createElement('div');
        node.classList.add('canvas-node');
        node.textContent = this.name
        node.setAttribute('draggable', this.draggable);
        shadow.appendChild(node);
    }

    onDragStarted(fn) {
		if(!this.events['dragStarted']) {
			this.events['dragStarted'] = [];
		}
		this.events['dragStarted'].push(fn);
	}
	
	onDragMoved(fn) {
		if(!this.events['dragMoved']) {
			this.events['dragMoved'] = [];
		}
		this.events['dragMoved'].push(fn);
	}
	
	onDragEnded(fn) {
		if(!this.events['dragEnded']) {
			this.events['dragEnded'] = [];
		}
		this.events['dragEnded'].push(fn);
	}

    _ondragstart() {
		let ctx = this;
		this.addEventListener('dragstart', function(e) {
			console.log('dragstart', e);
			e.dataTransfer.setDragImage(e.target.cloneNode(), 0, 0);
			ctx.startPos = {x: e.pageX, y: e.pageY};
			if(ctx.events['dragStarted']) {
				ctx.events['dragStarted'].forEach(fn=> fn.apply(ctx, arguments));
			}
		});
	}
	
	_ondrag() {
		let ctx = this;
		this.addEventListener('drag', function(e) {
			let movePos = {x: e.pageX, y: e.pageY};
			ctx.movPoint = {x: movePos.x - ctx.startPos.x + ctx.point.x, y: movePos.y - ctx.startPos.y + ctx.point.y };
            ctx.resetRelationLinePoint(ctx, ctx.movPoint);
			//边界判定 drag在鼠标划出浏览器就无法监听了。
			if(ctx.events['dragMoved']) {
				ctx.events['dragMoved'].forEach(fn=> fn.apply(ctx, arguments));
			}
		});
	}
	
	_ondragend() {
		let ctx = this;
		this.addEventListener('dragend', function(e) {
			console.log('dragEnd', e);
			let endPos = {x: e.pageX, y: e.pageY};
			ctx.movPoint = {x: endPos.x - ctx.startPos.x + ctx.point.x, y: endPos.y - ctx.startPos.y + ctx.point.y };
			ctx.point = ctx.movPoint;
            ctx.resetRelationLinePoint(ctx, ctx.point);
			if(ctx.events['dragEnded']) {
				ctx.events['dragEnded'].forEach(fn=> fn.apply(ctx, arguments));
			}
		});
	}

    _getNodeEl() {
        if(!this.shadowRoot) {
            return null;
        }
        return this.shadowRoot.querySelector('.canvas-node');
    }

    resetRelationLinePoint(ctx, point) {
        ctx.relationLine.prev.forEach(item=> {
            item.setStartPoint(point);
        });
        ctx.relationLine.next.forEach(item=> {
            item.setEndPoint(point);
        });
    }

}

class RuleDragNodeElement extends DragNodeElement {
    constructor() {
        super();
        
    }
}

customElements.define('rule-drag-node', RuleDragNodeElement);

DragCond

class DragCondElement extends HTMLElement {
    constructor() {
        super();
        this._startPoint = {x:0,y:0};
        this._endPoint = {x:0,y:0};
        this._name = '';
        this.init();
    }

    get name() {
        return this._name;
    }
    set name(v) {
        this._name = v;
        this.text.textContent = v;
    }

    get startPoint() {
        return this._startPoint;
    }
    set startPoint(v) {
        this.setStartPoint(v);
    }
    get endPoint() {
        return this._endPoint;
    }
    set endPoint(v) {
        this.setEndPoint(v);
    }
    setStartPoint(v) {
        this._startPoint = v;
        if(v) {
            let pos = this.calcPos(this.startPoint, this.endPoint);
            console.log(v,pos);
            this.text.setAttribute('x', pos.x);
            this.text.setAttribute('y', pos.y);
        }
    }
    setEndPoint(v) {
        this._endPoint = v;
        if(v) {
            let pos = this.calcPos(this.startPoint, this.endPoint);
            this.text.setAttribute('x', pos.x);
            this.text.setAttribute('y', pos.y);
        }
    }

    init() {
        this.text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
        this.text.textContent = "hello Word!";
        this.text.setAttribute('filter', 'url(#textBg)');
        this.append(this.text);
    }

    calcPos(s, e) {
        // 限定条件块的宽高,计算的坐标要拉回一半长度
        return {
            x: (s.x + 146 + e.x - 60) / 2,
            y: (s.y + 18 + e.y + 38) / 2
        }
    }
}

class RuleDragCondElement extends DragCondElement {
    constructor() {
        super();
    }

    
}   

customElements.define('rule-drag-cond', RuleDragCondElement);

index.html

<!DOCTYPE html>
<html>
<head>
	<title>WebComp Drag</title>
	<link rel="stylesheet" href="./css/styles.css"></link>
	<script src="./js/DragCanvas.js"></script>
	<script src="./js/DragNode.js"></script>
	<script src="./js/DragLine.js"></script>
	<script src="./js/DragCond.js"></script>
</head>
<body>
</body>
<script>
	let ruleDragCanvas = new RuleDragCanvasElement();
	document.body.append(ruleDragCanvas);

	let dragNode1 = createNode('Node 1', {x: 0, y: 0});
	let dragNode2 = createNode('Node 2', {x: 300, y: 50});
	let dragNode3 = createNode('Node 3', {x: 200, y: 400});
	let dragNode4 = createNode('Node 4', {x: 600, y: 350});

	let ruleDragCond = new RuleDragCondElement();
	ruleDragCond.name = '条件'

	let line1 = lineTo(dragNode1, dragNode2);
	let line2 = lineTo(dragNode1, dragNode3, 'move', ruleDragCond);
	let line3 = lineTo(dragNode2, dragNode4);
	let line4 = lineTo(dragNode3, dragNode4, 'move');


	function createNode(name, point) {
		let dragNode = new RuleDragNodeElement();
		dragNode.name = name;
		dragNode.point = point;
		ruleDragCanvas.ruleDragNodes.push(dragNode);
		return dragNode;
	}

	function lineTo(node1, node2, animate, cond) {
		let ruleDragLine = new RuleDragLineElement();
		if(animate) {
			ruleDragLine.animateMove = animate;
		}
		if(cond) {
			ruleDragLine.cond = cond;
		}
		ruleDragLine.startPoint = node1.point;
		ruleDragLine.endPoint = node2.point;
		ruleDragCanvas.ruleDragLines.push(ruleDragLine);

		node1.relationLine.prev.push(ruleDragLine);
		node2.relationLine.next.push(ruleDragLine);
	}


</script>
</html>

styles.css

* {
	margin: 0;
	padding: 0;
	box-sizing: border-box;
}
body {
	width: 100vw;
	height: 100vh;
	position: relative;
}
.canvas {
	min-height: 600px;
	width: 100%;
	background: #eee;
	position: relative;
}
.canvas::before {
	content: '';
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background-color: transparent;
    background-image: linear-gradient(
90deg, rgba(0, 0, 0, 0.1) 1px, transparent 0), linear-gradient(
180deg, rgba(0, 0, 0, 0.1) 1px, transparent 0);
    background-size: 25px 25px;
    min-height: 100%;
    min-width: 100%;
    user-select: none;
}
.point-line {
    width: 100%;
    height: 100%;
    position: absolute;
}

.point-line path {
    stroke: #4a90e2;
    stroke-width: 3;
    fill: none;
    cursor: pointer;
}
.point-line path:hover {
    stroke-width: 6;
    transition: .24s ease-out;
}
.canvas-node {
	border-radius: 4px;
    width: 152px;
    height: 36px;
    position: absolute;
    top: 0;
    left: 0;
	background: #c2185b;
	text-align: center;
    line-height: 36px;
	user-select: none;
	color: #fff;
}
.move {
    stroke-dasharray: 16 20%;
    stroke-dashoffset: 320;
    animation: move 2.5s linear normal infinite;
}

@keyframes move {
	from {
	  stroke-dashoffset: 104%;
	  stroke: #fff;
	}
	to {
	  stroke-dashoffset: 0%;
	  stroke: #fff;
	}
}
posted @ 2021-12-29 17:11  乐小天  阅读(212)  评论(0编辑  收藏  举报