14.5 利用拖曳功能对列表重新排序
一般从功能性的角度来讲,从一系列的元素中选择不止一个元素,应该比只从中选择一个元素更具挑战性。大家可以很容易地想到一个方案,那就是在列表中的每个项旁边放置一个箭头,用户如果想要移动那些列表项就需要重复点击旁边的箭头。不过这种方法用起来不是很方便。利用JavaScript的拖曳能力则可以实现一种比这种方法简单得多的方法,通过这种方法,用户就可以对列表项进行操作,并且能实时看到它们的变化。
方 法
前一个方法为普通的对象拖曳创建了一些代码,这一节将要介绍的可排序列表就使用了很多前面创建的代码。不过与前面介绍的那种方案比较起来却有很大的区别,当四处拖动选中的元素时,列表中的每个对象必须进行重新排序,以对拖曳行为做出合理的响应,另外当放下了被拖动的对象时,如果排序已经结束,还可以有很多种方式重新记录该列表的结构。
要进行重排序的列表的HTML如下所示:
File: list_order_drag_n_drop.html (excerpt)
<ol id="footballLadder">
<li>
Liverpool
</li>
<li>
Manchester United
</li>
<li>
Arsenal
</li>
<li>
Chelsea
</li>
<li>
West Ham
</li>
<li>
Fulham
</li>
</ol>
用于为每个列表项定位的CSS相当简单,而且很灵活,代码如下:
File: list_order_drag_n_drop.css (excerpt)
ol
{
list-style: none;
}
li
{
width: 195px;
height: 30px;
margin-bottom: 5px;
background-color: #666666;
color: #FFFFFF;
line-height: 30px;
![]()
}
初始化函数和mousedown监听者的实现与在前一节中创建的脚本有很大的不同:
File: list_order_drag_n_drop.js (excerpt)
addLoadListener(initSortableList);
function initSortableList()
{
if (identifyBrowser().indexOf("ie") != −1 &&
identifyOS() == "mac")
{
return false;
}
var LIs = document.getElementById("footballLadder"). getElementsByTagName("li");
for (var i = 0; i < LIs.length; i++)
{
attachEventListener(LIs[i], "mousedown",
mousedownSortableList, false);
LIs[i].style.cursor = "move";
}
}
function mousedownSortableList(event)
{
if (typeof event == "undefined")
{
event = window.event;
}
if (typeof event.pageY == "undefined")
{
event.pageY = event.clientY + getScrollingPosition()[1];
}
var target = getEventTarget(event);
while (target.nodeName.toLowerCase() != "li")
{
target = target.parentNode;
}
document.currentTarget = target;
target.clickOriginY = event.pageY;
attachEventListener(document, "mousemove",
mousemoveCheckThreshold, false);
attachEventListener(document, "mouseup", mouseupCancelThreshold,
false);
return true;
}
因为列表中的元素都是朝着一个方向移动的,所以不需要考虑水平坐标的处理,不过如果需要,也可以利用上一节中所介绍的方法来实现。
如果鼠标按键在某个可拖曳对象上已经按下了,那么一旦开始拖动,mousemoveCheckThreshold和mouseupCencelThreshold就会监视鼠标的动作,这些函数需要检测鼠标移动的距离是否是合理的,检测完之后才会实现真正的拖曳功能:
File: list_order_drag_n_drop.js (excerpt)
function mousemoveCheckThreshold(event)
{
if (typeof event == "undefined")
{
event = window.event;
}
if (typeof event.pageY == "undefined")
{
event.pageY = event.clientY + getScrollingPosition()[1];
}
var target = document.currentTarget;
if (Math.abs(target.clickOriginY − event.pageY) > 3)
{
if (typeof document.selection != "undefined")
{
var textRange = document.selection.createRange();
textRange.collapse();
textRange.select();
}
detachEventListener(document, "mousemove", mousemoveCheckThreshold, false);
detachEventListener(document, "mouseup", mouseupCancelThreshold, false);
attachEventListener(document, "mousemove", mousemoveSortableList, false);
attachEventListener(document, "mouseup", mouseupSortableList, false);
var cloneItem = target.cloneNode(true);
cloneItem.setAttribute("class", "clone");
cloneItem.style.position = "absolute";
cloneItem.style.top = getPosition(target)[1] + "px";
cloneItem.differenceY = parseInt(cloneItem.style.top) − event.pageY;
cloneItem = target.parentNode.appendChild(cloneItem);
target.clone = cloneItem;
target.style.visibility = "hidden";
}
stopDefaultAction(event);
return true;
}
function mouseupCancelThreshold()
{
detachEventListener(document, "mousemove", mousemoveCheckThreshold, false);
detachEventListener(document, "mouseup", mouseupCancelThreshold, false);
return true;
}
如果mousemoveCheckThreshold检测到符合条件的动作(幅度大于3 pixels),那么将会把中间事件监听者去除,然后安装真正的拖曳监听者。在这之前,有一个条件语句,该条件语句是对selection进行处理的。这是为了弥补Windows下低版本的IE浏览器中的bug而设计的,因为在这些版本的浏览器中,即使在拖曳对象的时候也不会取消文本的选择。这种做法只是简单地取消了所有的文本selection,让它们不再起作用。
新的事件监听者添加上去之后,需要创建目标对象的一个完全克隆,并将其放在目标对象原来的位置上。在排序过程的实现中,不是将实际的目标对象到处移动,而是创建目标对象绝对定位的完全克隆,并将真正的目标对象隐藏起来。这样做是因为需要保留列表中的空隙(被拖动的元素留下的空隙),而且还要提供元素在光标周围移动时的视觉显示。通过对目标对象进行克隆,并将其放置在列表的末尾,就可以方便地对两者进行操作了。在列表的末尾添加上目标对象的克隆,保证了该克隆可以继承和列表元素相关联的任何样式,所以无需为了使其和原来列表保持一致再手动对其进行样式化。另外将目标列表项的visibility设置为“hidden”,即使目标项含有某些链接,也无需去移除,因为隐藏元素不能接收事件。
|
|
克隆的克隆 |
|
将“克隆”这个类加到某对象的克隆之上,就可以另外添加一些CSS效果。例如透明度,它可以使该克隆看起来就像原始对象的影子。不过要注意下面这段代码并不是在所有的浏览器中都能正常工作: .clone { opacity: 0.5; } |
一旦创建了下面这种架构,克隆对象的位置以及周围元素的位置就就可以被mousemoveSortableList监听者修改了:
File: list_order_drag_n_drop.js (excerpt)
function mousemoveSortableList(event)
{
if (typeof event == "undefined")
{
event = window.event;
}
if (typeof event.pageY == "undefined")
{
event.pageY = event.clientY + getScrollingPosition()[1];
}
var target = document.currentTarget;
var clone = target.clone;
var plannedCloneTop = event.pageY + clone.differenceY;
var listItems = clone.parentNode.getElementsByTagName("li");
var firstItemPosition = getPosition(listItems[0]);
var lastItemPosition = getPosition(listItems[listItems.length − 2]);
if (plannedCloneTop < firstItemPosition[1])
{
plannedCloneTop = firstItemPosition[1];
}
else if (plannedCloneTop > lastItemPosition[1])
{
plannedCloneTop = lastItemPosition[1];
}
clone.style.top = plannedCloneTop + "px";
var LIs = target.parentNode.getElementsByTagName("li");
var currentItemHigher = true;
for (var i = 0; i < LIs.length; i++)
{
if (LIs[i] != target && LIs[i] != target.clone)
{
if (event.pageY < getPosition(LIs[i])[1] +
LIs[i].offsetHeight && currentItemHigher)
{
target.parentNode.insertBefore(target, LIs[i]);
break;
}
else if (event.pageY > getPosition(LIs[i])[1] && !currentItemHigher)
{
target.parentNode.insertBefore(LIs[i], target);
}
}
else
{
currentItemHigher = false;
}
}
stopDefaultAction(event);
return true;
}
拖动元素的位置计算是用“拖曳行为的实现”这一节中介绍的技术来处理的,不过相对于那一节来说,可排序列表还有其他一些特殊的方面需要考虑。
首先,由于列表的第一个项和最后一项的限制,目标对象克隆的位置就只能处在顶部以及底端之间这个范围中了。如果用户想要将对象的克隆拖动到这个范围之外,那么结果肯定是失败的。从技术上来说,对象的克隆本身就是列表中的最后一项,所以使用倒数第二个元素来标识底端的界限。
当对象的克隆位置确定下来后,还需要进行检查,看这时列表的其他元素应该如何在该克隆的新位置周围进行排序。对除了目标对象和对象的克隆之外的每个列表项,都应该加以检测,看它们的底部边缘是否高于当前光标的位置,而且还要看它们当前是否在目标对象之上。如果列表项满足这些要求,那么该列表项就应该移动到目标元素的下方,为了完成这个任务,可以使用DOM函数insertBefore来对列表进行重新排序。如果列表不满足这些要求,那么就要检查一下,看当前列表元素的顶端边缘是否比当前光标位置低,以及当前元素的位置是否在目标列表项的下方。如果满足了这些要求,就需要将列表重新进行调整,以便使当前列表项处在目标列表项之上。对除此之外的其他情况,只需保持原有的顺序即可。
这个脚本的作用是,当用户到处移动目标对象的克隆时,会造成目标列表项中各项的位置发生自动的改变。用户可以看到列表中的各项实时的顺序变化。
当用户放开鼠标时,脚本立刻删除对象的克隆,并且重新显现目标列表项,这时就又可以看到列表原来的样子了:
File: list_order_drag_n_drop.js (excerpt)
function mouseupSortableList()
{
var target = document.currentTarget;
var clone = target.clone;
clone.parentNode.removeChild(clone);
target.style.visibility = "visible";
detachEventListener(document, "mousemove",
mousemoveSortableList, false);
detachEventListener(document, "mouseup", mouseupSortableList, false);
return true;
}
可以看出,drag-and-drop事件监听者最后会被删除,这时列表就恢复原状了。
最终的脚本产生的效果如图14.4所示。

图14.4 使用drag-sortable列表将Manchester United放到正确的位置







