• 实现可拖拽,拉伸,吸附功能的甘特图(时间/任务表)


    最近应为业务需求需要开发一个任务调度后台,实现一个甘特图( 类似上学时候的课程表,‘时间/课程/代课老师’ 转换为: “时间/任务/执行人'”)。参考图片:

    每一行的00:00到24:00部分的 <div class="tr-right draggable ui-widget-content"> 是展示一个用户所有任务的容器;

    每个一个粉色 <div class="dragItem" >  是一个 任务元素 ;

    每个任务元素 <div class="rightLine">  是此任务用来左右拉伸的长度控制游标;

    <div class="tr-right draggable ui-widget-content">
        <div class="dragItem" style="left: 0px;">
            <div class="dragBox">
                任务三
                <div class="rightLine">
    
                </div>
            </div>
        </div>
        <div class="dragItem" style="left: 100px;">
            <div class="dragBox">
                任务四
                <div class="rightLine">
    
                </div>
            </div>
        </div>
        <div class="dragItem" style="left: 200px;">
            <div class="dragBox">
                任务五
                <div class="rightLine">
    
                </div>
            </div>
        </div>
        <div class="dragItem" id="ddd" style="left: 300px;">
            <div class="dragBox">
                任务六
                <div class="rightLine">
    
                </div>
            </div>
        </div>
    </div>

    首先,使用jqueryUI的 Draggable拖拽组件为每个任务元素绑定拖拽事件:

    $(".dragItem").draggable({
      containment: "parent",
      axis: "x",
      cursor: "move",
      start: function(data,data1) {
        //元素拖拽开始
      },
      drag: function(data,data1) {
        //元素拖拽中
      },
      stop: function(data,data1) {
        //元素拖拽结束
      }
    });

    containment: "parent", //元素只能在其父容器内进行拖拽;

    axis: "x",       //元素只能沿着x轴做水平运动;

    cursor: "move",    //鼠标hover样式为move

    此时任务元素已经可以拖动了,可是发现元素们再拖拽过程中可以相互重叠(因为拖拽只会改变元素的position:absolute的left值);

    但是需求是:用户同一个时段只能进行一个任务;那么必然我们要做重叠判断,并且要做好堆叠情况下元素位置的reset( 吸附或者还原操作 )。

     那么问题来了:

      1. 怎么判定两个元素之间的位置关系?

      2.判定两个元素是堆叠关系后,如何reset?

    对于两个position: absolute的元素 A和 B,因为元素只能水平拖拽,所以他们的top: 0 ;他们两个的位置关系有一下几种:

    1. AB包含。

    2. A包含了 B

    3. A的右边和 B有重叠。

    4. A的左边和 B有重叠。

    5. AB没有重叠部分。

    function situation(a,b){
       
    var aLeft = a.position().left;   //a元素的left值 var aRight = aLeft + a.outerWidth();  //a元素right。。。 var bLeft = b.position().left; var bRight = bLeft + b.outerWidth(); //a元素中心距离b元素中心的距离 var l_r = ((bRight+bLeft)-(aRight+aLeft))/2; if(aLeft>=bLeft&&aRight<=bRight){ //a被b包含 return [0,l_r]; } if(bLeft>=aLeft&&bRight<=aRight){ //a包含了b return [1,l_r]; } if(((bLeft>=aLeft&&bLeft<aRight)&&(bRight>aRight))||((aRight>bLeft&&aRight<=bRight)&&(aLeft<bLeft))){ //a的右边和b有重叠 return [2,l_r]; } if(((bRight>aLeft&&bRight<=aRight)&&(bLeft<aLeft))||((aLeft>=bLeft&&aLeft<bRight)&&(aRight>bRight))){ //a的左边和b有重叠 return [3,l_r]; } //a和b没有任何重叠 return [4,l_r]; }
     //此方法分别输入a,b元素的left,right值,结果返回true代表a,b有重合部分,返回false代表啊,b辆元素不重合。

      function checkPoint(a, b, c, d) {  

        //a代表a元素的left值,b代表a元素的right值,c代表b元素的left值,d代表b元素的right值。

        return(a >= c && b <= d) ||

        (c >= a && d <= b) ||
        (((c >= a && c < b) && (d > b)) || ((b > c && b <= d) && (a < c))) ||
        (((d > a && d <= b) && (c < a)) || ((a >= c && a < d) && (b > d)));
      }

    situation(a,b)方法返回a,b两个元素的位置关系:

    1. A被 B包含。[ 0, l_r ]

    2. A包含了 B。[ 1, l_r ]

    3. A的右边和 B有重叠。[ 2, l_r ]

    4. A的左边和 B有重叠。[ 3, l_r ]

    5. A与 B没有重叠部分。[ 4, l_r ]

    l_r>0代表B元素中心在A元素中心的右边。反之在左边。

    接着分析需求:

    拖拽两个任务对象有重叠时,需要对拖拽对象与其重叠对象做吸附处理。

    就是说:在拖拽并释放元素A的瞬间,需要判定A是否与其他任务元素有重叠。

    1. 如果此时有元素BA重叠,需要将A吸附到元素B的一边,至于吸附到B的左边还是右边,非常简单:判断释放时A的中心点在B的左边还是右边,

    左边就将A的最右侧吸附到B的最左侧,右边就将A的最左侧吸附到B的最右侧。

    2. 如果A释放的瞬间,有B,C,D...与A有重叠,需要A吸附到那个与他中心点距离最近的重叠元素。

    3. 如果A吸附到B元素后,会与C,D,E...重合。则重置A到拖拽之前的初始位置。

    4. 如果A吸附到B元素后,A元素却超出了父元素内部分。则重置A到拖拽之前的初始位置。

    //拖拽
    $(".dragItem").draggable({
        containment: "parent",
        axis: "x",
        cursor: "move",
        stop: function(data, data1) {
            //拖拽任务对象
            var zj = data1.helper;
            //获取与自己中心最近的接触对象
            var mostTouchDom = getMostTouchDom(zj);
            if(mostTouchDom != false) { //释放后是会和其他元素产生堆叠
                if(mostTouchDom.touchRange >= 0) {
              //如果touchRange>0,自己中心在接触元素中心的左边
    if(!eachItemsLR(0, zj[0], mostTouchDom.dom, 0, ($(mostTouchDom.dom).position().left - zj.outerWidth()))) {
                //如果吸附以后,不会再与其他元素产生重叠,此时执行吸附操作: zj.css(
    'left', ($(mostTouchDom.dom).position().left - zj.outerWidth())); } else {
                //吸附后会与其他元素产生堆叠,重置自己到拖拽前的初始状态 zj.css(
    'left', data1.originalPosition.left); } } else {
              //如果touchRange<0,自己的中心在接触元素中心的右边
    if(!eachItemsLR(0, zj[0], mostTouchDom.dom, 1, ($(mostTouchDom.dom).position().left + $(mostTouchDom.dom).outerWidth() + zj.outerWidth()), zj.parent())) {
                //同上个对称的if zj.css(
    'left', ($(mostTouchDom.dom).position().left + $(mostTouchDom.dom).outerWidth())); } else {
                //同上个对称else zj.css(
    'left', data1.originalPosition.left); } } } } });
    //判断是否有兄弟元素与自己产生堆叠,如果有返回此兄弟元素,如果没有返回false;
    function getMostTouchDom(zj) {
        var choseList = [];
        zj.parent().find('.dragItem').each(function() {
            if(zj[0] != this) {
                var result = situation(zj, $(this));
                if(result[0] < 4 && result[0] >= 0) {
                    choseList.push({
                        touchAbs: Math.abs(result[1]),  //与重叠元素中心点的距离的绝对值
                        dom: this,              //此重叠元素对象
                        touchRange: result[1]        //与重叠元素中心点距离
                    });
                }
            }
        });
        if(choseList.length == 0) {
            return false;
        }
       //数组以绝对值大小降序排列 choseList.sort(
    function(a, b) { return a.touchAbs - b.touchAbs; });
      //返回与zj中心点距离最近的元素
    return choseList[0]; }/* * //遍历检查:除了自身身和接触对象外,是否有其他元素会在吸附后产生重叠。
     * type:0:为拖拽,1:为拉伸(拉伸稍后会介绍)
     * item:自身元素对象.
     * target:当前与自身元素接触目标元素对象.
     * lr:值为0:自身吸附在目标元素的左侧. 值为1:自身吸附在目标元素的右侧.
     * can:代表在假设吸附状态形成后,自身元素的left值.(左吸附)。自身元素的right值.(右吸附)。
     * box:当前任务的父元素(任务容器对象)。
     */
    function eachItemsLR(type, item, target, lr, can, box) {
        var bool = false;
        var boxRight = (typeof(box) == "undefined") ? false : box.width();
        $(item).parent().find('.dragItem').each(function() {
            if(item != this && target != this) {
                if(lr == 0) {
                    //将要左吸附,对假设已完成左吸附的元素做容器类的重叠检测
                //type==0时为拖拽功能逻辑:checkPoint()第一个参数为 被吸附元素的left - 自身的宽度
                //type==1是为拉伸功能逻辑:checkPoint()第一个参数为 自身元素的left bool = checkPoint(type ? ($(item).position().left) : ($(target).position().left - $(item).outerWidth()), ($(target).position().left), $(this).position().left, ($(this).position().left + $(this).outerWidth())); if(bool) { return false; } } else { //将要右吸附,对假设已完成右吸附的元素做容器类的重叠检测 bool = checkPoint(($(target).position().left + $(target).outerWidth()), ($(target).position().left + $(target).outerWidth() + $(item).outerWidth()), ($(this).position().left), ($(this).position().left + $(this).outerWidth())); if(bool) { return false; } } if(bool) { return false; } } }); //如果为true则需要重置 return bool || (can < 0) || (boxRight === false ? false : (can > box.width())); }

     以上是拖拽任务的吸附处理逻辑代码,其中eachItemsLR( )方法包含对拉伸功能的处理逻辑。

    下面介绍拉伸功能。

    首先在任务对象元素的最右边放一个拉伸所用的游标<div class="rightLine"> 他的 position:absolute;right:0 !important;5px;height:100%;

    以确保游标永远在拖拽元素的最右边。当拉伸游标时同时改变父元素(拖拽元素)的宽度,从而做到手动改变元素的宽度。

    拉伸和拖拽功能逻辑处理上大同小异,稍稍有些不同:

    1. 拉伸会改变元素的宽度。

    2. 拉伸释放的瞬间,在与重叠元素做吸附判断时,如果吸附失败( 吸附后会与其他元素重叠或者超出父元素范围 )

     那么需要同时重置拉伸元素宽度位置到拉伸前的状态(鼠标松开后需要恢复到鼠标按下之前的状态)

    //拉伸
    $(".rightLine").draggable({
        axis: "x", 
        cursor: "e-resize",  //鼠标hover样式为拉伸
        drag: function(data, data1) {
            data1.helper.parent().parent().outerWidth(data1.position.left + 5);
        },
        stop: function(data, data1) {
            //拉伸任务对象
            var zj = data1.helper.parent().parent();
            if(data1.position.left < 0) {//游标left超出拖拽对象左侧(left<=)时重置游标left为0
                zj.outerWidth(5);
                data1.helper.css('left', 0);
                return;
            }
            var mostTouchDom = getMostTouchDom(zj);
            if(mostTouchDom != false) {
                var thisLeft = $(mostTouchDom.dom).position().left;
                var zjWidth = zj.outerWidth();
                var thisWidth = $(mostTouchDom.dom).outerWidth();
    
                if(mostTouchDom.touchRange >= 0) {
                    if(!eachItemsLR(1, zj[0], mostTouchDom.dom, 0, ((thisLeft - zjWidth) < 0 ? 0 : (thisLeft - zjWidth)))) {
                        zj.width($(mostTouchDom.dom).position().left - zj.position().left);
                        data1.helper.css('left', ($(mostTouchDom.dom).position().left - zj.position().left - 5));
                    } else {
                        zj.outerWidth(data1.originalPosition.left + 5);
                        data1.helper.css('left', data1.originalPosition.left);
                    }
    
                } else {
                    if(!eachItemsLR(1, zj[0], mostTouchDom.dom, 1, (thisLeft + zjWidth + thisWidth), zj.parent())) {
                        zj.css('left', ($(mostTouchDom.dom).position().left + $(mostTouchDom.dom).outerWidth()));
                    } else {
                        zj.outerWidth(data1.originalPosition.left + 5);
                        data1.helper.css('left', data1.originalPosition.left);
                    }
                }
            }
        }
    });

    这里需要注意的是游标向左拉到底时,任务元素的宽度要保持一个游标的宽度:5px,并且游标超出任务元素对象最左侧(left<0)时要重置游标left为0。

    插件预览地址:https://zhuxiao2004.github.io/draggable/

    项目是基于jqueryUI,地址为 https://github.com/zhuxiao2004/draggable

    代码我会继续完善,做到每天进步一点点,谢谢!

  • 相关阅读:
    详细解说python垃圾回收机制
    Vue-- 监听路由参数变化,数据无法更新 解决方案
    解决“只能通过Chrome网上应用商店安装该程序”的方法 ---离线安装谷歌浏览器插件
    axios POST提交数据的三种请求方式写法
    axios POST提交数据的三种请求方式写法
    vue+element后台系统 自己动手撸(一)
    element-ui中 table表格hover 修改背景色
    解决vue的{__ob__: observer}取值问题
    Vue [__ob__: Observer]取不到值问题的解决
    VUE监听路由变化的几种方式
  • 原文地址:https://www.cnblogs.com/zhuxiaoweb/p/8404438.html
Copyright © 2020-2023  润新知