本文以jquery-ui sortable排序功能插件为例分析jquery-ui源代码重点难点。
jquery-ui-sortable插件非常复杂深奥,本来是针对鼠标操作设计的,在手机运行无效,因为手机没有mouse事件,更没有drag事件。
但增加一个jquery-ui-touch-punch.js文件之后在手机运行也有效,touch有效,原理在于sortable源代码不是针对鼠标drag and drop设计的,而是针对mouse事件中的位置数据设计的,底层有复杂的算法,touch事件数据也包含位置数据,因此sortable能兼容touch,很强大。
sortable插件的调用方式是$().sortable(opts),那么sortable()函数就是入口初始化函数,我们从这个入口初始化函数源代码开始分析。
$.widget.bridge = function( name, object ) {
var fullName = object.prototype.widgetFullName || name;
$.fn[ name ] = function( options ) { // 此即为sortable()函数代码
this.each(function() { // this是$()元素集合,一般只有一个节点,$().each()就是循环一次,元素就是container元素。
var instance = $.data( this, fullName ); // this是元素集合里面每一个元素对象,这是从元素对象取比如ui-sortable属性值是一个实例
if ( instance ) {
instance.option( options || {} );
if ( instance._init ) {
instance._init();
}
} else {
$.data( this, fullName, new object( options, this ) ); // 如果没有就建一个实例保存到元素属性中
constructor = $[ namespace ][ name ] = function( options, element ) { // object代码
// allow instantiation without "new" keyword
if ( !this._createWidget ) {
return new constructor( options, element );
}
// allow instantiation without initializing for simple inheritance
// must use "new" keyword (the code above always passes args)
if ( arguments.length ) {
this._createWidget( options, element );
$.Widget.prototype = {
_createWidget: function( options, element ) {
this._create(); // 调sortable插件的内置方法初始化sortable插件,由于加了一层封装,用apply方式调用
var sortable = $.widget("ui.sortable", $.ui.mouse, { // sortable插件的定义
_create: function() {
this.refresh();//根据已经存在的网页中的列表创建一套items[]
this._mouseInit(); //调用mouse插件的通用初始化方法,这是juqery-ui-touch修改之后的init代码
$.ui.mouse.prototype._mouseInit=function(){
var _this=this;
_this.element.bind("touchstart",$.proxy(_this,"_touchStart"))
.bind("touchmove",$.proxy(_this,"_touchMove"))
.bind("touchend",$.proxy(_this,"_touchEnd"));
oldMouseInit.call(_this);
};
};
this._trigger( "create", null, this._getCreateEventData() );
this._init(); // 空函数
}
sortable入口函数就是创建一个实例保存在container元素属性中,先调用通用插件widget代码创建实例,再调用sortable插件的初始化方法,sortable插件初始化方法
创建items[]保存在sortable实例中,执行mouse初始化代码,如果执行了jquery-ui-touch-punch.js,mouse初始化代码被jquery-ui-touch修改了,就是绑定touch事件,touch事件会转换为mouse事件。
因此执行$().sortable()之后,创建了sortable实例,进行了初始化,绑定了touch事件,当touch操作时,执行mouse事件绑定的handler实现拖拽功能。
下面来看sortable插件的重点函数代码:
var sortable = $.widget("ui.sortable", $.ui.mouse, { // sortable的内置方法都加了一层封装,用value.apply方式调用
_mouseDrag: function(event) { //拖拽时会连续执行多次,拖拽时处理网页样式变化,把placeholder插入到目标元素之前,sortable不修改items[]原数据
//Compute the helpers position
//当拖动一个元素时,其class增加helper成为helper,成为浮动元素,元素跟着拖动浮动,离开了列表,helper的意思就是预览让人能看见元素被拖动走了。
//placeholder是新建一个元素代替它占据在列表中的位置,就是被拖动元素在列表中的替身,当拖动元素时,placeholder元素就插入相应的位置,这样就能预览插入的效果
this.position = this._generatePosition(event); // 计算helper的位置,来自touch event x/y位置数据,拖动时位置不断变化,helper跟着拖动走
_generatePosition: function(event) {
event中有touch位置x/y,计算时涉及到父容器scroll,要把父容器scroll去掉简化处理,调插件传参scroll:false忽略scroll处理。
return {
top: (
pageY - // The absolute mouse position
this.offset.click.top - // Click offset (relative to the element)
this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent
this.offset.parent.top + // The offsetParent's offset without borders (offset + border)
( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ))
),
left: (
pageX - // The absolute mouse position
this.offset.click.left - // Click offset (relative to the element)
this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent
this.offset.parent.left + // The offsetParent's offset without borders (offset + border)
( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ))
)
};
}
//Rearrange,产生placeholder元素插入到touch位置
for (i = this.items.length - 1; i >= 0; i--) { //
intersection = this._intersectsWithPointer(item); // intersect是touch与元素重叠的意思
_intersectsWithPointer: function(item) {
var isOverElementHeight = (this.options.axis === "x") || this._isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height),
isOverElementWidth = (this.options.axis === "y") || this._isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width),
_isOverAxis: function( x, reference, size ) { // x-touch位置 reference-元素位置
return ( x >= reference ) && ( x < ( reference + size ) ); // 判断touch位置是否在元素中,在水平方向和垂直方向分别判断
}
isOverElement = isOverElementHeight && isOverElementWidth,
verticalDirection = this._getDragVerticalDirection(),
horizontalDirection = this._getDragHorizontalDirection();
return this.floating ?
( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 )
: ( verticalDirection && (verticalDirection === "down" ? 2 : 1) );
if (!intersection) {
continue;
}
//如果item与touch重叠,要修改列表,把placeholder插入到item元素之前
this._rearrange(event, item);
_rearrange: function(event, i, a, hardRefresh) { // i是touch目标item元素
i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling));
//向上向左拖时direction=down,向下向右拖时direction=up,placeholder插入位置有可能是item之前,也有可能是item下一个元素之前
this._trigger("change", event, this._uiHash());
}
//Call callbacks
this._trigger("sort", event, this._uiHash()); //如果有绑定"sort"逻辑事件,则执行handler,实际上没有
}
_mouseStop: function(event, noPropagation) { // 单步看开始执行之前start/end元素已经交换了位置,是由drag过程处理的,stop只是善后处理
$.ui.ddmanager.drop(this, event); // debug看没有处理内容
that._clear(event);
_clear: function(event, noPropagation) {
this.placeholder.before(this.currentItem); // sortbale实例里面的placeholder对象的before方法
before: function() {
return domManip( this, arguments, function( elem ) {
if ( this.parentNode ) {
this.parentNode.insertBefore( elem, this ); // elm是helper(drag)元素,this是placeholder,把拖动的元素插入到placeholder位置,这也是整个sortable操作的最终目的,helper元素已经在网页中,insert只是移动位置,插入之后还要修改css样式(修改class即可)。
}
} );
}
function domManip( collection, args, callback, ignored ) { // dommanip非常复杂
callback.call( collection[ i ], node, i );
return collection;
}
this._trigger("update", event, this._uiHash());
this._trigger("beforeStop", event, this._uiHash());
this.placeholder[0].parentNode.removeChild(this.placeholder[0]); // 删除placeholder
this._trigger("stop", event, this._uiHash());
sortable处理网页在拖动时插入placeholder最后再把拖动的元素插入到placeholder位置。
sortable是根据鼠标的位置数据进行计算判断目标位置元素,所以也适用于touch触发,因为touch事件数据中含位置数据,只要把touch事件转换成mouse事件即可,
如果sortable是按drag target元素设计的,就没法适用于touch,因为touchend没有target元素,没法获取到target元素。
sortable插件代码非常复杂,很难控制,本人参考它的处理思路和算法,自己写了一个简化的sortable插件来实现touch触发排序,测试效果没问题:
因为用前端框架比如vue开发,touch拖动时无需处理拖拽效果(实际上鼠标操作时h5会自动产生拖拽效果),只需要在touchend时计算出target元素,有了start元素和end元素,只需要把原数组变换一下,框架会自动更新页面,无需写代码处理页面更新,而jquery-ui-sortable的功能就是在拖拽时更新页面效果,我不需要它这个功能,只需要参考它计算mouseup/touchend元素的算法,它的算法很复杂,简化一下测试结果也是可以的。
_touchEnd(e){
//console.log(e);
e.preventDefault();
e.stopPropagation();
var _this = this;
var intersection = false;
$("#tf_box > li").each(function(k,v){
//console.log(v.offsetLeft);
intersection = _this._intersectsWithTouch(e,v);
if (intersection) {
//item与touchend位置重叠,则item即为touchend/drag目标元素,修改原数组交换start/end元素
_this.endIndex = k;
_this.updateData(); // 修改原数组,页面会自动更新显示拖拽交换后的效果
}
});
},
_intersectsWithTouch(event,item) { // 计算touchend元素位置
var touchY = event.originalEvent.changedTouches[0].clientY; // 注意不要用screenX/Y,那是在屏幕中的位置,要用在浏览器窗口中的位置
var touchX = event.originalEvent.changedTouches[0].clientX;
//console.log(touchX+":"+item.offsetLeft);
var isOverElementHeight = touchY >= item.offsetTop && ( touchY < ( item.offsetTop + item.offsetHeight )),
isOverElementWidth = touchX >= item.offsetLeft && ( touchX < ( item.offsetLeft + item.offsetWidth )),
isOverElement = isOverElementHeight && isOverElementWidth;
//console.log(isOverElementHeight+":"+isOverElementWidth);
if (!isOverElement) {
return false;
}
return true;
},
本来无意研究jquery-ui源代码,因为发现有些sortble程序在手机运行无效果,有些在手机运行有效果,而jquery-ui-sortable在手机运行也是有效的,有点好奇,才研究了jquery-ui-sortable源代码,搞清楚了其中的是非,涉及到mouse/touch/drag事件原理和算法,还是非常复杂的,拖拽比较复杂,比拖拽更复杂的就是动画了。
文中错误之处欢迎大家指正交流。