當然我們可以研究js庫的源碼, 也可以自己去發明輪子試試看, 其過程還是挺有趣的...下面我就來實現下頁面元素的拖拽功能
現在就開始著手實現, 讓我們從最頂層的方法講起, 它用於初始化一個drag object, 方法的聲明如下
function DragObject(cfg)
這裡的cfg我們用一個對象來傳入, 有點像Extjs裡配置屬性
複製代碼 代碼如下:
var dragObj = new DragObject({
el: 'exampleB',
attachEl: 'exampleBHandle',
lowerBound: new Position(0, 0), //position代表一個點,有屬性x,y下面會詳細講到
upperBound: new Position(500, 500),
startCallback: ..., // 開始拖拽時觸發的回調 這裡均省略了
moveCallback: ..., // 拖拽過程中觸發的回調
endCallback: ..., // 拖拽結束觸發的回調
attachLater: ... // 是否立刻啟動拖拽事件的監聽
});
配置參數中el可以是具體元素的id, 也可以直接是個dom對象 attachEl就是例子中的handle元素, 通過拖拽它來拖拽元素, lowerBound和upperBound是用於限定拖拽範圍的, 都是Position對象, 關於這個對象的封裝和作用我們下面會分析到,不急哈: ), 如果沒有傳入的話, 拖拽的範圍就沒有限制. startCallback, moveCallback, endCallback都是些回呼函數, attachLater為true或者false. 如果不是很明白看了下面的分析, 我想你肯定很快會清楚的..
下面就來寫Position, 代碼如下:
複製代碼 代碼如下:
function Position(x, y) {
this.X = x;
thix.Y = y;
}
Position.prototype = {
constructor: Position,
add : function(val) {
var newPos = new Position(this.X, this.Y);
if (val) {
newPos.X += val.X;
newPos.Y += val.Y;
}
return newPos;
},
subtract : function(val) {
var newPos = new Position(this.X, this.Y);
if (val) {
newPos.X -= val.X;
newPos.Y -= val.Y;
}
return newPos;
},
min : function(val) {
var newPos = new Position(this.X, this.Y);
if (val) {
newPos.X = this.X > val.X ? val.X : this.X;
newPos.Y = this.Y > val.Y ? val.Y : this.Y;
return newPos;
}
return newPos;
},
max : function(val) {
var newPos = new Position(this.X, this.Y);
if (val) {
newPos.X = this.X < val.X ? val.X : this.X;
newPos.Y = this.Y < val.Y ? val.Y : this.Y;
return newPos;
}
return newPos;
},
bound : function(lower, upper) {
var newPos = this.max(lower);
return newPos.min(upper);
},
check : function() {
var newPos = new Position(this.X, this.Y);
if (isNaN(newPos.X))
newPos.X = 0;
if (isNaN(newPos.Y))
newPos.Y = 0;
return newPos;
},
apply : function(el) {
if(typeof el == 'string')
el = document.getElementById(el);
if(!el) return;
el.style.left = this.X + 'px';
el.style.top = this.Y + 'px';
}
};
一個座標點的簡單封裝, 它儲存兩個值: x, y座標. 我們能夠通過add和substract方法跟別的座標點進行+運算和-運算, 返回一個計算過的新座標點. min和max函數顧名思義用於跟其他座標點進行比較,並返回其中較小和教大的值.bound方法返回一個在限定範圍內的座標點. check方法用於確保屬性x, y的值是數字類型的, 否則會置0. 最後apply方法就是把屬性x,y作用於元素style.left和top上. 接著我把剩下的大部分代碼拿出來, 再一點一點看:
複製代碼 代碼如下:
function DragObject(cfg) {
var el = cfg.el,
attachEl = cfg.attachEl,
lowerBound = cfg.lowerBound,
upperBound = cfg.upperBound,
startCallback = cfg.startCallback,
moveCallback = cfg.moveCallback,
endCallback = cfg.endCallback,
attachLater = cfg.attachLater;
if(typeof el == 'string')
el = document.getElementById(el);
if(!el) return;
if(lowerBound != undefined && upperBound != undefined) {
var tempPos = lowerBound.min(upperBound);
upperBound = lowerBound.max(upperBound);
lowerBound = tempPos;
}
var cursorStartPos,
elementStartPos,
dragging = false,
listening = false,
disposed = false;
function dragStart(eventObj) {
if(dragging || !listening || disposed) return;
dragging = true;
if(startCallback)
startCallback(eventObj, el);
cursorStartPos = absoluteCursorPosition(eventObj);
elementStartPos = new Position(parseInt(getStyle(el, 'left')), parseInt(getStyle(el, 'top')));
elementStartPos = elementStartPos.check();
hookEvent(document, 'mousemove', dragGo);
hookEvent(document, 'mouseup', dragStopHook);
return cancelEvent(eventObj);
}
function dragGo(e) {
if(!dragging || disposed) return;
var newPos = absoluteCursorPosition(e);
newPos = newPos.add(elementStartPos)
.subtract(cursorStartPos)
.bound(lowerBound, upperBound);
newPos.apply(el);
if(moveCallback)
moveCallback(newPos, el);
return cancelEvent(e);
}
function dragStopHook(e) {
dragStop();
return cancelEvent(e);
}
function dragStop() {
if(!dragging || disposed) return;
unhookEvent(document, 'mousemove', dragGo);
unhookEvent(document, 'mouseup', dragStopHook);
cursorStartPos = null;
elementStartPos = null;
if(endCallback)
endCallback(el);
dragging = false;
}
this.startListening = function() {
if(listening || disposed) return;
listening = true;
hookEvent(attachEl, 'mousedown', dragStart);
};
this.stopListening = function(stopCurrentDragging) {
if(!listening || disposed)
return;
unhookEvent(attachEl, 'mousedown', dragStart);
listening = false;
if(stopCurrentDragging && dragging)
dragStop();
};
this.dispose = function() {
if(disposed) return;
this.stopListening(true);
el = null;
attachEl = null;
lowerBound = null;
upperBound = null;
startCallback = null;
moveCallback = null;
endCallback = null;
disposed = true;
};
this.isDragging = function() {
return dragging;
};
this.isListening = function() {
return listening;
};
this.isDisposed = function() {
return disposed;
};
if(typeof attachEl == 'string')
attachEl = document.getElementById(attachEl);
// 如果沒有配置, 或者沒找到該Dom對象, 則用el
if(!attachEl) attachEl = el;
if(!attachLater)
this.startListening();
}
其中一些未給出方法, 在往下分析的過程中, 會一一給出....
我們先通過cfg來使el和attachEl指向實際的Dom對象, 如果attachEl沒配置或者沒找到對應元素則用el替代. 我們同時設定了一些在拖拽中要用到的變數. cursorStartPos用於儲存滑鼠按下開始拖拽時滑鼠的座標點. elementStartPos用於儲存元素開始拖拽時的起始點. dragging, listening, disposed是一些狀態變數. listening: 指drag object是否正在監聽拖拽開始事件. dragging: 元素是否正在被拖拽. disposed: drag object被清理, 不能再被拖拽了.
在代碼的最後, 我們看到如果attachLater不為true, 那麼就調用startListening, 這是一個 public方法定義在drag object中, 讓我們看下它的實現
複製代碼 代碼如下:
this.startListening = function() {
if(listening || disposed) return;
listening = true;
hookEvent(attachEl, 'mousedown', dragStart);
};
前兩行就是做個判斷, 如果已經開始對拖拽事件進行監聽或者清理過了, 就什麼都不做直接return. 否則把listening狀態設為true, 表示我們開始監聽啦, 把dragStart函數關聯到attachEl的mousedown事件上. 這裡碰到個hookEvent函數, 我們來看看它的樣子:
複製代碼 代碼如下:
function hookEvent(el, eventName, callback) {
if(typeof el == 'string')
el = document.getElementById(el);
if(!el) return;
if(el.addEventListener)
el.addEventListener(eventName, callback, false);
else if (el.attachEvent)
el.attachEvent('on' + eventName, callback);
}
其實也沒什麼, 就是對元素事件的監聽做了個跨瀏覽器的封裝, 同樣的unhookEvent方法如下
複製代碼 代碼如下:
function unhookEvent(el, eventName, callback) {
if(typeof el == 'string')
el = document.getElementById(el);
if(!el) return;
if(el.removeEventListener)
el.removeEventListener(eventName, callback, false);
else if(el.detachEvent)
el.detachEvent('on' + eventName, callback);
}
接著我們來看看dragStart函數的實現, 它是drag object的一個私人函數
複製代碼 代碼如下:
function dragStart(eventObj) {
if(dragging || !listening || disposed) return;
dragging = true;
if(startCallback)
startCallback(eventObj, el);
cursorStartPos = absoluteCursorPosition(eventObj);
elementStartPos = new Position(parseInt(getStyle(el, 'left')), parseInt(getStyle(el, 'top')));
elementStartPos = elementStartPos.check();
hookEvent(document, 'mousemove', dragGo);
hookEvent(document, 'mouseup', dragStopHook);
return cancelEvent(eventObj);
}
attachEl所指的dom對象捕獲到mousedown事件後調用此函數. 首先我們先確定drag object在一個適合拖拽的狀態, 如果拖拽進行中, 或者沒有在監聽拖拽事件, 再或者已經處理完"後事"了, 那就什麼都不做. 如果一切ok, 我們把 dragging狀態設為true, 然後"開工了", 如果startCallback定義了, 那我們就調用下它, 以mousedown event和el為參數. 接著我們定位滑鼠的絕對位置, 儲存到cursorStartPos中. 然後拿到拖拽元素當前的top, left,封裝成Position對象儲存到elementStartPos中. 保險起見我們檢查下elementStartPos中屬性是否合法. 再看兩個hookEvent的調用, 一個是mousemove事件, 表示正在dragging,調用dragGo函數. 一個是mouseup事件, 代表拖拽的結束, 調用dragStopHook函數.可能你會問,為什麼事件綁定在document上, 而不是要拖拽的元素上,比如我們這裡的el或者attachEl.因為考慮到直接將事件綁定到元素上,可能由於瀏覽器的一些延時會影響效果,所以直接把事件綁定到document上. 如果實在不是很理解, 或許影響也不大: P.... 看最後一句話中的cancelEvent(eventObj)
複製代碼 代碼如下:
function cancelEvent(e) {
e = e ? e : window.event;
if(e.stopPropagation)
e.stopPropagation();
if(e.preventDefault)
e.preventDefault();
e.cancelBubble = true;
e.returnValue = false;
return false;
}
用於停止冒泡, 阻止預設事件, 可以理解為安全考慮....在dragStart中有些方法需要介紹下,先來 看看absoluteCursorPosition, 再看下getStyle
複製代碼 代碼如下:
function absoluteCursorPosition(e) {
e = e ? e : window.event;
var x = e.clientX + (document.documentElement || document.body).scrollLeft;
var y = e.clientY + (document.documentElement || document.body).scrollTop;
return new Position(x, y);
}
此方法就只是用於獲得滑鼠在瀏覽器中的絕對位置, 把捲軸考慮進去就行了
複製代碼 代碼如下:
function getStyle(el, property) {
if(typeof el == 'string')
el = document.getElementById(el);
if(!el || !property) return;
var value = el.style[property];
if(!value) {
if(document.defaultView && document.defaultView.getComputedStyle) {
var css = document.defaultView.getComputedStyle(el, null);
value = css ? css.getPropertyValue(property) : null;
} else if (el.currentStyle) {
value = el.currentStyle[property];
}
}
return value == 'auto' ? '' : value;
}
getStyle方法用於擷取元素的css屬性值, 這樣不管你樣式是寫成內聯形式還是定義在css中, 我們都能拿到正確的值, 當然我們這裡只要擷取元素的top, left屬性即可..下面真正處理拖拽工作的方法dragGo
複製代碼 代碼如下:
function dragGo(e) {
if(!dragging || disposed) return;
var newPos = absoluteCursorPosition(e);
newPos = newPos.add(elementStartPos)
.subtract(cursorStartPos)
.bound(lowerBound, upperBound);
newPos.apply(el);
if(moveCallback)
moveCallback(newPos, el);
return cancelEvent(e);
}
這個方法並不複雜, 像其他的方法一樣, 我們先查看下狀態如何, 如果沒有在拖拽中或者已經清理了, 那麼什麼都不做. 如果一切順利, 我們利用滑鼠當前位置, 元素初始位置, 滑鼠初始位置, 和限定範圍(如果配置upperBound, lowerBound的話)來計算出一個結果點, 通過apply方法我們把計算的座標賦給元素style.top和style.left, 讓拖拽元素確定其位置. 如果配置了moveCallback, 那麼就調用下, 最後來個cancelEvent...這裡的新座標運算,類似於jquery的操作, 因為Position對象的每個方法都返回了一個Position對像...dragStart裡還有個方法dragStopHook
複製代碼 代碼如下:
function dragStopHook(e) {
dragStop();
return cancelEvent(e);
}
function dragStop() {
if(!dragging || disposed) return;
unhookEvent(document, 'mousemove', dragGo);
unhookEvent(document, 'mouseup', dragStopHook);
cursorStartPos = null;
elementStartPos = null;
if(endCallback)
endCallback(el);
dragging = false;
}
關鍵看下dragStop方法, 同樣先判斷下狀態, 一切ok的話, 我們移除事件的綁定mousemove和mouseup, 並把 cursorStartPos和elementStartPos的值釋放掉, 一次拖拽結束啦..如果配置了endCallback那就調用下, 最後把dragging狀態設定為false......最後給出會用到的public方法
複製代碼 代碼如下:
this.stopListening = function(stopCurrentDragging) {
if(!listening || disposed)
return;
unhookEvent(attachEl, 'mousedown', dragStart);
listening = false;
if(stopCurrentDragging && dragging)
dragStop();
};
this.dispose = function() {
if(disposed) return;
this.stopListening(true);
el = null;
attachEl = null;
lowerBound = null;
upperBound = null;
startCallback = null;
moveCallback = null;
endCallback = null;
disposed = true;
};
this.isDragging = function() {
return dragging;
};
this.isListening = function() {
return listening;
};
this.isDisposed = function() {
return disposed;
};
stopListening移除監聽拖拽的mousedown事件, 把監聽狀態listening設定為false, 這裡有個參數stopCurrentDragging見名知意. dispose方法用於些處理工作, 如果你不想讓drag object能被拖拽,那麼調用一下dispose就可以了, 至於下面的三個小方法isDragging, isListening, isDisposed一看便知, 返回相關的狀態. 最後給個源碼的下拉連結 下載點我 歡迎園友留言, 交流!