likes
comments
collection
share

2️⃣从零开发一个拖动窗口插件2-视窗相关API以及设计🔆

作者站长头像
站长
· 阅读数 21

承接上文,本文是为了设计拖动窗口的逻辑。浏览器DOM操作中提供了多个API来获取DOM元素的位置,一般来说,最主要的逻辑仍然是获取DOM元素的边距,监听鼠标事件,然后再用JavaScript来改变对应DOM元素的位置。

事件参数

2️⃣从零开发一个拖动窗口插件2-视窗相关API以及设计🔆

首先Vue没有对事件对象做修改,所以在Vue中的事件对象仍然与原生一样。

其对象属性具体又分为4类:

鼠标 / 键盘属性

属性描述
altKey返回当事件被触发时,"ALT" 是否被按下。
button返回当事件被触发时,哪个鼠标按钮被点击。一个数字,表示鼠标事件发生时按下的鼠标按钮。可能的值:0:鼠标左键1:车轮按钮或中间按钮(如果有)2:鼠标右键注意: Internet Explorer 8和更早版本具有不同的返回值:1:鼠标左键2:鼠标右键4:车轮按钮或中间按钮(如果有)注意:对于左侧配置的鼠标,返回值会反转
buttonsbuttons属性返回一个数字,指示触发鼠标事件时按下了哪些鼠标按钮或鼠标按钮。一个数字,表示鼠标事件发生时按下的一个或多个鼠标按钮。如果按下多个按钮,则组合这些值以产生新数字(例如,如果按下左按钮(1)和右按钮(2),则返回值为1 + 2,即3) 。可能的值:1:鼠标左键2:鼠标右键4:滚轮按钮或中间按钮8:第四个鼠标按钮(通常是“浏览器返回”按钮)16:第五个鼠标按钮(通常是“浏览器转发”按钮)注意:对于左侧配置的鼠标,返回值会反转
clientX返回当事件被触发时,鼠标指针的水平坐标。
clientY返回当事件被触发时,鼠标指针的垂直坐标。
ctrlKey返回当事件被触发时,"CTRL" 键是否被按下。
metaKey返回当事件被触发时,"meta" 键是否被按下。
relatedTarget返回与事件的目标节点相关的节点。
screenX返回当某个事件被触发时,鼠标指针的水平坐标。
screenY返回当某个事件被触发时,鼠标指针的垂直坐标。
shiftKey返回当事件被触发时,"SHIFT" 键是否被按下。

IE 属性

除了上面的鼠标/事件属性,IE 浏览器还支持下面的属性:

属性描述
cancelBubble如果事件句柄想阻止事件传播到包容对象,必须把该属性设为 true。
fromElement对于 mouseover 和 mouseout 事件,fromElement 引用移出鼠标的元素。
keyCode对于 keypress 事件,该属性声明了被敲击的键生成的 Unicode 字符码。对于 keydown 和 keyup 事件,它指定了被敲击的键的虚拟键盘码。虚拟键盘码可能和使用的键盘的布局相关。
offsetX,offsetY发生事件的地点在事件源元素的坐标系统中的 x 坐标和 y 坐标。
returnValue如果设置了该属性,它的值比事件句柄的返回值优先级高。把这个属性设置为 fasle,可以取消发生事件的源元素的默认动作。比如在a标签的事件中如果将该值设为false,则不会跳转;在submit按钮中就不会提交事件。
srcElement对于生成事件的 Window 对象、Document 对象或 Element 对象的引用。
toElement对于 mouseover 和 mouseout 事件,该属性引用移入鼠标的元素。
x,y事件发生的位置的 x 坐标和 y 坐标,它们相对于用CSS动态定位的最内层包容元素。

但是当前大部分浏览器的事件对象都已经趋近于统一,比如上图是chrome的事件对象,其中也涵盖了大部分上述所谓IE属性。

标准 Event 属性

下面列出了 2 级 DOM 事件标准定义的属性。

属性描述
bubbles返回布尔值,指示事件是否是起泡事件类型。
cancelable返回布尔值,指示事件是否拥有可取消的默认动作。
currentTarget返回其事件监听器触发该事件的元素。
eventPhase返回事件传播的当前阶段。
target返回触发此事件的元素(事件的目标节点)。
timeStamp返回事件生成的日期和时间。
type返回当前 Event 对象表示的事件的名称。

这部分参数中使用较多的可能是前面几个属性,比如bubblescancelables

标准 Event 方法

下面列出了 2 级 DOM 事件标准定义的方法。IE 的事件模型不支持这些方法:

方法描述
initEvent()初始化新创建的 Event 对象的属性。
preventDefault()通知浏览器不要执行与事件关联的默认动作。
stopPropagation()不再派发事件。

这些方法是挂载在原型链的Event对象上的,所以直接的点击或其他事件是看不到的。

参数详解

  1. currentTargetsrcElementtarget

    1. currentTarget事件属性返回其事件侦听器触发事件的元素。

      target始终返回触发事件的真实元素。(在事件冒泡中与currentTarget不同)

    2. currentTarget在直接捕捉e得到的结果为null,必须在函数中通过一个变量存储下来。

      target的值会始终存储在e中,可以直接通过e查看。

    3. srcElementtarget一致,指向事件触发的元素,旧版本firefox不支持。

  2. detaildetail属性返回一个包含事件详细信息的数字。在onclick和ondblclick上使用时,该数字表示当前的点击次数。在onmousedown和onmouseup上使用时,该数字表示当前点击次数加1。

  3. isTrusted:isTrusted事件属性返回一个布尔值,指示事件是否可信。 注意:在Chrome,Firefox和Opera中,如果事件由用户调用,则该事件是受信任的,如果由脚本调用,则不受信任。在IE中,除了使用createEvent()方法创建的事件之外,所有事件都是可信任的。

  4. relatedTargetrelatedTarget属性返回与触发鼠标事件的元素相关的元素。relatedTarget属性可以与mouseover事件一起使用,以指示光标刚刚退出的元素,或者使用mouseout事件来指示光标刚刚输入的元素。

  5. whichwhich属性返回一个数字,表示触发鼠标事件时按下了哪个鼠标按钮。(注意与button不一致)

    项目描述
    返回值:一个数字,表示鼠标事件发生时按下的鼠标按钮。可能的值:0:没有按钮1:鼠标左键2:滚轮按钮或中间按钮(如果有)3:鼠标右键注意:对于左侧配置的鼠标,返回值会反转。

视窗相关参数

这里具体详解视窗相关参数:

  1. clientX,clientY:在页面(不包括浏览器bar部分)可视范围,与最左面,最上面的距离。

  2. pageXpageY:在整个页面范围内(包括不可视的部分),与最左面,最上面的距离。

  3. screenXscreenY:距离显示器最左面,最上面的距离。(当窗口全屏时,即client加浏览器bar)

  4. layerXlayerY:鼠标相比较于当前坐标系的位置,即如果触发元素没有设置绝对定位或相对定位,以页面为参考点,如果有,将改变参考坐标系,从触发元素盒子模型的border区域的左上角为参考点。值得注意的是:该属性是以border边界为0,而offset是以content边界为0。

    但是MDN上标注该属性为非规范属性,各个浏览器结果预期不一致,最好不要使用。

  5. offsetXoffsetY:距离事件触发元素的左面,上面的距离。

    1. 注意:offset中,padding会被算在内。border会被算作负值。即offset是以padding作为边界的,超过这个边界即被判为负值。
  6. DOM元素还含有一个属性用于计算该元素相对于视窗的距离:getBoundingClientRect(),该方法有4个属性:

    1. top:元素上边到视窗上边的距离;

    2. right:元素右边到视窗左边的距离;

    3. bottom:元素下边到视窗上边的距离;

    4. left:元素左边到视窗左边的距离;

      注意:这些属性一旦超出视窗范围即为负数

  7. DOM元素还带有一下一组(4个)属性:

    1. offsetLeft:返回元素相对于父级元素的左侧偏移量
    2. offsetTop:返回元素相对于父级元素的左侧偏移量
    3. offsetHeight:返回元素的高度(包括paddingborder
    4. offsetWidth:返回元素的宽度(包括paddingborder
    5. offsetParent:返回元素的父级元素
    6. clientLeft:返回元素到父级元素左侧的距离(包括padding,不包括border
    7. clientTop:返回元素到父级元素上侧的距离(包括padding,不包括border
    8. clientHeigt:返回元素的高度(包括padding,不包括border
    9. clientWidth:返回元素的宽度(包括padding,不包括border
    10. scrollHeight:返回元素的滚动总高度(即总页面,包括因滚动而不在视野内的)(包括padding,不包括border
    11. scrollWidth:返回元素的滚动宽度(即总页面,包括因滚动而不在视野内的)(包括padding,不包括border

具体需要注意的是:

  1. offsetxxx包括border,其他的clientxxxscrollxxx不包括border
  2. 这个距离与dom.style.left一样,指的是相对于外层具有定位标志(具有positon样式的元素)的祖先元素的距离。(若外层元素没有positon属性,则以ducoment为其定位元素)
  3. 通过element.style.left只能用来赋值(是字符串,需加后缀px),无法直接获取其值,必须通过上面的offsetLeft来获取其具体值。

2️⃣从零开发一个拖动窗口插件2-视窗相关API以及设计🔆

思路

思路一

中心思想:首先算出首次拖动窗口距视窗的上面和左面的距离,然后计算出每次鼠标的偏移量(可正可负)。然后将其相加,就可得到新的偏移量。

代码思路:

  1. 设置一个变量,用于标记鼠标是否按下。
  2. mousedown钩子函数中记录下此时的clientX1,clientY1,offsetX,offsetY
  3. mousemove钩子函数中记录下次此时的clientX2,clientY2,然后计算两组量:
    1. 拖动之前拖动框距文档边框的距离left1 = clientX1 - offsetXtop1 = clientY1 - offsetY
    2. 拖动的距离disX = clientX2 - clientX1disY = clientY2 - clientY1
  4. 此时新的left2 = left + disXtop2 = top + disY

代码实现:

        banMouseDown(e) {
            console.log("鼠标按下");
            this.isDown = true;
            let dw = this.$refs[this.wid];
            let banner = this.$refs["banner"];
            banner.style.cursor = "move";
            let RectData = dw.getBoundingClientRect();
            Vue.set(this.currenctPositon, "x", e.clientX);
            Vue.set(this.currenctPositon, "y", e.clientY);
            Vue.set(this.currenctPositon, "offX", e.offsetX);
            Vue.set(this.currenctPositon, "offY", e.offsetY);
        },

        banMouseUp() {
            this.isDown = false;
            let banner = this.$refs["banner"];
            banner.style.cursor = "default";
        },

        banMouseMove(e) {
            debounce(this, this.handle, 20)(e);			//节流函数
        },

        handle(e) {
            if (this.isDown) {
                console.log(e);
                let dw = this.$refs[this.wid];
                let banner = this.$refs["banner"];
                banner.style.cursor = "move";
                let RectData = dw.getBoundingClientRect();
                let cx = e.clientX;
                let cy = e.clientY;
                let moveX =
                    (this.currenctPositon.x -
                    this.currenctPositon.offX) +
                    (cx - this.currenctPositon.x);
                let moveY =
                    (this.currenctPositon.y -
                    this.currenctPositon.offY) +
                    (cy - this.currenctPositon.y);
                Vue.set(this.currenctPositon, "x", cx);
                Vue.set(this.currenctPositon, "y", cy);
                dw.style.left = moveX + "px";
                dw.style.top = moveY + "px";
            }
        },

思路二

然后又发现另一个思路(似乎更简单):

中心思想:要想达到拖动效果,即要保持鼠标的位置相对于拖动框是相对静止的。而鼠标的位置相对于拖动框就是offset的值,是不会变化的。所以当鼠标发生移动时,拖动框的位置也要发生变化,才能保证offset的值是不变的。所以我们在新的一次鼠标移动(mousemove)后,新的偏移量应当赋予拖动框的left,right值。即left = clientX2 - offsetXtop = clientY2 - offsetY

代码思路:

  1. 设置一个变量,用于标志鼠标是否按下。
  2. mousedown钩子函数中记录下此时的clientX1,clientY1,offsetX,offsetY
  3. mousemove钩子函数中记录下次此时的clientX2,clientY2,然后计算拖动出偏移量dOffsetXdOffsetY,具体
    1. left = clientX2 - offsetX
    2. top = clientY2 - offsetY
  4. 将拖动窗口的样式:
    1. style.left设置为left
    2. style.top设置为top

实测是可以使用的。

代码实现:

banMouseDown(e) {
    console.log("鼠标按下");
    this.isDown = true;
    let dw = this.$refs[this.wid];
    let banner = this.$refs["banner"];
    banner.style.cursor = "move";
    let RectData = dw.getBoundingClientRect();
    Vue.set(this.currenctPositon, "x", e.clientX);
    Vue.set(this.currenctPositon, "y", e.clientY);
    Vue.set(this.currenctPositon, "offX", e.offsetX);
    Vue.set(this.currenctPositon, "offY", e.offsetY);
},

    banMouseUp() {
        this.isDown = false;
        let banner = this.$refs["banner"];
        banner.style.cursor = "default";
    },

        banMouseMove(e) {
            debounce(this, this.handle, 20)(e);
        },

            handle(e) {
                if (this.isDown) {
                    let dw = this.$refs[this.wid];
                    let banner = this.$refs["banner"];
                    banner.style.cursor = "move";
                    let RectData = dw.getBoundingClientRect();
                    let cx = e.clientX;
                    let cy = e.clientY;
                    Vue.set(this.currenctPositon, "x", cx);
                    Vue.set(this.currenctPositon, "y", cy);
                    let moveX = this.currenctPositon.x - this.currenctPositon.offX;
                    let moveY = this.currenctPositon.y - this.currenctPositon.offY;
                    dw.style.left = moveX + "px";
                    dw.style.top = moveY + "px";
                }
            },
}

思路三

接下来我发现了另外一个apimovementXmovementY,这个api会在mousemove时记录与上一次移动的距离,所以就在思路一的基础上可以省略求disX,disY的过程,所以有了以下代码:



handle(e) {
    if (this.isDown) {
        let dw = this.$refs[this.wid];
        let banner = this.$refs["banner"];
        banner.style.cursor = "move";
        let RectData = dw.getBoundingClientRect();
        let cx = e.clientX;
        let cy = e.clientY;
        Vue.set(this.currenctPositon, "x", cx);
        Vue.set(this.currenctPositon, "y", cy);
        let moveX = this.currenctPositon.x -this.currenctPositon.offX + e.movementX;
        let moveY = this.currenctPositon.y -this.currenctPositon.offY + e.movementY;
        dw.style.left = moveX + "px";
        dw.style.top = moveY + "px";
    }
},

但是由于movementX,movementY返回的是int,精度不够,所以会出现不跟手的情况;而且IE均不支持该属性,所以不是最优方案。

思路四

最后我去参考了layui的方案。

 moveElem.on('mousedown', function(e){
    e.preventDefault();
    if(config.move){
      dict.moveStart = true;
      dict.offset = [
        e.clientX - parseFloat(layero.css('left'))
        ,e.clientY - parseFloat(layero.css('top'))
      ];
      ready.moveElem.css('cursor', 'move').show();
    }
  });

_DOC.on('mousemove', function(e){

    //拖拽移动
    if(dict.moveStart){
      var X = e.clientX - dict.offset[0]
      ,Y = e.clientY - dict.offset[1]
      ,fixed = layero.css('position') === 'fixed';
      
      e.preventDefault();
      
      dict.stX = fixed ? 0 : win.scrollLeft();
      dict.stY = fixed ? 0 : win.scrollTop();

      //控制元素不被拖出窗口外
      if(!config.moveOut){
        var setRig = win.width() - layero.outerWidth() + dict.stX
        ,setBot = win.height() - layero.outerHeight() + dict.stY;  
        X < dict.stX && (X = dict.stX);
        X > setRig && (X = setRig); 
        Y < dict.stY && (Y = dict.stY);
        Y > setBot && (Y = setBot);
      }
      
      layero.css({
        left: X
        ,top: Y
      });
    }

layui采用的是一种更为常见的方式。

其关键在于其并没有直接拿offsetX,而是通过clientX - leftclientY - top来计算offset。其原因在于可能是offset属性的兼容性问题。

FeatureChromeEdgeFirefox (Gecko)Internet ExplorerOperaSafari
Basic support(Yes)(Yes)39.0 (39.0)6(Yes)(Yes)
Redefined from long to double56?????

offsetX在早期返回一个int,这对于拖动窗口有很大的影响,比如上面的movement属性,所以layui选择了更加稳定的直接获取其style.left属性。

函数节流

这一部分,我觉得可加可不加,因为虽然DOM消耗很大,但是目前计算机的性能是完全足够承担一秒几十次到上百次的DOM重绘的。额可以在后期加入检测机制,如果机器性能较差,则可以节流mousemove函数。具体细节可以查看函数的节流与防抖

转载自:https://juejin.cn/post/7128962995555663879
评论
请登录