[原创]prototype对于标签定位的一些BUG

问题起因:
在原来产品中实现的 ajax tree上面添加拖拽效果,为了方便,使用了prototype来简化开发。代码中使用了Poistion.absolutize来改变拖动标签时改变它的坐标为绝对坐标显示,拖动结束后再使用Poistion.relativize变回相对坐标。

解决过程:
其实一开始测试时都挺好的,但后来在tree上面使用时就发生问题了,在拖动过程,标签跟着鼠标的移动而改变,没有问题,但在鼠标释放后,标签并没有放置在鼠标释放的位置,而是向左和向上偏移了,而这偏移的距离刚好就是tree显示位置的left和top。在对拖动结束后的位置计算的代码,拖动过程坐标计算的代码debug了一天没有收获后,突然想到把样式中的滚动条设置(overflow-x : "auto", overflow-y: "scroll",)删掉试下,没想到就可以了。

经过反复验证,终于证实是滚动条惹的祸,接着就跟踪了prototype中的相关代码,在实现Position.absolutize方法时是这样写的:

Position.absolutize  =   function (element)  {
    element 
=  $(element);
    
if  (element.style.position  ==  'absolute')  return ;
    Position.prepare();

var  offsets  =  Position.positionedOffset(element);

    
var  top      =  offsets[ 1 ];
    
var  left     =  offsets[ 0 ];
    
var  width    =  element.clientWidth;
    
var  height   =  element.clientHeight;

    element._originalLeft   
=  left  -  parseFloat(element.style.left   ||   0 );
    element._originalTop    
=  top   -  parseFloat(element.style.top  ||   0 );
    element._originalWidth  
=  element.style.width;
    element._originalHeight 
=  element.style.height;

    element.style.position 
=  'absolute';
    element.style.top    
=  top  +  'px';
    element.style.left   
=  left  +  'px';
    element.style.width  
=  width  +  'px';
    element.style.height 
=  height  +  'px';
}
;

其中Position.positionedOffset就是取当前标签到body的偏移量,然后将信息存入_original*的相关属性中,等到调用Position.relativize时,再从这些_original*属性中从新计算出当前标签的相对位置。
再看一下Position.relativize的实现:

Position.relativize 
=   function (element)  {
    element 
= $(element);
    
if (element.style.position == 'relative') {
        
return;    
    }

    
    Position.prepare();
    
    element.style.position 
= 'relative';
    
var top  = parseFloat(element.style.top  || 0- (element._originalTop  || 0);
    
var left = parseFloat(element.style.left || 0- (element._originalLeft || 0);
    element.style.left   
= left + 'px';
    element.style.top    
= top  + 'px';
    element.style.height 
= element._originalHeight;
    element.style.width  
= element._originalWidth;
}
;

嗯,处理得非常漂亮,没有存在什么问题,以下是用来测试有html,试下会有什么效果
< div  style ="height:50px" ></ div >
< div  style ="width:500px;overflow-y:auto;height:300px" >
< div  style ="height:200px" ></ div >
< div  style ="height:300px" >
< div  id ="test"  style ="height:20px" > test </ div >
    
< input  type ="button"  value ="abs"  onclick ="Position.absolutize('test');" >
    
< input  type ="button"  value ="rel"  onclick ="Position.relativize('test');" >
    
</ div >
</ div >

没错,按下abs按钮后,test向下移了50px左右(第二个div的offsetTop),也向右移了一点(第二个div的offsetLeft)。(如果把overflow-y:aut去掉,则没有此情况出现)而再按下rel按钮后,test能回复正常的位置,这就表示它在算法上没有什么问题,问题出在了absolutize后的位置上了,而与位置相关的信息有 _originalTop 和_originalLeft,而它们的值是与Position.positionedOffset直接相关的,再查看了Position.positionedOffset的代码:

Position.positionedOffset 
=   function  (element)  {    
    
var node = element.parentElement;    
    
var valueT = 0, valueL = 0;
    
    
do {
        valueT 
+= element.offsetTop || 0;
        valueL 
+= element.offsetLeft || 0;
        element 
= element.offsetParent;
        
if (element) {        
            p 
= Element.getStyle(element, "position");
            
if (p == "relative" || p == "absolute" ){
                
break;
            }

        }

    }
 while (element);        
    
    
return [valueL, valueT];
}
;

看起来似乎也无法从中找出什么毛病来。可是,查了一下html的相关文档后,发现这段代码存在着相当严重的bug。html文档里,当样式position取绝对坐标"absolute "时,其内容如下:
absolute :Object is positioned relative to parent element's position—or to the body object if its parent element is not positioned—using the top and left properties.
结合文档内容,经过测试,如果标签的所有祖先节点中,有任何一个是可滚动的(overflow,overflow-y,overflow-x其中一个属性的值为auto或scroll),那标签的绝对定位就是在此标签中的坐标位置,而不是对于BODY的。
所以positionedOffset方法没有考虑到这种情况而处理,当然在一般情况下行得通了,所以代码更改如下:


Position.positionedOffset 
=   function  (element)  {    
    
    
    
/**//*
     * 经过测试,如果标签的所有祖先节点中,有任何一个是可滚动的(overflow,overflow-y,overflow-x其中一个属性的值为auto或scroll),
     * 那标签的绝对定位就是在此标签中的坐标位置,而不是对于BODY的。所以在返回时应该将此祖先节点对于body的偏移量减掉.
     
*/

        
    
var valueT = 0, valueL = 0;
    
    
do {
        valueT 
+= element.offsetTop || 0;
        valueL 
+= element.offsetLeft || 0;
        element 
= element.offsetParent;
        
if (element) {
            
var scrollable = [element.style.overflow, element.style.overflowX, element.style.overflowY];
            p 
= Element.getStyle(element, "position");
            
if (p == "relative" || p == "absolute" || scrollable.include( "auto" ) || scrollable.include( "scroll" )) {
                
break;
            }

        }

    }
 while (element);        
    
    
return [valueL, valueT];
}
;

至此,拖动后的标签定位问题终于解决,看来有时候人应该相信自己多一点,多怀疑一下别人的代码,正所谓,读书要善疑,更何况读别人的程序。

你可能感兴趣的:([原创]prototype对于标签定位的一些BUG)