2️⃣从零开发一个拖动窗口插件2-视窗相关API以及设计🔆
承接上文,本文是为了设计拖动窗口的逻辑。浏览器DOM操作中提供了多个API来获取DOM元素的位置,一般来说,最主要的逻辑仍然是获取DOM元素的边距,监听鼠标事件,然后再用JavaScript来改变对应DOM元素的位置。
事件参数
首先Vue
没有对事件对象做修改,所以在Vue
中的事件对象仍然与原生一样。
其对象属性具体又分为4类:
鼠标 / 键盘属性
属性 | 描述 |
---|---|
altKey | 返回当事件被触发时,"ALT" 是否被按下。 |
button | 返回当事件被触发时,哪个鼠标按钮被点击。一个数字,表示鼠标事件发生时按下的鼠标按钮。可能的值:0:鼠标左键1:车轮按钮或中间按钮(如果有)2:鼠标右键注意: Internet Explorer 8和更早版本具有不同的返回值:1:鼠标左键2:鼠标右键4:车轮按钮或中间按钮(如果有)注意:对于左侧配置的鼠标,返回值会反转 |
buttons | buttons属性返回一个数字,指示触发鼠标事件时按下了哪些鼠标按钮或鼠标按钮。一个数字,表示鼠标事件发生时按下的一个或多个鼠标按钮。如果按下多个按钮,则组合这些值以产生新数字(例如,如果按下左按钮(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 对象表示的事件的名称。 |
这部分参数中使用较多的可能是前面几个属性,比如bubbles
,cancelables
标准 Event 方法
下面列出了 2 级 DOM 事件标准定义的方法。IE 的事件模型不支持这些方法:
方法 | 描述 |
---|---|
initEvent() | 初始化新创建的 Event 对象的属性。 |
preventDefault() | 通知浏览器不要执行与事件关联的默认动作。 |
stopPropagation() | 不再派发事件。 |
这些方法是挂载在原型链的Event
对象上的,所以直接的点击或其他事件是看不到的。
参数详解
-
currentTarget
、srcElement
与target
-
currentTarget
事件属性返回其事件侦听器触发事件的元素。target
始终返回触发事件的真实元素。(在事件冒泡中与currentTarget
不同) -
currentTarget
在直接捕捉e得到的结果为null
,必须在函数中通过一个变量存储下来。target
的值会始终存储在e中,可以直接通过e查看。 -
srcElement
与target
一致,指向事件触发的元素,旧版本firefox不支持。
-
-
detail
:detail属性返回一个包含事件详细信息的数字。在onclick和ondblclick上使用时,该数字表示当前的点击次数。在onmousedown和onmouseup上使用时,该数字表示当前点击次数加1。 -
isTrusted
:isTrusted事件属性返回一个布尔值,指示事件是否可信。 注意:在Chrome,Firefox和Opera中,如果事件由用户调用,则该事件是受信任的,如果由脚本调用,则不受信任。在IE中,除了使用createEvent()
方法创建的事件之外,所有事件都是可信任的。 -
relatedTarget
:relatedTarget属性返回与触发鼠标事件的元素相关的元素。relatedTarget属性可以与mouseover事件一起使用,以指示光标刚刚退出的元素,或者使用mouseout事件来指示光标刚刚输入的元素。 -
which
:which属性返回一个数字,表示触发鼠标事件时按下了哪个鼠标按钮。(注意与button
不一致)项目 描述 返回值: 一个数字,表示鼠标事件发生时按下的鼠标按钮。可能的值:0:没有按钮1:鼠标左键2:滚轮按钮或中间按钮(如果有)3:鼠标右键注意:对于左侧配置的鼠标,返回值会反转。
视窗相关参数
这里具体详解视窗相关参数:
-
clientX
,clientY
:在页面(不包括浏览器bar部分)可视范围,与最左面,最上面的距离。 -
pageX
、pageY
:在整个页面范围内(包括不可视的部分),与最左面,最上面的距离。 -
screenX
、screenY
:距离显示器最左面,最上面的距离。(当窗口全屏时,即client
加浏览器bar) -
layerX
、layerY
:鼠标相比较于当前坐标系的位置,即如果触发元素没有设置绝对定位或相对定位,以页面为参考点,如果有,将改变参考坐标系,从触发元素盒子模型的border区域的左上角为参考点。值得注意的是:该属性是以border
边界为0,而offset
是以content
边界为0。但是
MDN
上标注该属性为非规范属性,各个浏览器结果预期不一致,最好不要使用。 -
offsetX
、offsetY
:距离事件触发元素的左面,上面的距离。- 注意:
offset
中,padding
会被算在内。border
会被算作负值。即offset
是以padding
作为边界的,超过这个边界即被判为负值。
- 注意:
-
DOM
元素还含有一个属性用于计算该元素相对于视窗的距离:getBoundingClientRect()
,该方法有4个属性:-
top:元素上边到视窗上边的距离;
-
right:元素右边到视窗左边的距离;
-
bottom:元素下边到视窗上边的距离;
-
left:元素左边到视窗左边的距离;
注意:这些属性一旦超出视窗范围即为负数
-
-
DOM元素还带有一下一组(4个)属性:
offsetLeft
:返回元素相对于父级元素的左侧偏移量offsetTop
:返回元素相对于父级元素的左侧偏移量offsetHeight
:返回元素的高度(包括padding
,border
)offsetWidth
:返回元素的宽度(包括padding
,border
)offsetParent
:返回元素的父级元素clientLeft
:返回元素到父级元素左侧的距离(包括padding
,不包括border
)clientTop
:返回元素到父级元素上侧的距离(包括padding
,不包括border
)clientHeigt
:返回元素的高度(包括padding
,不包括border
)clientWidth
:返回元素的宽度(包括padding
,不包括border
)scrollHeight
:返回元素的滚动总高度(即总页面,包括因滚动而不在视野内的)(包括padding
,不包括border
)scrollWidth
:返回元素的滚动宽度(即总页面,包括因滚动而不在视野内的)(包括padding
,不包括border
)
具体需要注意的是:
- 即
offsetxxx
包括border
,其他的clientxxx
,scrollxxx
不包括border
- 这个距离与
dom.style.left
一样,指的是相对于外层具有定位标志(具有positon
样式的元素)的祖先元素的距离。(若外层元素没有positon
属性,则以ducoment
为其定位元素) - 通过
element.style.left
只能用来赋值(是字符串,需加后缀px
),无法直接获取其值,必须通过上面的offsetLeft
来获取其具体值。
思路
思路一
中心思想:首先算出首次拖动窗口距视窗的上面和左面的距离,然后计算出每次鼠标的偏移量(可正可负)。然后将其相加,就可得到新的偏移量。
代码思路:
- 设置一个变量,用于标记鼠标是否按下。
mousedown
钩子函数中记录下此时的clientX1
,clientY1
,offsetX
,offsetY
。mousemove
钩子函数中记录下次此时的clientX2
,clientY2
,然后计算两组量:- 拖动之前拖动框距文档边框的距离
left1 = clientX1 - offsetX
,top1 = clientY1 - offsetY
- 拖动的距离
disX = clientX2 - clientX1
,disY = clientY2 - clientY1
- 拖动之前拖动框距文档边框的距离
- 此时新的
left2 = left + disX
,top2 = 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 - offsetX
,top = clientY2 - offsetY
代码思路:
- 设置一个变量,用于标志鼠标是否按下。
mousedown
钩子函数中记录下此时的clientX1
,clientY1
,offsetX
,offsetY
。mousemove
钩子函数中记录下次此时的clientX2
,clientY2
,然后计算拖动出偏移量dOffsetX
,dOffsetY
,具体left = clientX2 - offsetX
top = clientY2 - offsetY
- 将拖动窗口的样式:
style.left
设置为left
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";
}
},
}
思路三
接下来我发现了另外一个api
:movementX
,movementY
,这个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 - left
,clientY - top
来计算offset
。其原因在于可能是offset
属性的兼容性问题。
Feature | Chrome | Edge | Firefox (Gecko) | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|---|
Basic support | (Yes) | (Yes) | 39.0 (39.0) | 6 | (Yes) | (Yes) |
Redefined from long to double | 56 | ? | ? | ? | ? | ? |
offsetX
在早期返回一个int
,这对于拖动窗口有很大的影响,比如上面的movement
属性,所以layui
选择了更加稳定的直接获取其style.left
属性。
函数节流
这一部分,我觉得可加可不加,因为虽然DOM消耗很大,但是目前计算机的性能是完全足够承担一秒几十次到上百次的DOM重绘的。额可以在后期加入检测机制,如果机器性能较差,则可以节流mousemove
函数。具体细节可以查看函数的节流与防抖
转载自:https://juejin.cn/post/7128962995555663879