dojo/dom-geometry元素大小

  在進入源碼分析前,咱們先來點基礎知識。下面這張圖畫的是元素的盒式模型,這個沒有兼容性問題,有問題的是元素的寬高怎麼算。以寬度爲例,ff中 元素寬度=content寬度,而在ie中 元素寬度=content寬度+border寬度+padding寬度。IE8中加入了box-sizzing,該css屬性有兩個值:border-box、content-box分別對應ie和ff中元素寬度的工做方式。css

  偏移量:offsetLeft、offsetTop、offsetWidth、offsetHeighthtml

  offsetLeft:包含元素的左內邊框到元素的左外邊框之間的像素距離。前端

  offsetTop:包含元素的上內邊框到元素的上外邊框之間的相續距離。node

  offsetWidth:包括元素的內容區寬度、左右內邊距寬度、左右邊框寬度、垂直方向滾動條的寬度之和。web

  offsetHeight:包括元素內容區高度、左右內邊距高度、左右邊框高度、水平方向滾動條的高度之和。瀏覽器

  包含元素的引用在offsetParent屬性中,offsetParent屬性不必定與parentNode屬性相同,好比<td>的offsetParent是<table>而不是<tr>.app

  客戶區大小:clientWidth、clientHeightdom

  clientWidth:元素的內容區寬度+內邊距寬度ide

  clientHeight:元素的內容區高度+內邊距高度函數

  滾動大小:scrollTop、scrollLeft、scrollWidth、scrollHeight。滾動大小指的是包含滾動內容的元素大小。

  scrollTop:被隱藏在內容區域上方的像素數。

  scrollLeft:被隱藏在內容區域左側的像素數。

  經過設置以上兩個屬性能夠改變元素的滾動位置。

  scrollWidth:在沒有滾動條狀況下,元素的內容的寬度。

  scrollHeight:在沒有滾動條狀況下,元素內容的高度。

 

  以上基礎知識,對咱們分析dom-geometry模塊的代碼會有很多幫助。下面咱們進入源碼學習階段。

  dom-geometry模塊封裝了許多跟盒式模型相關的函數,主要涉及:content、padding、border、margin四方面。在前面的幾篇文章中咱們屢次提到,前端js庫中對dom操做的封裝最終都是要用到DOM原生的API。在此模塊中,最經常使用的原生方法就是elemet.ownerDocument.defaultView.getComputedStyle和element.getBoundingClientRect。儘管這兩個方法都存在着兼容性問題,但咱們都有適當的方法來解決。

   getComputedStyle方法已經在dom-style模塊中介紹過(ie中使用element.currentStyle其餘瀏覽器利用原生的getComputedStyle,在webkit中對於不在正常文檔流中的元素先改變display),這裏簡單看一下:

 1 if(has("webkit")){
 2         getComputedStyle = function(/*DomNode*/ node){
 3             var s;
 4             if(node.nodeType == 1){
 5                 var dv = node.ownerDocument.defaultView;
 6                 s = dv.getComputedStyle(node, null);
 7                 if(!s && node.style){
 8                     node.style.display = "";
 9                     s = dv.getComputedStyle(node, null);
10                 }
11             }
12             return s || {};
13         };
14     }else if(has("ie") && (has("ie") < 9 || has("quirks"))){
15         getComputedStyle = function(node){
16             // IE (as of 7) doesn't expose Element like sane browsers
17             // currentStyle can be null on IE8!
18             return node.nodeType == 1 /* ELEMENT_NODE*/ && node.currentStyle ? node.currentStyle : {};
19         };
20     }else{
21         getComputedStyle = function(node){
22             return node.nodeType == 1 /* ELEMENT_NODE*/ ?
23                 node.ownerDocument.defaultView.getComputedStyle(node, null) : {};
24         };
25     }
26     style.getComputedStyle = getComputedStyle;
View Code

  getComputedStyle獲得的某些計算後樣式是帶有單位的,咱們要把單位去掉。這裏依賴dom-style中的toPixelValue方法:

 1 var toPixel;
 2     if(!has("ie")){
 3         toPixel = function(element, value){
 4             // style values can be floats, client code may want
 5             // to round for integer pixels.
 6             return parseFloat(value) || 0;
 7         };
 8     }else{
 9         toPixel = function(element, avalue){
10             if(!avalue){ return 0; }
11             // on IE7, medium is usually 4 pixels
12             if(avalue == "medium"){ return 4; }
13             // style values can be floats, client code may
14             // want to round this value for integer pixels.
15             if(avalue.slice && avalue.slice(-2) == 'px'){ return parseFloat(avalue); }
16             var s = element.style, rs = element.runtimeStyle, cs = element.currentStyle,
17                 sLeft = s.left, rsLeft = rs.left;
18             rs.left = cs.left;
19             try{
20                 // 'avalue' may be incompatible with style.left, which can cause IE to throw
21                 // this has been observed for border widths using "thin", "medium", "thick" constants
22                 // those particular constants could be trapped by a lookup
23                 // but perhaps there are more
24                 s.left = avalue;
25                 avalue = s.pixelLeft;
26             }catch(e){
27                 avalue = 0;
28             }
29             s.left = sLeft;
30             rs.left = rsLeft;
31             return avalue;
32         };
33     }
34     style.toPixelValue = toPixel;
View Code

  函數有點複雜,對於ie瀏覽器只要看懂這句就行:if(avalue.slice && avalue.slice(-2) == 'px'){ return parseFloat(avalue); }

 

  回到dom-geometry的源碼,geom.boxModel變量表明當前瀏覽器中對元素使用的盒式模型,默認爲content-box,同時判斷了ie瀏覽器下的狀況:

var geom = {
        // summary:
        //        This module defines the core dojo DOM geometry API.
    };

// can be either:
    //    "border-box"
    //    "content-box" (default)
    geom.boxModel = "content-box";

if(has("ie") /*|| has("opera")*/){
        // client code may have to adjust if compatMode varies across iframes
        geom.boxModel = document.compatMode == "BackCompat" ? "border-box" : "content-box";
    }

  接下來的幾個函數比較簡單、基礎,經過getComputedStyle都能直接拿到相應屬性:

  getPadExtents():getComputedStyle後獲得paddingLeft、paddingRight、paddingTop、paddingBottom

1 geom.getPadExtents = function getPadExtents(/*DomNode*/ node, /*Object*/ computedStyle){
2 
3         node = dom.byId(node);
4         var s = computedStyle || style.getComputedStyle(node), px = style.toPixelValue,
5             l = px(node, s.paddingLeft), t = px(node, s.paddingTop), r = px(node, s.paddingRight), b = px(node, s.paddingBottom);
6         return {l: l, t: t, r: r, b: b, w: l + r, h: t + b};
7     };
View Code

  getBorderExtents():getComputedStyle後獲得borderLeftWidth、borderRightWidth、borderTopWidth、borderBottomWidth;同時若是border-style設置爲none,border寬度爲零

1 geom.getBorderExtents = function getBorderExtents(/*DomNode*/ node, /*Object*/ computedStyle){
2         node = dom.byId(node);
3         var px = style.toPixelValue, s = computedStyle || style.getComputedStyle(node),
4             l = s.borderLeftStyle != none ? px(node, s.borderLeftWidth) : 0,
5             t = s.borderTopStyle != none ? px(node, s.borderTopWidth) : 0,
6             r = s.borderRightStyle != none ? px(node, s.borderRightWidth) : 0,
7             b = s.borderBottomStyle != none ? px(node, s.borderBottomWidth) : 0;
8         return {l: l, t: t, r: r, b: b, w: l + r, h: t + b};
9     };
View Code

  getPadBorderExtents():經過上兩個方法,pad+border

 1 geom.getPadBorderExtents = function getPadBorderExtents(/*DomNode*/ node, /*Object*/ computedStyle){
 2         node = dom.byId(node);
 3         var s = computedStyle || style.getComputedStyle(node),
 4             p = geom.getPadExtents(node, s),
 5             b = geom.getBorderExtents(node, s);
 6         return {
 7             l: p.l + b.l,
 8             t: p.t + b.t,
 9             r: p.r + b.r,
10             b: p.b + b.b,
11             w: p.w + b.w,
12             h: p.h + b.h
13         };
14     };
View Code

  getMarginExtents():getComputedStyle後獲得marginLeft、marginRight、marginTop、marginBottom

1 geom.getMarginExtents = function getMarginExtents(node, computedStyle){
2         node = dom.byId(node);
3         var s = computedStyle || style.getComputedStyle(node), px = style.toPixelValue,
4             l = px(node, s.marginLeft), t = px(node, s.marginTop), r = px(node, s.marginRight), b = px(node, s.marginBottom);
5         return {l: l, t: t, r: r, b: b, w: l + r, h: t + b};
6     };
View Code

   

  下面的幾個函數稍微有點複雜

  getMarginBox()這個方法返回一個對象

{
  t: 父元素上內邊框到元素上外邊距的距離,
  l: 父元素左內邊框到元素左外邊距的距離,
  w: 元素左外邊距到右外邊距的距離,
  h: 元素上外邊距到下外邊距的距離        
}

  這個函數中主要用到上文提到的偏移量,正常狀況下:

  t = offsetTop,

  l = offsetLeft,

  w = offsetWidth + marginExtents.w,

  h = offsetHeight + marginExtents.h

  在這個函數中有幾個兼容性問題:

  一、在firefox中,若是元素的overflow樣子的計算值不爲visible,那麼offsetLeft/offsetTop獲得的值是減去borderLeftStyle/borderTopStyle後的值。這應該是firefox的bug,因此咱們要對此進行修復。若是getComputedStyle中可以獲得left和top那就用這兩個屬性代替offsetLeft和offsetTop,不然計算parentNode的border寬度,手動加上這部分值

 1 if(has("mozilla")){
 2             // Mozilla:
 3             // If offsetParent has a computed overflow != visible, the offsetLeft is decreased
 4             // by the parent's border.
 5             // We don't want to compute the parent's style, so instead we examine node's
 6             // computed left/top which is more stable.
 7             var sl = parseFloat(s.left), st = parseFloat(s.top);
 8             if(!isNaN(sl) && !isNaN(st)){
 9                 l = sl;
10                 t = st;
11             }else{
12                 // If child's computed left/top are not parseable as a number (e.g. "auto"), we
13                 // have no choice but to examine the parent's computed style.
14                 if(p && p.style){
15                     pcs = style.getComputedStyle(p);
16                     if(pcs.overflow != "visible"){
17                         l += pcs.borderLeftStyle != none ? px(node, pcs.borderLeftWidth) : 0;
18                         t += pcs.borderTopStyle != none ? px(node, pcs.borderTopWidth) : 0;
19                     }
20                 }
21             }
22         }
View Code

  二、在IE8和opera中狀況正好相反,offsetLeft/offsetTop包含了父元素的邊框,這裏咱們須要把他們減去

1 if(has("opera") || (has("ie") == 8 && !has("quirks"))){
2             // On Opera and IE 8, offsetLeft/Top includes the parent's border
3             if(p){
4                 pcs = style.getComputedStyle(p);
5                 l -= pcs.borderLeftStyle != none ? px(node, pcs.borderLeftWidth) : 0;
6                 t -= pcs.borderTopStyle != none ? px(node, pcs.borderTopWidth) : 0;
7             }
8         }
View Code

  真個函數代碼以下:

 1 geom.getMarginBox = function getMarginBox(/*DomNode*/ node, /*Object*/ computedStyle){
 2         node = dom.byId(node);
 3         var s = computedStyle || style.getComputedStyle(node), me = geom.getMarginExtents(node, s),
 4             l = node.offsetLeft - me.l, t = node.offsetTop - me.t, p = node.parentNode, px = style.toPixelValue, pcs;
 5         if(has("mozilla")){
 6             var sl = parseFloat(s.left), st = parseFloat(s.top);
 7             if(!isNaN(sl) && !isNaN(st)){
 8                 l = sl;
 9                 t = st;
10             }else{
11                 if(p && p.style){
12                     pcs = style.getComputedStyle(p);
13                     if(pcs.overflow != "visible"){
14                         l += pcs.borderLeftStyle != none ? px(node, pcs.borderLeftWidth) : 0;
15                         t += pcs.borderTopStyle != none ? px(node, pcs.borderTopWidth) : 0;
16                     }
17                 }
18             }
19         }else if(has("opera") || (has("ie") == 8 && !has("quirks"))){
20             if(p){
21                 pcs = style.getComputedStyle(p);
22                 l -= pcs.borderLeftStyle != none ? px(node, pcs.borderLeftWidth) : 0;
23                 t -= pcs.borderTopStyle != none ? px(node, pcs.borderTopWidth) : 0;
24             }
25         }
26         return {l: l, t: t, w: node.offsetWidth + me.w, h: node.offsetHeight + me.h};
27     };
View Code

  

  getContentBox()函數返回以下對象:

{
l: 元素左內邊距,
t: 元素上內邊距,
w: 元素內容區的寬度,
h: 元素內容區的高度  
}

  對象中的w和h與元素的盒式模型無關。之內容區的寬高都有兩套方案:clientWidth-padingWidth或者offsetWidth-paddingWidth-borderWidth,下面是函數的源碼:

 1 geom.getContentBox = function getContentBox(node, computedStyle){
 2         // summary:
 3         //        Returns an object that encodes the width, height, left and top
 4         //        positions of the node's content box, irrespective of the
 5         //        current box model.
 6         // node: DOMNode
 7         // computedStyle: Object?
 8         //        This parameter accepts computed styles object.
 9         //        If this parameter is omitted, the functions will call
10         //        dojo/dom-style.getComputedStyle to get one. It is a better way, calling
11         //        dojo/dom-style.getComputedStyle once, and then pass the reference to this
12         //        computedStyle parameter. Wherever possible, reuse the returned
13         //        object of dojo/dom-style.getComputedStyle().
14 
15         // clientWidth/Height are important since the automatically account for scrollbars
16         // fallback to offsetWidth/Height for special cases (see #3378)
17         node = dom.byId(node);
18         var s = computedStyle || style.getComputedStyle(node), w = node.clientWidth, h,
19             pe = geom.getPadExtents(node, s), be = geom.getBorderExtents(node, s);
20         if(!w){
21             w = node.offsetWidth;
22             h = node.offsetHeight;
23         }else{
24             h = node.clientHeight;
25             be.w = be.h = 0;
26         }
27         // On Opera, offsetLeft includes the parent's border
28         if(has("opera")){
29             pe.l += be.l;
30             pe.t += be.t;
31         }
32         return {l: pe.l, t: pe.t, w: w - pe.w - be.w, h: h - pe.h - be.h};
33     };
View Code

 

  接下來有三個私有函數setBox、isButtonTag、usersBorderBox。

  setBox忽略盒式模型,直接對元素樣式的width、height、left、top進行設置

 1 function setBox(/*DomNode*/ node, /*Number?*/ l, /*Number?*/ t, /*Number?*/ w, /*Number?*/ h, /*String?*/ u){
 2         // summary:
 3         //        sets width/height/left/top in the current (native) box-model
 4         //        dimensions. Uses the unit passed in u.
 5         // node:
 6         //        DOM Node reference. Id string not supported for performance
 7         //        reasons.
 8         // l:
 9         //        left offset from parent.
10         // t:
11         //        top offset from parent.
12         // w:
13         //        width in current box model.
14         // h:
15         //        width in current box model.
16         // u:
17         //        unit measure to use for other measures. Defaults to "px".
18         u = u || "px";
19         var s = node.style;
20         if(!isNaN(l)){
21             s.left = l + u;
22         }
23         if(!isNaN(t)){
24             s.top = t + u;
25         }
26         if(w >= 0){
27             s.width = w + u;
28         }
29         if(h >= 0){
30             s.height = h + u;
31         }
32     }
View Code

  isButtonTag函數用來判斷元素是不是button按鈕,button元素多是直接的<button>標籤,也多是<input type="button">,因此要對着兩方面進行判斷

1 function isButtonTag(/*DomNode*/ node){
2         // summary:
3         //        True if the node is BUTTON or INPUT.type="button".
4         return node.tagName.toLowerCase() == "button" ||
5             node.tagName.toLowerCase() == "input" && (node.getAttribute("type") || "").toLowerCase() == "button"; // boolean
6     }
View Code

  usersBorderBox判斷元素的盒式模型是否爲border-box,三個方面:geom的boxModel是否爲border-box、元素是否爲table元素,元素是否爲button元素

 1 function usesBorderBox(/*DomNode*/ node){
 2         // summary:
 3         //        True if the node uses border-box layout.
 4 
 5         // We could test the computed style of node to see if a particular box
 6         // has been specified, but there are details and we choose not to bother.
 7 
 8         // TABLE and BUTTON (and INPUT type=button) are always border-box by default.
 9         // If you have assigned a different box to either one via CSS then
10         // box functions will break.
11 
12         return geom.boxModel == "border-box" || node.tagName.toLowerCase() == "table" || isButtonTag(node); // boolean
13     }
View Code

 

  setContentSize方法,設置元素內容區的大小。若是元素盒式模式是border-box,則須要在參數傳入的width基礎上加上padding與border的寬度,不然直接設置width、height樣式。

 1 geom.setContentSize = function setContentSize(/*DomNode*/ node, /*Object*/ box, /*Object*/ computedStyle){
 2         // summary:
 3         //        Sets the size of the node's contents, irrespective of margins,
 4         //        padding, or borders.
 5         // node: DOMNode
 6         // box: Object
 7         //        hash with optional "w", and "h" properties for "width", and "height"
 8         //        respectively. All specified properties should have numeric values in whole pixels.
 9         // computedStyle: Object?
10         //        This parameter accepts computed styles object.
11         //        If this parameter is omitted, the functions will call
12         //        dojo/dom-style.getComputedStyle to get one. It is a better way, calling
13         //        dojo/dom-style.getComputedStyle once, and then pass the reference to this
14         //        computedStyle parameter. Wherever possible, reuse the returned
15         //        object of dojo/dom-style.getComputedStyle().
16 
17         node = dom.byId(node);
18         var w = box.w, h = box.h;
19         if(usesBorderBox(node)){
20             var pb = geom.getPadBorderExtents(node, computedStyle);
21             if(w >= 0){
22                 w += pb.w;
23             }
24             if(h >= 0){
25                 h += pb.h;
26             }
27         }
28         setBox(node, NaN, NaN, w, h);
29     };
View Code

  setMarginBox方法,設置marginBox的寬度。該方法中不去判斷元素的盒式模型,width = w-padding - border -margin。經過這種方式直接設置元素的width或height屬性。這裏涉及的兼容性問題,主要對於低版本瀏覽器,因此不去分析他。

 1 geom.setMarginBox = function setMarginBox(/*DomNode*/ node, /*Object*/ box, /*Object*/ computedStyle){
 2         // summary:
 3         //        sets the size of the node's margin box and placement
 4         //        (left/top), irrespective of box model. Think of it as a
 5         //        passthrough to setBox that handles box-model vagaries for
 6         //        you.
 7         // node: DOMNode
 8         // box: Object
 9         //        hash with optional "l", "t", "w", and "h" properties for "left", "right", "width", and "height"
10         //        respectively. All specified properties should have numeric values in whole pixels.
11         // computedStyle: Object?
12         //        This parameter accepts computed styles object.
13         //        If this parameter is omitted, the functions will call
14         //        dojo/dom-style.getComputedStyle to get one. It is a better way, calling
15         //        dojo/dom-style.getComputedStyle once, and then pass the reference to this
16         //        computedStyle parameter. Wherever possible, reuse the returned
17         //        object of dojo/dom-style.getComputedStyle().
18 
19         node = dom.byId(node);
20         var s = computedStyle || style.getComputedStyle(node), w = box.w, h = box.h,
21         // Some elements have special padding, margin, and box-model settings.
22         // To use box functions you may need to set padding, margin explicitly.
23         // Controlling box-model is harder, in a pinch you might set dojo/dom-geometry.boxModel.
24             pb = usesBorderBox(node) ? nilExtents : geom.getPadBorderExtents(node, s),
25             mb = geom.getMarginExtents(node, s);
26         if(has("webkit")){
27             // on Safari (3.1.2), button nodes with no explicit size have a default margin
28             // setting an explicit size eliminates the margin.
29             // We have to swizzle the width to get correct margin reading.
30             if(isButtonTag(node)){
31                 var ns = node.style;
32                 if(w >= 0 && !ns.width){
33                     ns.width = "4px";
34                 }
35                 if(h >= 0 && !ns.height){
36                     ns.height = "4px";
37                 }
38             }
39         }
40         if(w >= 0){
41             w = Math.max(w - pb.w - mb.w, 0);
42         }
43         if(h >= 0){
44             h = Math.max(h - pb.h - mb.h, 0);
45         }
46         setBox(node, box.l, box.t, w, h);
47     };
View Code

   position()方法,主要使用node.getBoundingClientRect() ,這個方法獲得left、right、top、bottom。在老版本ie下,這個方法的基準點並非從(0,0)開始計算的,而是以(2,2)位基準點。因此ie中這個方法獲得的位置信息比實際位置多了兩個像素,咱們要把這兩個像素減掉。基準點位置偏移兩個像素,因此dcument.documentElement即<html>標籤的位置也不是0;因此咱們能夠利用document.documentElement.getBoundingClientRect().left/top獲得偏移量。減去偏移量就獲得了真正的位置。(偏移量問題在IE9已經修復了,而IE8標準模式是沒有這個問題,因此具體獲取偏移量的細節不討論)

 1 geom.position = function(/*DomNode*/ node, /*Boolean?*/ includeScroll){
 2         // summary:
 3         //        Gets the position and size of the passed element relative to
 4         //        the viewport (if includeScroll==false), or relative to the
 5         //        document root (if includeScroll==true).
 6         //
 7         // description:
 8         //        Returns an object of the form:
 9         //        `{ x: 100, y: 300, w: 20, h: 15 }`.
10         //        If includeScroll==true, the x and y values will include any
11         //        document offsets that may affect the position relative to the
12         //        viewport.
13         //        Uses the border-box model (inclusive of border and padding but
14         //        not margin).  Does not act as a setter.
15         // node: DOMNode|String
16         // includeScroll: Boolean?
17         // returns: Object
18 
19         node = dom.byId(node);
20         var    db = win.body(node.ownerDocument),
21             ret = node.getBoundingClientRect();
22         ret = {x: ret.left, y: ret.top, w: ret.right - ret.left, h: ret.bottom - ret.top};
23 
24         if(has("ie") < 9){
25             // On IE<9 there's a 2px offset that we need to adjust for, see dojo.getIeDocumentElementOffset()
26             var offset = geom.getIeDocumentElementOffset(node.ownerDocument);
27 
28             // fixes the position in IE, quirks mode
29             ret.x -= offset.x + (has("quirks") ? db.clientLeft + db.offsetLeft : 0);
30             ret.y -= offset.y + (has("quirks") ? db.clientTop + db.offsetTop : 0);
31         }
32 
33         // account for document scrolling
34         // if offsetParent is used, ret value already includes scroll position
35         // so we may have to actually remove that value if !includeScroll
36         if(includeScroll){
37             var scroll = geom.docScroll(node.ownerDocument);
38             ret.x += scroll.x;
39             ret.y += scroll.y;
40         }
41 
42         return ret; // Object
43     };
View Code

  normalizeEvent()方法主要針對鼠標位置信息layerX/layerY、pageX/pageY作修正;前者利用ie中的offsetX/offsetY便可,後者利用clientX+documentElement/body.scrollLeft - offset和clientY+documentElement/body.scrollTop - offset,offset便是上文提到的在ie中偏移量。

geom.normalizeEvent = function(event){
        // summary:
        //        Normalizes the geometry of a DOM event, normalizing the pageX, pageY,
        //        offsetX, offsetY, layerX, and layerX properties
        // event: Object
        if(!("layerX" in event)){
            event.layerX = event.offsetX;
            event.layerY = event.offsetY;
        }
        if(!has("dom-addeventlistener")){
            // old IE version
            // FIXME: scroll position query is duped from dojo/_base/html to
            // avoid dependency on that entire module. Now that HTML is in
            // Base, we should convert back to something similar there.
            var se = event.target;
            var doc = (se && se.ownerDocument) || document;
            // DO NOT replace the following to use dojo/_base/window.body(), in IE, document.documentElement should be used
            // here rather than document.body
            var docBody = has("quirks") ? doc.body : doc.documentElement;
            var offset = geom.getIeDocumentElementOffset(doc);
            event.pageX = event.clientX + geom.fixIeBiDiScrollLeft(docBody.scrollLeft || 0, doc) - offset.x;
            event.pageY = event.clientY + (docBody.scrollTop || 0) - offset.y;
        }
    };
View Code

 

  一個周的學習研究結束,若是您以爲這篇文章對您有幫助,請不吝點擊下方推薦

相關文章
相關標籤/搜索