设置与获取样式属性是UI控件与特效库非常关键的部分,设置样式我们基本可以用cssText来包打天下,但获取样式就不是这回事了。JQuery,mootools,Ext等类库在这部分实现得非常艰辛,盘根错节地动用一大堆方法,因此想把这部分抠出来难度很大。深入研究它们的实现后,根据我积累的CSS知识,终于做出一个非常简炼的版本出来。它相当于JQuery.cssCur吧,不过或许功能还丰富一些,按饮食业话说叫“加量不加价”,我的可能还应叫“加量还减价”……版本还处于Beta阶段,由于只个工具函数就不弄成类了。
01.
var
getStyle =
function
(el, style){
02.
if
(!+
"\v1"
){
03.
style = style.replace(/\-(\w)/g,
function
(all, letter){
04.
return
letter.toUpperCase();
05.
});
06.
return
el.currentStyle[style];
07.
}
else
{
08.
return
document.defaultView.getComputedStyle(el,
null
).getPropertyValue(style)
09.
}
10.
}
这是函数的最原始状态,由于used value是W3C那边的人搞出来的,因此document.defaultView.getComputedStyle 基本连动也不动就解决百分之99的问题。IE那边的复杂了,虽然微软搞了style,currentStyle与runtimeStyle,但始终都没有一个与getComputedStyle相近的实现,最相近的是currentStyle,它只能取到内部样式,而且我们取值时要把CSS属性转换成驼峰风格。为了方便,我现在再把它分离出来。
1.
var
camelize =
function
(attr){
2.
return
attr.replace(/\-(\w)/g,
function
(all, letter){
3.
return
letter.toUpperCase();
4.
});
5.
}
接着我们单独解决IE的透明度问题,基本各大类库都是这样做的,可见这问题多么棘手,实在要感谢微软那帮天才:
01.
var
getIEOpacity =
function
(el){
02.
var
filter;
03.
if
(!!window.XDomainRequest){
04.
filter = el.style.filter.match(/progid:DXImageTransform.Microsoft.Alpha\(.?opacity=(.*).?\)/i);
05.
}
else
{
06.
filter = el.style.filter.match(/alpha\(opacity=(.*)\)/i);
07.
}
08.
if
(filter){
09.
var
value = parseFloat(filter[1]);
10.
if
(!isNaN(value)) {
11.
return
value ? value / 100 : 0;
12.
}
13.
}
14.
return
1;
15.
}
这时我们的函数就变成这样:
01.
var
getStyle =
function
(el, style){
02.
if
(!+
"\v1"
){
03.
if
(style ==
"opacity"
){
04.
return
getIEOpacity(el)
05.
}
06.
return
el.currentStyle[camelize(style)];
07.
}
else
{
08.
return
document.defaultView.getComputedStyle(el,
null
).getPropertyValue(style)
09.
}
10.
}
接着下来float属性问题。IE这边是styleFloat,W3C是cssFloat。解决不是问题,不过每次都要转换太麻烦了,我参照Ext的实现把它们缓存起来。
01.
var
propCache = [];
02.
var
propFloat = !+
"\v1"
?
'styleFloat'
:
'cssFloat'
;
03.
var
camelize =
function
(attr){
04.
return
attr.replace(/\-(\w)/g,
function
(all, letter){
05.
return
letter.toUpperCase();
06.
});
07.
}
08.
var
memorize =
function
(prop) {
//意思为:check out form cache
09.
return
propCache[prop] || (propCache[prop] = prop ==
'float'
? propFloat : camelize(prop));
10.
}
11.
var
getIEOpacity =
function
(el){
12.
//*****************略**********************
13.
}
14.
var
getStyle =
function
(el, style){
15.
if
(!+
"\v1"
){
16.
if
(style ==
"opacity"
){
17.
return
getIEOpacity(el)
18.
}
19.
return
el.currentStyle[memorize(style)];
20.
}
else
{
21.
if
(style ==
"float"
){
22.
style = propFloat;
23.
}
24.
return
document.defaultView.getComputedStyle(el,
null
).getPropertyValue(style)
25.
}
26.
}
到最难的部分了——精确取得高度与宽度。如果用过JQuery的人都知道,Dan Wellman 是单独处理这两个属性。其实何止JQuery,其他库都为此头痛。问题的起因是如果没有内联样式或内部样式显式地设置这两个属性,我们在IE下是无法获取它们精确的值,或者获得的是百分比,空字符串,auto,inhert等让人无可奈何的东西。另,对于国人来说,px的地位是远远高于em。总总原因,让我们不能放弃这两个属性。为了取得这两个属性的精确值,我们就需要研究一下CSS继承问题了。我也已撰写相关博文《CSS的inhert与auto》来探讨这问题,没有看,请看完再回来,要不,根本是无法看下去的。
根据CSS的分类,width与height是属性于non-inherited property,由此可知,如果我们不为它设值,默认为auto。这个auto是个神奇的东东,如果是块状元素,它的宽就相当于100%,撑满父元素的内容区,当然这是在不考虑其padding,border与margin的情况下。如果是内联元素,由于不具备盒子模型,你给它设置宽与高是没有意义的,就算在火狐,返回的改过转换的精确值与你看到的情况完全不吻合,如果没有设置,直接返回auto。为了取得以px为单位的精确值,为了屏蔽块状元素与内联元素,我们需要转换思路,不过光盯着CSS属性转。这时,微软做了件好事,开发出offsetXX,clientXX与scrollXX三大家族,现在终于纳入W3C的标准。不过早在这之前,各浏览器已经跟风实现了。
在标准模式中,offsetWidth是包含padding,borderWidth与width,如果存在滚动条,它的offsetWidth也不会变,滚动条的宽度在各浏览器非常一致,都为17px,这时它就会把width减去17px,缺失的空间由滚动条补上。 offsetWidth 如果存在padding与padding,我们就要减去padding与padding 在怪癖模式下,offsetWidth等于width,而width是包含padding与borderWidth。 offsetHeight同理。 clientXX家族好理解,就是不包含borderWidth与滚动条。scrollXX家族不说了,在五大浏览器都不一致。因此,在标准模式中,要取得高与宽,首选clientXX家族,怪癖模式中,首先offsetXX家族。
这时不得不说一下怪癖模式了,别以为升到IE8,设置相应DocType就可以逃过一劫,只要你的网页太多地方不按标准写,IE也转为兼容模式中运作。这兼容模式是否等于怪癖模式就不得而已,因为IE8有多达五种渲染模式,IE5怪癖模式,IE7标准模式,IE8几乎标准模式,IE7兼容模式与支持HTML5的边缘模式。这么多模式,你以为光靠 document.compatMode
1.
var
isQuirk = (document.documentMode) ? (document.documentMode==5) ?
true
:
2.
false
: ((document.compatMode==
"CSS1Compat"
) ?
false
:
true
);
于是我们有如下伪码:
1.
var
getWidth =
function
(el){
2.
if
(isQuirk){
3.
return
el.offsetWidth
4.
}
else
{
5.
return
el.clientWidth - parseFloat(getStyle(el,
"padding-left"
))- parseFloat(getStyle(el,
"padding-right"
))
6.
}
7.
}
对比一下Ext的实现(只抠出核心部分):
1.
getWidth :
function
(contentWidth){
2.
var
me =
this
,
3.
dom = me.dom,
4.
w = MATH.max(dom.offsetWidth, dom.clientWidth) || 0;
5.
w = !contentWidth ? w : w - me.getBorderWidth(
"lr"
) - me.getPadding(
"lr"
);
6.
return
w < 0 ? 0 : w;
7.
},
非常危险的做法,比Prototype的实现还差劲,因此它就不得在其他部分进行纠正,这就是为什么它的UI体积如此庞大的缘故,从侧面也突现JQuery实现手法的高超。不过JQuery的实现了也是相当复杂,如果本元素无法取得精确值,就从上级元素着手,这个遍历消耗非常严重。其中还借用了Dean Edwards的一个伟大的hack:
01.
var
convertPixelValue =
function
(el, value){
02.
var
style = el.style,left = style.left,rsLeft = el.runtimeStyle.left;
03.
el.runtimeStyle.left = el.currentStyle.left;
04.
style.left = value || 0;
05.
var
px = style.pixelLeft;
06.
style.left = left;
07.
el.runtimeStyle.left = rsLeft;
08.
return
px;
09.
}
这个hack是用于将em、pc、pt、cm、in、ex等单位转换为px的,当然不包括百分比。
再回来看我的getWidth函数,主要问题是获取padding-left与padding-right的值。在标准浏览器,我们用getComputedStyle可以轻而易举地获取经过转换的精确值,单位为px。在IE中,如果你给它的值为2em,它就返回2em,很懒。在《CSS的inherit与auto》一文,我也指出了,padding为non-inherited property,这就不用处理inhert这个无厘头的值;在auto列表中,padding也不在列,减少了处理auto这个模糊值的风险;加之是可度量单位,各浏览器都很厚道地设置默认值为0px,换言之,我们需要的做的事是,当值的单位不为px,我们把它转换为px。我们把Dean Edwards的hack整合到我们的主函数getStyle中即可。如果padding-left是百分比,我们就取其父元素的width乘以百分比即可。总而言之,遍历的层次与数算的次数都被压缩最少。从这方面说,我在这方面的处理比JQuery优胜得多(JQuery连border,margin都列入计算范围,而border与margin在IE中存在模糊值,这就逼使JQuery动不动就往上计算父元素,换言之,需要重复计算其父元素的属性五次;我最多为三次,怪异模式下只需一次)。有时在IE中取得的值,比拥有getComputedStyle的火狐还精确(不过,好像精确过头,自己用toFixed调整精确度)。
01.
var
getStyle =
function
(el, style){
02.
if
(!+
"\v1"
){
03.
if
(style ==
"opacity"
){
04.
return
getIEOpacity(el)
05.
}
06.
var
value = el.currentStyle[memorize(style)];
07.
if
(/^(height|width)$/.test(style)){
08.
var
values = (style ==
'width'
) ? [
'left'
,
'right'
] : [
'top'
,
'bottom'
], size = 0;
09.
if
(isQuirk){
10.
return
el[camelize(
"offset-"
+style)]
11.
}
else
{
12.
var
client = parseFloat(el[camelize(
"client-"
+style)]),
13.
paddingA = parseFloat(getStyle(el,
"padding-"
+ values[0])),
14.
paddingB = parseFloat(getStyle(el,
"padding-"
+ values[1]));
15.
return
(client - paddingA - paddingB)+
"px"
;
16.
}
17.
}
18.
if
(!/^\d+px$/.test(value)){
19.
//转换可度量的值
20.
if
(/(em|pt|mm|cm|pc|
in
|ex|rem|vw|vh|vm|ch|gr)$/.test(value)){
21.
return
convertPixelValue(el,value);
22.
}
23.
//转换百分比
24.
if
(/%/.test(value)){
25.
return
parseFloat(getStyle(el.parentNode,
"width"
)) * parseFloat(value) /100 +
"px"
26.
}
27.
}
28.
return
value;
//如 0px
29.
}
else
{
30.
if
(style ==
"float"
){
31.
style = propFloat;
32.
}
33.
return
document.defaultView.getComputedStyle(el,
null
).getPropertyValue(style)
34.
}
35.
}
说多无谓,我们测试一下吧。
1.
<
div
id
=
"text"
style
=
"width:3in;height:180px;background:#8080c0;padding:2%;"
>父元素
2.
<
div
id
=
"text2"
style
=
"width:78%;height:4em;padding:1%;background:red;border:1px solid red"
>子元素</
div
>
3.
</
div
>
1.
window.onload =
function
(){
2.
alert(getStyle(_(
"text"
),
"width"
))
3.
alert(getStyle(_(
"text2"
),
'width'
))
4.
alert(getStyle(_(
"text2"
),
'padding-left'
))
5.
};
发现在IE取得值太精确了,比火狐还准确,不过我也不打算在程序内部设置取精度的运算,因为我不确定现实中究竟会向上遍历多少次。在某一父元素的padding与width取精度,很可能会严重影响其最终值的精度。基本上,width与height与padding的取值就到此为止,我们再来看盒子模型的两个东西:margin与border。
margin的auto通常为0px,但在IE6下,当我们将其父元素的text-align设为center,它不但把文本居中,连块级元素本身也忧患中了,这是IE6居中布局的基石。很难说这是BUG,就像IE5的怪癖模式的盒子模型那样,很符合人通常的思维习惯,但是较难实现,加之W3C是偏袒被微软阴死的网景,于是网景的盒子模型成为标准了。IE6庞大的用户基础不容忽视,我们不能随便给0px了事。我也说了,auto有个对称性,因此好办,我们求出其父元素的width然后减于其offsetWidth再除以2就行了。因为offsetWidth是针对于offsertParent的,因此我们用时临时把其父元素的position设为relative,让子元素来得offsetWidth后,再将父元素的position还原。
01.
//转换margin的auto
02.
if
(/^(margin).+/.test(style) && value ==
"auto"
){
03.
var
father = el.parentNode;
04.
if
(/MSIE 6/.test(navigator.userAgent) && getStyle(father,
"text-align"
) ==
"center"
){
05.
var
fatherWidth = parseFloat(getStyle(father,
"width"
)),
06.
_temp = getStyle(father,
"position"
);
07.
father.runtimeStyle.postion =
"relative"
;
08.
var
offsetWidth = el.offsetWidth;
09.
father.runtimeStyle.postion = _temp;
10.
return
(fatherWidth - offsetWidth)/2 +
"px"
;
11.
}
12.
return
"0px"
;
13.
}
borderWidth的默认值为medium,即使它为0px,但如果我们显式地设置其宽为medium呢?!它就不为0px了。这个比较恶心,我们需要判定这值是我们自己加上的还是浏览器的默认值。不过我们发现如果是默认的,其border-XX-style为none。另,除了medium外,还存在两个模糊值thin与thick。它们在浏览器的精确值见下表:
IE8 ,firefox等标准浏览器 | IE4-7 | |
---|---|---|
thin | 1px | 2px |
medium | 3px | 4px |
thick | 5px | 6px |
01.
//转换border的thin medium thick
02.
if
(/^(border).+(width)$/.test(style)){
03.
var
s = style.replace(
"width"
,
"style"
),
04.
b = {
05.
thin:[
"1px"
,
"2px"
],
06.
medium:[
"3px"
,
"4px"
],
07.
thick:[
"5px"
,
"6px"
]
08.
};
09.
if
(value ==
"medium"
&& getStyle(el,s) ==
"none"
){
10.
return
"0px"
;
11.
}
12.
return
!!window.XDomainRequest ? b[value][0] : b[value][1];
13.
}
再看top,left,right与bottom,想不到firefox,safari都会偷懒,会返回auto,只有opera老老实实地返回精确值。解决办法也很简单,因为微软一个好用的函数已被所有浏览器支持了。
1.
//转换top|left|right|bottom的auto
2.
if
(/(top|left|right|bottom)/.test(style) && value ==
"auto"
){
3.
return
el.getBoundingClientRect()[style]
4.
}
嗯,文章已经很长很长,剩下的部分下次再讲。
来源:http://www.cnblogs.com/rubylouvre/archive/2009/09/05/1559883.html