要说getPosition()的起源,可能没人说得清楚。但是网上广为流传的这段代码,却是许多人使用的:
function getPosition (el) {
var left = 0, top = 0;
do {
left += el.offsetLeft || 0;
top += el.offsetTop || 0;
} while (el = el.offsetParent);
return {x:left,y:top};
}
它真的正确吗?答案当然是否定的。至于为什么,看完Mootools的Element类的相关实现,一切就会大白于天下。
Element.implement({
getScroll: function(){
if (isBody(this)) return this.getWindow().getScroll();
return {x: this.scrollLeft, y: this.scrollTop};
},
getScrolls: function(){
var element = this, position = {x: 0, y: 0};
while (element && !isBody(element)){
position.x += element.scrollLeft;
position.y += element.scrollTop;
element = element.parentNode;
}
return position;
},
getOffsetParent: function(){
var element = this;
if (isBody(element)) return null;
if (!Browser.Engine.trident) return element.offsetParent;
while ((element = element.parentNode) && !isBody(element)){
if (styleString(element, 'position') != 'static') return element;
}
return null;
},
getOffsets: function(){
if (this.getBoundingClientRect){
var bound = this.getBoundingClientRect(),
html = document.id(this.getDocument().documentElement),
htmlScroll = html.getScroll(),
elemScrolls = this.getScrolls(),
elemScroll = this.getScroll(),
isFixed = (styleString(this, 'position') == 'fixed');
return {
x: bound.left.toInt() + elemScrolls.x - elemScroll.x + ((isFixed) ? 0 : htmlScroll.x) - html.clientLeft,
y: bound.top.toInt() + elemScrolls.y - elemScroll.y + ((isFixed) ? 0 : htmlScroll.y) - html.clientTop
};
}
var element = this, position = {x: 0, y: 0};
if (isBody(this)) return position;
while (element && !isBody(element)){
position.x += element.offsetLeft;
position.y += element.offsetTop;
if (Browser.Engine.gecko){
if (!borderBox(element)){
position.x += leftBorder(element);
position.y += topBorder(element);
}
var parent = element.parentNode;
if (parent && styleString(parent, 'overflow') != 'visible'){
position.x += leftBorder(parent);
position.y += topBorder(parent);
}
} else if (element != this && Browser.Engine.webkit){
position.x += leftBorder(element);
position.y += topBorder(element);
}
element = element.offsetParent;
}
if (Browser.Engine.gecko && !borderBox(this)){
position.x -= leftBorder(this);
position.y -= topBorder(this);
}
return position;
},
getPosition: function(relative){
if (isBody(this)) return {x: 0, y: 0};
var offset = this.getOffsets(),
scroll = this.getScrolls();
var position = {
x: offset.x - scroll.x,
y: offset.y - scroll.y
};
var relativePosition = (relative && (relative = document.id(relative))) ? relative.getPosition() : {x: 0, y: 0};
return {x: position.x - relativePosition.x, y: position.y - relativePosition.y};
}
});
var styleString = Element.getComputedStyle;
function styleNumber(element, style){
return styleString(element, style).toInt() || 0;
};
function borderBox(element){
return styleString(element, '-moz-box-sizing') == 'border-box';
};
function topBorder(element){
return styleNumber(element, 'border-top-width');
};
function leftBorder(element){
return styleNumber(element, 'border-left-width');
};
function isBody(element){
return (/^(?:body|html)$/i).test(element.tagName);
};
以上即是摘抄的部分代码,其中每个方法我会逐步叙说一遍。至于牵扯到其它一些关于mt核心的内容的,只做简单介绍,看不懂的话就会比较吃力了(尤其是mt的OOP理念)。
- getScroll()
这是最简单的方法,判断当前元素是否是Body元素。是的话返回window对象的scroll;否则返回元素自身的scroll值。
- getScrolls()
多了一个s,直接从命名上就能猜出获取的是当前元素相对于顶级body的scroll值。相对于上一个方法来说,它只是多了一个循环而已。
- getOffsetParent()
获取元素的父偏移节点。首先,需要判断当前元素是不是Body,如果是的话,那么它爸爸自然就是null了;其它情况下,循环向上找,直到position不是static的为止(不是static自然就是relative、fixed或者absolute)。
- getOffsets()
获取总偏移量。这是最难的方法,也是为什么说开始的那段代码是错误的原因所在!仔细看看合模型就会发现,开头的代码在每次循环时会少计算元素的border宽度,但是这个在不同浏览器和版本中的表现却是不同的。比如IE8反而将其包括进来,不再少算border宽度。只是我们无需再用循环的办法来做,因为有更好的东西——getBoundingClientRect()。
这里不会叙说历史,介绍getBoundingClientRect()如何如何(我也不知道),但是较新版本的浏览器(FF 3.0+和Opera 9.5+)都已开始支持这一方法,用它可以直接获取页面元素的绝对位置。看代码的第29~39行,逻辑很清楚:getBoundingClientRect()的值赋予bound;然后分别取left和top转换成数字;再减去自身的scroll()加上scrolls()——这两个方法刚刚介绍过;最后根据元素本身是否是fixed定位,来减去documentElement的scroll()值;得出的结果减去clientLeft或clientTop即可。
getBoundingClientRect()是很好,那么那些低版本或者不支持的浏览器呢?没办法,老套路,循环了。循环前依然要判断是否是Body元素,那样就直接返回0。
在循环体的内部,我们发现mt对不同内核的浏览器分开了处理。gecko核心(Firefox为代表)在51行有个borderBox判断,这是什么?94行的方法给出了解答——box-sizing的值是否为border-box(改变css2.1合模型组成模式,width=content,而不是border+padding+content,具体请翻阅手册),是的话无需多计算元素本身的border。随后的56行判断父元素的overflow属性是否为visible,否的话要包含计算父元素的border。
对于webkit引擎的浏览器(Safari为代表),处理显得简单多了:只需包含计算父元素的border即可(不包括自己)。
循环完毕后,结果就出来了。只是针对gecko还需做一步处理:box-sizing不是border-box时要减去元素自身的border值。
- getPosition()
这个方法名字有点误导,实际上是需要传入一个元素参数的,返回当前节点相对于参数元素的偏移量。当然,Body元素还是会直接返回0。
首先计算出自己的offset、scroll,两值相减得出position;然后计算参数元素的position(注意,同样使用了getPosition()方法,只是没有传入参数,这样82行就不会产生计算,直接返回offset – scroll);最后,将元素的position减去参数的position就是返回值了。
88行以下的一些方法是mt的内置方法,上面的介绍中可能用到,我一一简单例举出来,就不多做解释——当然它们也是非常简单的。搞定绝对位置这个烫手山芋之后,想要做出兼容的拖动效果,便是水到渠成了。