介绍

在这里插入图片描述

求关注,求收藏,谢谢!
之前负责了我司的几个后台管理型的项目的迭代和新需求开发,第一次以前端负责人的身份参与项目,从技术方案的选型到需求的迭代,再到工作包的分配,组件的拆分,代码的审核,最后的提测、改BUG,虽然有点累,但确实接触到了很多以前不曾接触到的东西,涨了很多见识,当然也见识到了一些叹为观止的骚操作…

今天要说的是组件开发中的一项,我司的项目技术栈主要是围绕着Vue的体系开发的,UI框架基本上不是IView就是Element,且这两者都有其自带的基础组件:弹窗组件,但是从需求的角度出发,这两者的弹窗组件其实都不能满足业务的开发,比如,我司的需求中有一项比较常用但是组件又没有的功能,拖动,(IView可以拖动,但是设置拖动属性会强制将遮罩移除,这个问题直到4.6.0版本才改为不强制,而我司的iView又比较早,升级是不可能升级了);

考虑到其他模块中也有使用到了拖动这个功能,且在Vue中DOM的操作能和自定义指令非常完美契合,最终决定使用自定义指令实现该功能,并且该功能会被作为一个基础组件放入项目;

效果图

话不多说,直接先看最终的效果图,后台模版借用的是IVew的,这个无所谓,主要是弹窗:
在这里插入图片描述

大致效果其实就是这样,挺简单的,这部分的用法就是我们指定了只有按住title部分才可以触发拖动,且拖动的时候是整个弹窗都会被拖动;

下载

其实我们已经积累了一部分常用的自定义指令作为后台模版的基础建设,比如v-loading,以及这个v-move之类的,如果有需要的话,可以看看后面怎么办,这里先暂时上传了v-move的相关开发版的代码,有需要的小伙伴可以下载看看:v-move自定义指令,如果不能下载请及时留言告知,谢谢;

流程图

既然是一个自定义的指令,并且该指令具有一定的通用性,那么我们就必须要进行一些简单的设计,毕竟在开发前我们就想的很周到的话,那么不是就可以轻松很多,并且后期有人接手维护也能有一定的参考,不是啥资料都没有,大致流程图如下:
在这里插入图片描述

代码实现

其实整个代码的实现并不复杂,拖动相关的功能说到底就是一个跟踪鼠标移动实时改变被拖DOM的x轴和y轴坐标的过程,坐标改变了,那么就相当于被拖的DOM拖动了

基础知识

阅读一下功能模块需要对vue的自定义指令有一些基础认知,具体可以直接看官网的说明:Vue自定义指令,简单的说自定义指令就是Vue允许通过自定义指令来对DOM元素进行一些相关操作;
接下来,就按照流程图的顺序,逐一开发相关代码

合法参数验证

在这个阶段,我们需要验证自定义指令的绑定对象,先直接看代码:

/**
 * 初始化阶段
 * @param {HTMLElement} el 待待绑定的元素
 * @param {Object} binding Vue binding集合
 */
inserted(el, binding) {
    // 容错判断,排除非DOM
    if (!(el instanceof HTMLElement)) {
        console.error("类型错误,绑定对象必须是HTMLElement");
        return false;
    }

},

我们知道,自定义指令也是有钩子函数的,示例中使用的是**inserted()**这个钩子函数,这个函数表示:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中),并且这个函数存在两个参数,这里我们用到了第一个:

  • el:代表当前被绑定的DOM元素;

既然是验证,那么我们肯定要验证一下这个el是不是HTMLElement类型,只要是这个类型,那么就代表这个自定义指令绑定的对象是DOM元素;

合并动态参数

在这一步,我们需要将用户自定义的动态参数和v-move中预设好的默认参数进行合并,合并的规则是:如果用户设定了参数那么用户设定的优先,如果没有设定那么就启用默认参数,比如,我们默认拖动的DOM和实际移动的DOM是同一个,但弹窗就相对比较特殊,弹窗是拖动的是title部分,移动的确实整个弹窗整体;

/**
 * 处理动态参数
 * @param {String | Obejct} params 动态参数原始数据
 * @returns {Obejct} 期望的动态参数对象
 */
export function handleParams(params) {
    const data = {};

    Object.assign(data, DEFAULT_CONFIG);
    // 动态参数类型
    switch (commom.getType(params)) {
        // 字符串
        case "[object String]":
            data.move = params;
            data.click = params;
            break;
        // 对象
        case "[object Object]":
            for (let item in data) {
                if (!Object.prototype.hasOwnProperty.call(data, item)) continue;

                // 容错,判断是否为undefined
                data[item] = commom.isUndefined(params[item])
                    ? data[item]
                    : params[item];
            }
            break;
        default:
            // 启用默认参数
            break;
    }

    return data;
}

在这里,我们允许用户输入的动态参数类型有两种,一种是字符串类型,一种是对象类型,除此之外的所有类型都会被认为是非法类型:

  1. 字符串:那么我们就认为拖动和实际移动的DOM是同一种;
  2. 对象:根据key进行一次遍历,并逐一对key对应的值进行覆盖;

最后会得到一个全新的合法的动态参数,并返回;

合并修饰符

动态参数合并完了,那么紧接着就是合并修饰符,和动态参数一样的规则:用户设定优先,如果没有设定,启动默认修饰符,这一块主要合并的是一些比如是否可以超屏拖动的参数,比如设定:

// 允许超屏拖动
<div v-move.all></div>

开启超屏拖动后,那么被拖动的DOM边界可以超出屏幕之外,这个功能是什么用处呢?具体举例一下,比如:实际项目中图片预览功能,某张图片被放大后,因为视角不方便,需要将图片中待阅读的部分拖动到屏幕中央,因为图片放大后肯定有部分会被移出屏幕,因此就需要超屏拖动了,如果不开启,那么被拖动的DOM永远只能在屏幕内部拖动;
具体参数合并代码如下:

/**
 * 处理修饰符
 * @param {Object} modifiers 修饰符原始数据
 * @returns {Object} 期望的修饰符对象
 */
export function handleModifiers(modifiers) {
    // 容错判断,排除非对象
    if (!commom.isObj(modifiers)) {
        console.error("类型错误,修饰符必须是对象类型");
        return false;
    }

    return {
        dirPosition: ABSOLUTE,
        dirAll: modifiers.all ? ALL : null,
    };
}

挺简单的代码,首先是判断了修饰符参数是否是一个对象,做了一个简单的容错,其实这里不对,如果修饰符如果设定异常,那么返回的应该是默认参数,而不是false,这里有问题

拖动验证

肯定有小伙伴觉得奇怪,为什么会有一个拖动验证,很简单,有时候这个拖动功能也是需要开启的,比如,有时候我们希望这个DOM默认是不可以拖动的,只有当开启了某个开关后才能拖动,用法示例:

// isMove判断当前拖动是否生效,true-可拖动,false-不可拖动
<div v-move.all="isMove"></div>

代码上的话,我们只需要对这个value值做一个判断即可,如果vlaue值是false,直接返回,不给走代码就行了

 // false-不可拖动
if (commom.isBol(binding.value) && !binding.value) {
    return false;
}

初始化坐标

这一步中,主要的作用就是初始化DOM的坐标,一是我们得获取到当前DOM现有的坐标,二是初始化一些下面步骤中需要使用到的变量

// 坐标,这个是初始化下面步骤需要的变量
let x = 0,
    y = 0;
let offsetLeft = 0,
    offsetRight = 0;

具体初始化的时机就在于我们鼠标按下的那一刻,在那一刻,我们可以获取到当前被拖动DOM的初始偏移量,我们需要将这些偏移量保存下来做计算:

// 触发鼠标左击
dom.click.onmousedown = function(event) {
    // 阻止默认事件和冒泡
    event.preventDefault();
    event.stopPropagation();

    // 设置初始坐标
    x = event.pageX;
    y = event.pageY;

    // 设置初始偏移位置
    offsetLeft = dom.move.offsetLeft;
    offsetRight = dom.move.offsetTop;

    onMove = true;
};

计算坐标

在计算坐标这一部分,一共有四个坐标需要计算:

  • x轴上的最小坐标,最大坐标
  • y轴上的最小坐标,最大坐标

当我们拖动DOM的时候,需要实时计算出对应的坐标

最小值计算

/**
 * 计算在X轴/Y轴可拖动的最小值
 * @param {Obejct} modifiers 修饰符
 * @param {String} position X轴/Y轴
 * @param {HTMLElement} el
 * @returns {Number} 可拖动的最小距离值
 */
export function computedMin(modifiers, position, el) {
    // 容错判断,排除非对象的修饰符
    if (!commom.isObj(modifiers)) {
        console.error("类型错误,修饰符必须是对象类型");
        return false;
    }
    // 容错判断,排除非DOM
    if (!(el instanceof HTMLElement)) {
        console.error("类型错误,绑定对象必须是HTMLElement");
        return false;
    }

    // 获得x轴最小值
    const minXDistance = parseInt(window.getComputedStyle(el).width);
    // 获得Y轴最小值
    const minYDistance = parseInt(window.getComputedStyle(el).height);

    // 超屏拖动
    if (modifiers.dirAll === ALL) {
        return position === "width" ? -minXDistance : -minYDistance;
    }
    // 非超屏拖动
    else {
        return position === "width" ? 0 : 0;
    }
}

因为x轴和y轴上都存在最小值的计算,因此就通用了一下

最大值计算

/**
 * 计算在X轴/Y轴可拖动的最大值
 * @param {Object} modifiers 修饰符
 * @param {String} position X轴-width,Y轴-height
 * @param {HTMLElement} el 绑定的元素
 * @returns {Number} 可拖动的最大距离值
 */
export function computedMax(modifiers, position, el) {
    // 容错判断,排除非对象
    if (!commom.isObj(modifiers)) {
        console.error("类型错误,修饰符必须是对象类型");
        return false;
    }
    // 容错判断,排除非DOM
    if (!(el instanceof HTMLElement)) {
        console.error("类型错误,绑定对象必须是HTMLElement");
        return false;
    }
    // 无上级元素
    if (!el.parentNode) {
        return 0;
    }

    // 获得X轴最大值
    const maxXDistance = parseInt(window.getComputedStyle(el).width);
    // 获得Y轴最大值
    const maxYDistance = parseInt(window.getComputedStyle(el).height);

    // 超屏拖动
    if (modifiers.dirAll === ALL) {
        return position === "width"
            ? el.parentNode.clientWidth + maxXDistance
            : el.parentNode.clientHeight + maxYDistance;
    }
    // 非超屏拖动
    else {
        return position === "width"
            ? el.parentNode.clientWidth - maxXDistance
            : el.parentNode.clientHeight - maxYDistance;
    }
}

同理,最大值的计算也存在x轴和y轴上的,所以也通用了一下

这样在这一步中,我们就得到了四个坐标,分别是:x轴的最小值和最大值,y轴上的最小值和最大值

设置坐标

在这一步就是对DOM进行坐标设置了,我们需要实时修改DOM的left和top的值,达到DOM的位置的变化,当然,这部分代码需要写在mousemove中,以及鼠标松开时,移除拖动事件

// 触发鼠标拖动
document.onmousemove = function(e) {
    // 判断控制阀确认是否可拖动
    if (!onMove) return false;

    // 初始化位置坐标
    let initMouseX = e.pageX;
    let initMouseY = e.pageY;
    // 初始化偏移坐标
    let offsetX = initMouseX - (x - offsetLeft);
    let offsetY = initMouseY - (y - offsetRight);

    // 获得X轴最大值
    let maxX = computedMax(modifiers, "width", dom.move);
    // 获得X轴最小值
    let minX = computedMin(modifiers, "width", dom.move);

    // 获得Y轴最大值
    let maxY = computedMax(modifiers, "height", dom.move);
    // 获得Y轴最小值
    let minY = computedMin(modifiers, "height", dom.move);

    // 拖动后坐标
    offsetX = offsetX > maxX ? maxX : offsetX < minX ? minX : offsetX;
    offsetY = offsetY > maxY ? maxY : offsetY < minY ? minY : offsetY;

    // 设置坐标
    dom.move.style.left = offsetX + "px";
    dom.move.style.top = offsetY + "px";
};

// 触发鼠标左击释放
document.onmouseup = function() {
    // 关闭控制阀
    onMove = false;
    // 清空状态
    document.onmousemove = document.onmouseup = null;

    // 非函数时返回,函数时触发函数
    if (!commom.isFunction(binding.value)) {
        return false;
    }
    binding.value.call(this);
};

当然,这里还有一个小细节,就是如果我们的value值是一个函数的时候,那么松开鼠标的时候需要触发函数

代码

以下为v-move的主体代码,有兴趣的小伙伴可以试下

/**
 * @Description 本指令应用于设置DOM元素为可拖动状态,具体用法见文档
 */

// 引入工具函数
import commom from "../../Utils/common";

// dom的position值
const ABSOLUTE = "absolute";
// 是否可以超屏拖动
const ALL = "all";

/**
 * 处理修饰符
 * @param {Object} modifiers 修饰符原始数据
 * @returns {Object} 期望的修饰符对象
 */
export function handleModifiers(modifiers) {
    // 容错判断,排除非对象
    if (!commom.isObj(modifiers)) {
        console.error("类型错误,修饰符必须是对象类型");
        return false;
    }

    return {
        dirPosition: ABSOLUTE,
        dirAll: modifiers.all ? ALL : null,
    };
}

// 默认参数
const DEFAULT_CONFIG = {
    move: "",
    click: "",
};

/**
 * 处理动态参数
 * @param {String | Obejct} params 动态参数原始数据
 * @returns {Obejct} 期望的动态参数对象
 */
export function handleParams(params) {
    const data = {};

    Object.assign(data, DEFAULT_CONFIG);
    // 动态参数类型
    switch (commom.getType(params)) {
        // 字符串
        case "[object String]":
            data.move = params;
            data.click = params;
            break;
        // 对象
        case "[object Object]":
            for (let item in data) {
                if (!Object.prototype.hasOwnProperty.call(data, item)) continue;

                // 容错,判断是否为undefined
                data[item] = commom.isUndefined(params[item])
                    ? data[item]
                    : params[item];
            }
            break;
        default:
            // 启用默认参数
            break;
    }

    return data;
}

/**
 * 计算在X轴/Y轴可拖动的最大值
 * @param {Object} modifiers 修饰符
 * @param {String} position X轴-width,Y轴-height
 * @param {HTMLElement} el 绑定的元素
 * @returns {Number} 可拖动的最大距离值
 */
export function computedMax(modifiers, position, el) {
    // 容错判断,排除非对象
    if (!commom.isObj(modifiers)) {
        console.error("类型错误,修饰符必须是对象类型");
        return false;
    }
    // 容错判断,排除非DOM
    if (!(el instanceof HTMLElement)) {
        console.error("类型错误,绑定对象必须是HTMLElement");
        return false;
    }
    // 无上级元素
    if (!el.parentNode) {
        return 0;
    }

    // 获得X轴最大值
    const maxXDistance = parseInt(window.getComputedStyle(el).width);
    // 获得Y轴最大值
    const maxYDistance = parseInt(window.getComputedStyle(el).height);

    // 超屏拖动
    if (modifiers.dirAll === ALL) {
        return position === "width"
            ? el.parentNode.clientWidth + maxXDistance
            : el.parentNode.clientHeight + maxYDistance;
    }
    // 非超屏拖动
    else {
        return position === "width"
            ? el.parentNode.clientWidth - maxXDistance
            : el.parentNode.clientHeight - maxYDistance;
    }
}

/**
 * 计算在X轴/Y轴可拖动的最小值
 * @param {Obejct} modifiers 修饰符
 * @param {String} position X轴/Y轴
 * @param {HTMLElement} el
 * @returns {Number} 可拖动的最小距离值
 */
export function computedMin(modifiers, position, el) {
    // 容错判断,排除非对象的修饰符
    if (!commom.isObj(modifiers)) {
        console.error("类型错误,修饰符必须是对象类型");
        return false;
    }
    // 容错判断,排除非DOM
    if (!(el instanceof HTMLElement)) {
        console.error("类型错误,绑定对象必须是HTMLElement");
        return false;
    }

    // 获得x轴最小值
    const minXDistance = parseInt(window.getComputedStyle(el).width);
    // 获得Y轴最小值
    const minYDistance = parseInt(window.getComputedStyle(el).height);

    // 超屏拖动
    if (modifiers.dirAll === ALL) {
        return position === "width" ? -minXDistance : -minYDistance;
    }
    // 非超屏拖动
    else {
        return position === "width" ? 0 : 0;
    }
}

/**
 * 获得DOM
 * @param {String} className dom的类名
 * @param {HTMLElement} el DOM
 * @returns {HTMLElement} 获得的DOM
 */
export function getDOM(className, el) {
    // 容错判断,排除非DOM
    if (!(el instanceof HTMLElement)) {
        console.error("类型错误,绑定对象必须是HTMLElement");
        return false;
    }
    // 容错判断,排除非字符串
    if (!commom.isString(className)) {
        return el;
    }

    // 存在重复命名DOM时,仅取第一个
    const domArr = el.getElementsByClassName(`${className}`);
    switch (domArr.length) {
        case 1:
            return domArr[0];
        default:
            return el;
    }
}

/**
 * 执行拖动事件
 * @param {HTMLElement} el 可拖动的DOM
 * @param {Object} binding 参数集合
 * @returns {Boolean} true-成功 false-失败
 */
export function handleMove(el, binding) {
    // 容错判断,排除非DOM
    if (!(el instanceof HTMLElement)) {
        console.error("类型错误,绑定对象必须是HTMLElement");
        return false;
    }

    // 获得动态参数
    const arg = handleParams(binding.arg);

    // 可操作对象操作
    const dom = {
        // 拖动元素
        move: getDOM(arg.move, el),
        // 点击元素
        click: getDOM(arg.click, el),
    };

    // 获得修饰符
    const modifiers = handleModifiers(binding.modifiers);

    // 坐标
    let x = 0,
        y = 0;
    let offsetLeft = 0,
        offsetRight = 0;

    // 控制阀
    let onMove = false;
    // 初始化事件
    document.onmousemove = document.onmouseup = dom.click.onmousedown = null;
    // 设置鼠标状态
    dom.click.style.cursor = "default";

    // false-不可拖动
    if (commom.isBol(binding.value) && !binding.value) {
        return false;
    }
    // 初始化拖动对象的鼠标状态及position类型
    dom.click.style.cursor = "move";
    dom.move.style.position = modifiers.dirPosition;

    // 触发鼠标左击
    dom.click.onmousedown = function(event) {
        // 阻止默认事件和冒泡
        event.preventDefault();
        event.stopPropagation();

        // 设置初始坐标
        x = event.pageX;
        y = event.pageY;

        // 设置初始偏移位置
        offsetLeft = dom.move.offsetLeft;
        offsetRight = dom.move.offsetTop;

        onMove = true;

        // 触发鼠标拖动
        document.onmousemove = function(e) {
            // 判断控制阀确认是否可拖动
            if (!onMove) return false;

            // 初始化位置坐标
            let initMouseX = e.pageX;
            let initMouseY = e.pageY;
            // 初始化偏移坐标
            let offsetX = initMouseX - (x - offsetLeft);
            let offsetY = initMouseY - (y - offsetRight);

            // 获得X轴最大值
            let maxX = computedMax(modifiers, "width", dom.move);
            // 获得X轴最小值
            let minX = computedMin(modifiers, "width", dom.move);

            // 获得Y轴最大值
            let maxY = computedMax(modifiers, "height", dom.move);
            // 获得Y轴最小值
            let minY = computedMin(modifiers, "height", dom.move);

            // 拖动后坐标
            offsetX = offsetX > maxX ? maxX : offsetX < minX ? minX : offsetX;
            offsetY = offsetY > maxY ? maxY : offsetY < minY ? minY : offsetY;

            // 设置坐标
            dom.move.style.left = offsetX + "px";
            dom.move.style.top = offsetY + "px";
        };

        // 触发鼠标左击释放
        document.onmouseup = function() {
            // 关闭控制阀
            onMove = false;
            // 清空状态
            document.onmousemove = document.onmouseup = null;

            // 非函数时返回,函数时触发函数
            if (!commom.isFunction(binding.value)) {
                return false;
            }
            binding.value.call(this);
        };
    };

    return true;
}

export default {
    // 指令名称
    name: "tc-move",
    // 指令
    directive: {
        /**
         * 初始化阶段
         * @param {HTMLElement} el 待待绑定的元素
         * @param {Object} binding Vue binding集合
         * @returns {Boolean} true-成功 false-失败
         */
        inserted(el, binding) {
            // 容错判断,排除非DOM
            if (!(el instanceof HTMLElement)) {
                console.error("类型错误,绑定对象必须是HTMLElement");
                return false;
            }

            return handleMove(el, binding);
        },
        /**
         * 更新阶段
         * @param {HTMLElement} el 待待绑定的元素
         * @param {Object} binding Vue binding集合
         * @returns {Boolean} true-成功 false-失败
         */
        update(el, binding) {
            // 容错判断,排除非DOM
            if (!(el instanceof HTMLElement)) {
                console.error("类型错误,绑定对象必须是HTMLElement");
                return false;
            }

            return handleMove(el, binding);
        },
    },
};

小结

本文简单讲述了一个拖动自定义指令的实现,步骤上主要分为:动态参数,修饰符的合并,合并规则是优先用户设定,如果用户没有设定参数那么就启用默认参数,之后就是实时计算拖动后的坐标,并修改DOM的left和top的值以达到拖动的效果;

至此简单的v-move就实现了,之后在项目中注册指令就可以直接使用了;

Logo

前往低代码交流专区

更多推荐