本文详解如何使用 JavaScript 实现同时支持鼠标和键盘的拖拽功能。通过监听 mousedown/mousemove/mouseup 实现常规拖拽,利用 keydown/keypress 监听方向键实现键盘控制。代码重点解决了跨浏览器键盘事件兼容性问题,确保无鼠标用户也能通过 Tab 键聚焦链接并使用方向键移动元素,提升页面无障碍访问体验。
当示例的 box 上的 # 链接处于活动状态时(无论是通过 Tab 键聚焦后按 Enter,还是直接鼠标点击),该元素即可通过方向键进行拖拽操作。随后点击 Enter 或 Esc 键即可释放元素(这两个释放键的定义可随意调整,目前设定为两者均可触发释放)。
使用方法
1、复制文章后方的 dragDrop 对象代码。
2、复制所需的 addEventSimple 和 removeEventSimple 函数。
3、配置 keyHTML 和 keySpeed 属性(具体解释见下文)。
4、确保所有需要拖拽的元素已设置 position 属性为 absolute 或 fixed。
5、将所有可拖拽元素传递给对象的 initElement 函数,参数可以是对象引用或表示元素 ID 的字符串。例如:
dragDrop.initElement('test');
dragDrop.initElement(document.getElementById('test2'));
6、元素被拖拽后,代码会自动为其添加 dragged 类名,你可以据此定义 CSS 样式效果。
7、若需在用户释放元素后执行特定操作,可为 releaseElement 方法注册自定义回调函数。
属性说明
你需要重点设置两个属性。
keyHTML 包含一个供键盘访问的链接内容,用于触发拖拽。为保持 HTML 简洁,此处仅添加了一个带有简单样式的类。你可以自由构建 HTML 结构,但必须确保存在一个可通过键盘访问的链接,因为键盘用户需要一个焦点来触发拖拽事件。
keySpeed 用于设置键盘拖拽的速度,即每次按键移动多少像素。建议设置为 10,你也可以尝试其他数值。
此外还有 7 个内部属性,初始化为 undefined,由相应函数在运行时设置。
拖拽对象代码
将下方对象复制到你的页面中,并务必包含 addEventSimple 和 removeEventSimple 函数。
代码如下:
dragDrop = {
keyHTML: '<a href="#" class="keyLink">#</a>',
keySpeed: 10, // pixels per keypress event
initialMouseX: undefined,
initialMouseY: undefined,
startX: undefined,
startY: undefined,
dXKeys: undefined,
dYKeys: undefined,
draggedObject: undefined,
initElement: function (element) {
if (typeof element == 'string')
element = document.getElementById(element);
element.onmousedown = dragDrop.startDragMouse;
element.innerHTML += dragDrop.keyHTML;
var links = element.getElementsByTagName('a');
var lastLink = links[links.length-1];
lastLink.relatedElement = element;
lastLink.onclick = dragDrop.startDragKeys;
},
startDragMouse: function (e) {
dragDrop.startDrag(this);
var evt = e || window.event;
dragDrop.initialMouseX = evt.clientX;
dragDrop.initialMouseY = evt.clientY;
addEventSimple(document,'mousemove',dragDrop.dragMouse);
addEventSimple(document,'mouseup',dragDrop.releaseElement);
return false;
},
startDragKeys: function () {
dragDrop.startDrag(this.relatedElement);
dragDrop.dXKeys = dragDrop.dYKeys = 0;
addEventSimple(document,'keydown',dragDrop.dragKeys);
addEventSimple(document,'keypress',dragDrop.switchKeyEvents);
this.blur();
return false;
},
startDrag: function (obj) {
if (dragDrop.draggedObject)
dragDrop.releaseElement();
dragDrop.startX = obj.offsetLeft;
dragDrop.startY = obj.offsetTop;
dragDrop.draggedObject = obj;
obj.className += ' dragged';
},
dragMouse: function (e) {
var evt = e || window.event;
var dX = evt.clientX - dragDrop.initialMouseX;
var dY = evt.clientY - dragDrop.initialMouseY;
dragDrop.setPosition(dX,dY);
return false;
},
dragKeys: function(e) {
var evt = e || window.event;
var key = evt.keyCode;
switch (key) {
case 37: // left
case 63234:
dragDrop.dXKeys -= dragDrop.keySpeed;
break;
case 38: // up
case 63232:
dragDrop.dYKeys -= dragDrop.keySpeed;
break;
case 39: // right
case 63235:
dragDrop.dXKeys += dragDrop.keySpeed;
break;
case 40: // down
case 63233:
dragDrop.dYKeys += dragDrop.keySpeed;
break;
case 13: // enter
case 27: // escape
dragDrop.releaseElement();
return false;
default:
return true;
}
dragDrop.setPosition(dragDrop.dXKeys,dragDrop.dYKeys);
if (evt.preventDefault)
evt.preventDefault();
return false;
},
setPosition: function (dx,dy) {
dragDrop.draggedObject.style.left = dragDrop.startX + dx + 'px';
dragDrop.draggedObject.style.top = dragDrop.startY + dy + 'px';
},
switchKeyEvents: function () {
// for Opera and Safari 1.3
removeEventSimple(document,'keydown',dragDrop.dragKeys);
removeEventSimple(document,'keypress',dragDrop.switchKeyEvents);
addEventSimple(document,'keypress',dragDrop.dragKeys);
},
releaseElement: function() {
removeEventSimple(document,'mousemove',dragDrop.dragMouse);
removeEventSimple(document,'mouseup',dragDrop.releaseElement);
removeEventSimple(document,'keypress',dragDrop.dragKeys);
removeEventSimple(document,'keypress',dragDrop.switchKeyEvents);
removeEventSimple(document,'keydown',dragDrop.dragKeys);
dragDrop.draggedObject.className = dragDrop.draggedObject.className.replace(/dragged/,'');
dragDrop.draggedObject = null;
}
}拖拽是什么
拖拽是指在屏幕上移动元素的方法。要使元素可移动,必须设置其 position 属性为 absolute 或 fixed,这样才能通过修改 style.top 和 style.left 坐标来实现位移。
(理论上 position:relative 也可行,但几乎无用。此外,那样需要额外数据计算,此处未作实现。)
设置坐标本身很简单;代码的难点在于确定需要设置的元素坐标,大部分代码都用于解决这一问题。
另外,保持易用性至关重要。传统上通过鼠标拖拽元素是最佳方式,但必须兼顾无鼠标用户,因此需保证键盘操作的可用性。
基础知识
让我们先来看看一些基础知识
初始化一个元素
每个拖拽代码都从初始化元素开始。这个工作通过下面的函完成:
代码如下:
initElement: function (element) {
if (typeof element == 'string')
element = document.getElementById(element);
element.onmousedown = dragDrop.startDragMouse;
element.innerHTML += dragDrop.keyHTML;
var links = element.getElementsByTagName('a');
var lastLink = links[links.length-1];
lastLink.relatedElement = element;
lastLink.onclick = dragDrop.startDragKeys;
}, 如果函数接收到一个字符串,那么就会当做元素 ID 来处理。然后给这个元素设置一个 onmousedown 事件,用来开始鼠标部分的代码。注意这里我使用的是传统事件注册方式;因为我希望 this 关键字能够在 startDragDrop 里起作用。
然后把用户定义的 keyHTML 添加到元素上,我相信这个链接是用来触发键盘事件的。然后为这个链接设置键盘的触发程序。然后存储主元素在 relatedElement 里面,我们后面需要。
现在代码就等用户动作了
基本位置信息
我打算使用下面的方法来:首先我会读取拖拽元素的初始位置,保存在 startX 和 startY 里面。然后计算鼠标移动的位置或者键盘控制下移动的位置来决定元素从初始位置移动的范围。
startX 和 startY 通过 startDrag 函数来设置,这个函数在鼠标和键盘事件里都会用到。
代码如下:
startDrag: function (obj) {
if (dragDrop.draggedObject)
dragDrop.releaseElement();
dragDrop.startX = obj.offsetLeft;
dragDrop.startY = obj.offsetTop;
dragDrop.draggedObject = obj;
obj.className += ' dragged';
}, 首先,如果元素处于拖拽状态,那么我们就释放他(我们后面再讲)。
然后函数会找到元素在起始位置的坐标(offsetLeft 和 offsetTop),然后保存在 startX 和 startY 以备后用。
然后在 draggedObject 里面保存一个对象的引用。然后给他添加 dragged 类,这样就可以通过 CSS 来设置拖拽时候的样式了
当用户使用鼠标或者键盘拖拽元素的时候,代码的最复杂的部分就要跟踪位置的变化。然后给出 dX 和 dY(X 和 Y 的变化)。然后加上 startX 和 startY 就是元素现在的位置。
下面的函数用来设定位置:
代码如下:
setPosition: function (dx,dy) {
dragDrop.draggedObject.style.left = dragDrop.startX + dx + 'px';
dragDrop.draggedObject.style.top = dragDrop.startY + dy + 'px';
}, 函数通过鼠标和键盘的移动计算所得的 dX 和 dY 与初始位置,来设定元素的新位置。
这部分很简单,复杂的地方就在于 dx 和 dy 的获得,鼠标部分和键盘部分的处理非常的不同,我们分别来看。
鼠标部分的代码
鼠标部分的计算方面比键盘的要复杂一些,但是在浏览器兼容性方面问题不大。所以我们从鼠标部分开始。
事件
首先我们来讨论事件。很明显的,在拖拽过程中需要 mousedown,mousemove,mouseup 用来完成选择对象,拖拽,拖拽完成这几个动作。
这一系列事件从需要拖拽元素的 mousedown 事件开始。所以所有的拖拽元素都需要这个事件来标明拖拽开始了。我们看到:
代码如下:
element.onmousedown = dragDrop.startDragMouse; 然而 mousemove 和 mouseup 事件不应该设置在元素上而应该设置在整个文档上。因为用户可能很快很疯狂的移动鼠标,然后丢失拖拽元素。如果设置在元素上,因为鼠标不在元素上了所以可能会无法控制,这对于易用性来说不是好事。
如果我们再文档上设置 mousemove 和 mouseup,就没有这个问题。不管鼠标在哪,元素都会响应 mousemove 和 mouseup。这个的易用性就很强。
另外你只能在拖拽开始以后再设置 mousemove 和 mouseup,然后当用户释放元素之后删除它们。这样代码很干净而且节省系统资源,因为 mousemove 对系统的消耗很大。
Mousedown
当拖拽元素发生 mousedown 事件的时候,startDragMouse 函数就开始执行:
首先会执行我们之前讨论过的 startDrag。然后查找鼠标的坐标然后保存在 initialMouseX 和 initialMouseY 中。后面我们会把鼠标位置跟这个比较。
最后会返回 false,这个用来阻止默认鼠标事件:选择文本。我们不想再拖拽的时候有文本被选中,这很烦人。

然后给文档设置 mouseup 和 mousemove 事件处理程序。因为有可能文档有他自己的 mouseup 和 mousemove 事件处理程序,所以我使用我的 addEventSimple 函数防止原来的事件处理程序失效。 Mousemove 现在,当用户移动鼠标的时候 dragMouse 函数就执行了。
代码如下:
dragMouse: function (e) {
var evt = e || window.event;
var dX = evt.clientX - dragDrop.initialMouseX;
var dY = evt.clientY - dragDrop.initialMouseY;
dragDrop.setPosition(dX,dY);
return false;
}, 这个函数会读取鼠标现在的坐标,然后减去之前的坐标,把得到的 dX 和 dY 传递给 sePosition。
然后通过返回 false 来阻止鼠标选择文本的默认属性。
Mouseup
当用户松开鼠标的时候,会调用 releaseElement。我们后面讨论。
键盘部分代码
现在我们开始更复杂的键盘部分代码。不像鼠标拖拽那样,键盘拖拽并没有一个标准。虽说基本的交互不是太复杂,但是最好还是简要说明一下。
基本交互
用来拖拽的键最好是方向键,这很简单。
激活和释放元素是比较有技巧的,在这里我的代码还需要加强。
我觉得如果用键盘来激活的话就应该使用一个我添加的额外的链接。这里没有太多选择:因为链接能够在所有的浏览器里面获得焦点(好吧,表单也可以,你也可是选择复选框),而且把一个链接放置在可拖拽的元素里面也是合乎逻辑的(你可以放在任何地方,但是如何让用户知道那个是用来激活拖拽的呢?)。
我假设当用户点击 enter 或者 Esc 的时候释放元素,至少我没找到其他合适的键。你想选择其他的话可以在这里查找键盘代码。
case 13: // enter
case 27: // escape
dragDrop.releaseElement();
return false;
事件
点击可以激活元素。当鼠标点击链接或者当元素获得焦点的时候点击 enter 键就能激活。所以键盘代码的激活可以使点击 enter 键或者点击链接。
(严格来说,当你用鼠标点击链接的时候,元素先被鼠标事件激活然后释放了然后再被键盘模式激活。)
事件的其余部分也非常的模糊。当你想检测方向键的时候键盘事件尤为麻烦。
首先我们需要一个允许重复点击的事件,因为用户可能按着方向键不放,那么事件就需要一遍遍的触发,这样拖拽才能继续。所以我们使用 keypress 事件。
不幸的是,IE 在 keypress 的情况下不支持方向键。在 IE 里面 keydown 会重复发生,看起来我们需要使用 keydown 事件了。
你可能才到事情没那么简单。在 Opera 和 Safari 里面 keydown 事件只能触发一次,所以当用户按下键之后,元素移动一次之后就不动了。在这些浏览器中我们需要 keypress。
所以理想情况下,我们使用 keypress,如果不支持就是用 keydown。但是怎么切换事件呢?你又怎么知道 keypress 在这个时候不能用呢?
我的解决办法就是给 keypress 事件设置一个事件处理程序。如果这个程序执行了说明支持 keypress,我们就可以安全的切换了。
startDragKeys 函数用来设置 keydown 和 keypress 事件:
代码如下:
addEventSimple(document,'keydown',dragDrop.dragKeys);
addEventSimple(document,'keypress',dragDrop.switchKeyEvents); 首先 keydown 触发完成拖拽的 dragKeys 函数。这是第一个触发的事件,而且元素总会移动。然后我们做其他的话,那么元素在 Opera 和 Safari1.3 里面移动一次以后就会停止。
这就是为什么我们还需要 keypress。第一个 keypress 事件会触发 switchKeyEvents 函数,这个函数会调整事件处理程序:
代码如下:
switchKeyEvents: function () {
removeEventSimple(document,'keydown',dragDrop.dragKeys);
removeEventSimple(document,'keypress',dragDrop.switchKeyEvents);
addEventSimple(document,'keypress',dragDrop.dragKeys);
}, 他会先删除掉原来的事件处理程序,然后将 keypress 设置为触发 dragKeys。因为这个函数只会在支持他的浏览器里面执行,所以我们只在这些浏览器里面将 keydown 改为 keypress。
初始键盘代码
当用户点击了连接激活了元素,那么就会调用 startDragKeys。
代码如下:
startDragKeys: function () {
dragDrop.startDrag(this.relatedElement);
dragDrop.dXKeys = dragDrop.dYKeys = 0;
addEventSimple(document,'keydown',dragDrop.dragKeys);
addEventSimple(document,'keypress',dragDrop.switchKeyEvents);
this.blur();
return false;
}, 首先会调用我们之前讨论过的 startDrag 函数。他会给这个函数传递 relatedElement,也就是要拖拽的元素。
然后将 dXKeys 和 dYKeys 设置为 0。这些变量用来跟踪元素的位移。
然后设置事件处理程序,上面已经讨论过了。
然后移除刚才点击的链接的焦点。我这样做是因为 Enter 键会释放元素,但是如果不移除焦点,当用户点击了 Enter 键之后,元素被释放,但是链接却再次被 Enter 点击,又成了可拖动的模式。如果我们移除焦点,那么问题就不存在了。
最后返回 false 来阻止默认动作。
通过键盘拖拽
dragKeys 负责键盘拖拽:
代码如下:
dragKeys: function(e) {
var evt = e || window.event;
var key = evt.keyCode; 我们首先读取键盘的键值。
然后我们使用 switch 语句来决定我们怎么做。这部分的目的是更新 dXKeys 和 dYKeys 的值,就可以通过设置元素的位置来移动元素了。
代码如下:
switch (key) {
case 37: // left
case 63234:
dragDrop.dXKeys -= dragDrop.keySpeed;
break;
case 38: // up
case 63232:
dragDrop.dYKeys -= dragDrop.keySpeed;
break;
case 39: // right
case 63235:
dragDrop.dXKeys += dragDrop.keySpeed;
break;
case 40: // down
case 63233:
dragDrop.dYKeys += dragDrop.keySpeed;
break; 作者通过设置 keySpeed 来确定每次移动的像素大小。当用户点击左方向键,就减去 keySpeed。
这个代码包含 63232-63235 的情况。因为 Safari1.3 没有使用标准的 37-40 的方向键的键值(Safari 3 已经支持了)。
代码如下:
case 13: // enter
case 27: // escape
dragDrop.releaseElement();
return false; 如果用户点击 Enter 或者 Esc 键,就调用 releaseElement() 函数。如果你想改变释放元素的按键,可以再这里添加。
代码如下:
default:
return true;
} 如果用户按下了其他键,就执行默认动作并且结束函数。
代码如下:
dragDrop.setPosition(dragDrop.dXKeys,dragDrop.dYKeys); 现在 dXKeys 和 dYKeys 已经更新我们发送到 setPosition() 函数中来改变元素的位置。
代码如下:
if (evt.preventDefault)
evt.preventDefault();
return false;
}, 最后我们需要阻止默认事件,如果用户点击下方向键,那么在执行完上面的代码后页面会向下滚动。W3C 兼容浏览器中通过 preventDefault 实现,在 IE 中通过返回 false 实现。
释放元素
当用户释放了元素,函数 releaseElement 就会被调用。他会移除所有代码设置的事件处理程序,移除 dragged 类,清理 draggedObject 然后等待用户动作。
代码如下:
releaseElement: function() {
removeEventSimple(document,'mousemove',dragDrop.dragMouse);
removeEventSimple(document,'mouseup',dragDrop.releaseElement);
removeEventSimple(document,'keypress',dragDrop.dragKeys);
removeEventSimple(document,'keypress',dragDrop.switchKeyEvents);
removeEventSimple(document,'keydown',dragDrop.dragKeys);
dragDrop.draggedObject.className = dragDrop.draggedObject.className.replace(/dragged/,'');
dragDrop.draggedObject = null;
} 你或许在用户释放元素之后还想做些什么,可以把你的函数添加在这里。
翻译地址:http://www.quirksmode.org/js/dragdrop.html
转载请保留以下信息
作者:北玉(tw:@rehawk)