zoukankan      html  css  js  c++  java
  • 记一种拖拉拽编排流程的思路

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

    有这么一个场景,我们希望能在界面上编排流程,可以添加任意类型的节点,也可以编排节点之间的约束条件。拿采购流程举例,项目经理节点发起采购流程指向采购部门,如果金额在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;
         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;
         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 {
    	 100vw;
    	height: 100vh;
    	position: relative;
    }
    .canvas {
    	min-height: 600px;
    	 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- 100%;
        user-select: none;
    }
    .point-line {
         100%;
        height: 100%;
        position: absolute;
    }
    
    .point-line path {
        stroke: #4a90e2;
        stroke- 3;
        fill: none;
        cursor: pointer;
    }
    .point-line path:hover {
        stroke- 6;
        transition: .24s ease-out;
    }
    .canvas-node {
    	border-radius: 4px;
         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;
    	}
    }
    
    敌人总是会在你最不想它出现的地方出现!
  • 相关阅读:
    js获取鼠标的位置
    去掉a标签的虚线框,避免出现奇怪的选中区域
    点击按钮 可以显示隐藏
    input标签获取焦点时文本框内提示信息清空背景颜色发生变化
    ie6下面不支持!important的处理方法
    [zz]【整理】Python中Cookie的处理:自动处理Cookie,保存为Cookie文件,从文件载入Cookie
    [vim]大小写转换
    [zabbix]zabbix2.0apt源安装
    [mysql]replace
    [ethernet]ubuntu更换网卡驱动
  • 原文地址:https://www.cnblogs.com/longhx/p/15745776.html
Copyright © 2011-2022 走看看