最近要写前端组件了,狂砍各种组件源码,这里分析一款jqueryui中的posistion插件,注意,它不是jqueryui widget,首先看下源码总体结构图



1、看到$.fn.position 是不是很熟悉?嗯,就是将position方法挂载到原型上,然后控件就可以直接调用了,

2、$.ui.position 这个对象是,用来进行冲突判断的,什么冲突?就是元素与父容器所拥有的空间以及当前可用窗口的控件,默认情形下,如果冲突则采用反转方向的方式显示;对这一点不要惊讶,一切都是为了正常显示而用的

3、源码里废话较多,部分可以忽略,慢慢读


/*!
 * jQuery UI Position @VERSION
 * http://jqueryui.com
 *
 * Copyright 2014 jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 *
 * http://api.jqueryui.com/position/
 */
(function ($, undefined) {

  $.ui = $.ui || {};

  var cachedScrollbarWidth,
    max = Math.max,
    abs = Math.abs,
    round = Math.round,
    rhorizontal = /left|center|right/,
    rvertical = /top|center|bottom/,
    roffset = /[\+\-]\d+(\.[\d]+)?%?/,
    rposition = /^\w+/,
    rpercent = /%$/, //检测是否以xx%结尾
    _position = $.fn.position; //保存旧的position功能,如果需要用的话

  function getOffsets(offsets, width, height) {
//    debugger
//    这块代码基本是都是0,0
    return [
      parseFloat(offsets[ 0 ]) * ( rpercent.test(offsets[ 0 ]) ? width / 100 : 1 ), //检测是否以xx%结尾
      parseFloat(offsets[ 1 ]) * ( rpercent.test(offsets[ 1 ]) ? height / 100 : 1 )
    ];
  }

  function parseCss(element, property) {
//    debugger
//    获取css属性方法
    return parseInt($.css(element, property), 10) || 0;
  }

  /**
   * 获取元素的尺寸
   * @param elem
   * @returns {*}
   */
  function getDimensions(elem) {

    var raw = elem[0];
    if (raw.nodeType === 9) {//Node.DOCUMENT_NODE
      return {
        width: elem.width(),
        height: elem.height(),
        offset: { top: 0, left: 0 }
      };
    }
    if ($.isWindow(raw)) {
      return {
        width: elem.width(),
        height: elem.height(),
        offset: { top: elem.scrollTop(), left: elem.scrollLeft() }
      };
    }
    if (raw.preventDefault) {
      return {
        width: 0,
        height: 0,
        offset: { top: raw.pageY, left: raw.pageX }
      };
    }
    return {
      width: elem.outerWidth(),
      height: elem.outerHeight(),
      offset: elem.offset()
    };
  }

  $.position = {
    /**
     * 计算滚动条的宽度
     * @returns {*}
     */
    scrollbarWidth: function () {
//      debugger

      if (cachedScrollbarWidth !== undefined) {
        return cachedScrollbarWidth;
      }
      var w1, w2,
        div = $("<div style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>"),
        innerDiv = div.children()[0];

      $("body").append(div);
      w1 = innerDiv.offsetWidth;
      div.css("overflow", "scroll");

      w2 = innerDiv.offsetWidth;

      if (w1 === w2) {
        w2 = div[0].clientWidth;
      }

      div.remove();
      //if you want to see 'div', you can comment this line

      return (cachedScrollbarWidth = w1 - w2);
    },

    /**
     * 获取滚动信息
     * @param within
     * @returns {{width: *, height: *}}
     */
    getScrollInfo: function (within) {
//      debugger

      var overflowX = within.isWindow || within.isDocument ? "" :
          within.element.css("overflow-x"),
        overflowY = within.isWindow || within.isDocument ? "" :
          within.element.css("overflow-y"),
        hasOverflowX = overflowX === "scroll" ||
          ( overflowX === "auto" && within.width < within.element[0].scrollWidth ),
        hasOverflowY = overflowY === "scroll" ||
          ( overflowY === "auto" && within.height < within.element[0].scrollHeight );
      return {
        width: hasOverflowY ? $.position.scrollbarWidth() : 0,
        height: hasOverflowX ? $.position.scrollbarWidth() : 0
      };
    },
    /**
     * 获取内部信息,封装过一层
     * @param element
     * @returns {{element: (*|HTMLElement), isWindow: *, isDocument: boolean, offset: (*|{left: number, top: number}), scrollLeft: (jQuery.scrollLeft|*), scrollTop: (jQuery.scrollTop|*), width: *, height: *}}
     */
    getWithinInfo: function (element) {
//      debugger

      var withinElement = $(element || window),
        isWindow = $.isWindow(withinElement[0]),
        isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9;
      return {
        element: withinElement,
        isWindow: isWindow,
        isDocument: isDocument,
        offset: withinElement.offset() || { left: 0, top: 0 },
        scrollLeft: withinElement.scrollLeft(),
        scrollTop: withinElement.scrollTop(),
        width: isWindow ? withinElement.width() : withinElement.outerWidth(),
        height: isWindow ? withinElement.height() : withinElement.outerHeight()
      };
    }
  };

  $.fn.position = function (options) {

    debugger
    console.log('invoke position');

    if (!options || !options.of) {
      //第二次是jQuery.offset.setOffset:function( elem, options, i )中发起的调用 options中没有of属性F
      //
      return _position.apply(this, arguments); //_position为原始的$.fn.position(jquery中,非jqueryui中的)
    }

    console.log('invoke position do something');

    // make a copy, we don't want to modify arguments
    options = $.extend({}, options);

    var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions,
      target = $(options.of), //父容器
      within = $.position.getWithinInfo(options.within),
      scrollInfo = $.position.getScrollInfo(within),
      collision = ( options.collision || "flip" ).split(" "),
      offsets = {};

    dimensions = getDimensions(target);//父容器尺寸
    if (target[0].preventDefault) {
      // force left top to allow flipping
      options.at = "left top";
    }
    targetWidth = dimensions.width;
    targetHeight = dimensions.height;
    targetOffset = dimensions.offset;
    // clone to reuse original targetOffset later
    basePosition = $.extend({}, targetOffset); //父容器的基准偏移量

    // force my and at to have valid horizontal and vertical positions
    // if a value is missing or invalid, it will be converted to center
    //pass it
    $.each([ "my", "at" ], function () {
      var pos = ( options[ this ] || "" ).split(" "),
        horizontalOffset,
        verticalOffset;

      if (pos.length === 1) {
        pos = rhorizontal.test(pos[ 0 ]) ?
          pos.concat([ "center" ]) :
          rvertical.test(pos[ 0 ]) ?
            [ "center" ].concat(pos) :
            [ "center", "center" ];
      }
      pos[ 0 ] = rhorizontal.test(pos[ 0 ]) ? pos[ 0 ] : "center";
      pos[ 1 ] = rvertical.test(pos[ 1 ]) ? pos[ 1 ] : "center";

      // calculate offsets
//      debugger
      // 这里可以带有运算
      horizontalOffset = roffset.exec(pos[ 0 ]);
      verticalOffset = roffset.exec(pos[ 1 ]);
      offsets[ this ] = [
        horizontalOffset ? horizontalOffset[ 0 ] : 0,
        verticalOffset ? verticalOffset[ 0 ] : 0
      ];

      // reduce to just the positions without the offsets
      options[ this ] = [
        rposition.exec(pos[ 0 ])[ 0 ],
        rposition.exec(pos[ 1 ])[ 0 ]
      ];
    });

    // normalize collision option
    if (collision.length === 1) {
      collision[ 1 ] = collision[ 0 ]; //如果只有一个元素,则collision中的两个值都是一样的
    }

    //父元素的‘水平’位置进行定位,计算值中默认是left,所以处理下right、center
    if (options.at[ 0 ] === "right") {
      basePosition.left += targetWidth;
    } else if (options.at[ 0 ] === "center") {
      basePosition.left += targetWidth / 2;
    }

    //父元素的‘垂直’位置进行定位,
    if (options.at[ 1 ] === "bottom") {
      basePosition.top += targetHeight;
    } else if (options.at[ 1 ] === "center") {
      basePosition.top += targetHeight / 2;
    }

    atOffset = getOffsets(offsets.at, targetWidth, targetHeight);
    basePosition.left += atOffset[ 0 ];
    basePosition.top += atOffset[ 1 ];

    //this is jQuery Object. This is a jQuery plugin ,not jqueryui widget!
    return this.each(function () {
//      debugger
      var collisionPosition, using,
        elem = $(this), //jQuery object
        elemWidth = elem.outerWidth(), //包括内边距和边框
        elemHeight = elem.outerHeight(),
        marginLeft = parseCss(this, "marginLeft"),
        marginTop = parseCss(this, "marginTop"),
        collisionWidth = elemWidth + marginLeft + parseCss(this, "marginRight") + scrollInfo.width,
        collisionHeight = elemHeight + marginTop + parseCss(this, "marginBottom") + scrollInfo.height,
        position = $.extend({}, basePosition),
        myOffset = getOffsets(offsets.my, elem.outerWidth(), elem.outerHeight());
      //my: $( "#my_horizontal" ).val() + "+5 " + $( "#my_vertical" ).val()+'+5',
      // 定义增加的

      //子(目标)元素的‘水平’位置定位计算
      if (options.my[ 0 ] === "right") {
        position.left -= elemWidth;
      } else if (options.my[ 0 ] === "center") {
        position.left -= elemWidth / 2;
      }

      //子(目标)元素的‘垂直’位置定位计算
      if (options.my[ 1 ] === "bottom") {
        position.top -= elemHeight;
      } else if (options.my[ 1 ] === "center") {
        position.top -= elemHeight / 2;
      }

      //加上用户自定义变化量
      position.left += myOffset[ 0 ];
      position.top += myOffset[ 1 ];

      // if the browser doesn't support fractions, then round for consistent results
      if (!$.support.offsetFractions) { //如果浏览器不支持小数,则四舍五入
        position.left = round(position.left);
        position.top = round(position.top);
      }

      collisionPosition = {
        marginLeft: marginLeft,
        marginTop: marginTop
      };

      //这里为什么会只遍历left,top,因为只要有left和top,我们就可以进行定位了
      $.each([ "left", "top" ], function (i, dir) {

        //collision is ['flip'] by default
        if ($.ui.position[ collision[ i ] ]) {
          $.ui.position[ collision[ i ] ][ dir ](position, {
            targetWidth: targetWidth,
            targetHeight: targetHeight,
            elemWidth: elemWidth,
            elemHeight: elemHeight,
            collisionPosition: collisionPosition,
            collisionWidth: collisionWidth,
            collisionHeight: collisionHeight,
            offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ],
            my: options.my,
            at: options.at,
            within: within,
            elem: elem
          });
        }

      });

      if (options.using) {
        // adds feedback as second argument to using callback, if present
        using = function (props) {
          var left = targetOffset.left - position.left,
            right = left + targetWidth - elemWidth,
            top = targetOffset.top - position.top,
            bottom = top + targetHeight - elemHeight,
            feedback = {
              target: {
                element: target,
                left: targetOffset.left,
                top: targetOffset.top,
                width: targetWidth,
                height: targetHeight
              },
              element: {
                element: elem,
                left: position.left,
                top: position.top,
                width: elemWidth,
                height: elemHeight
              },
              horizontal: right < 0 ? "left" : left > 0 ? "right" : "center",
              vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle"
            };
          if (targetWidth < elemWidth && abs(left + right) < targetWidth) {
            feedback.horizontal = "center";
          }
          if (targetHeight < elemHeight && abs(top + bottom) < targetHeight) {
            feedback.vertical = "middle";
          }
          if (max(abs(left), abs(right)) > max(abs(top), abs(bottom))) {
            feedback.important = "horizontal";
          } else {
            feedback.important = "vertical";
          }
          options.using.call(this, props, feedback);
        };
      }

      console.log(position);

      elem.offset($.extend(position, { using: using }));
    });
  };

  /**
   * 进行冲突判断的解决方案
   *
   * @type {{fit: {left: Function, top: Function}, flip: {left: Function, top: Function}, flipfit: {left: Function, top: Function}}}
   */
  $.ui.position = {
    fit: {
      left: function (position, data) {
        var within = data.within,
          withinOffset = within.isWindow ? within.scrollLeft : within.offset.left,
          outerWidth = within.width,
          collisionPosLeft = position.left - data.collisionPosition.marginLeft,
          overLeft = withinOffset - collisionPosLeft,
          overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset,
          newOverRight;

        // element is wider than within
        if (data.collisionWidth > outerWidth) {
          // element is initially over the left side of within
          if (overLeft > 0 && overRight <= 0) {
            newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset;
            position.left += overLeft - newOverRight;
            // element is initially over right side of within
          } else if (overRight > 0 && overLeft <= 0) {
            position.left = withinOffset;
            // element is initially over both left and right sides of within
          } else {
            if (overLeft > overRight) {
              position.left = withinOffset + outerWidth - data.collisionWidth;
            } else {
              position.left = withinOffset;
            }
          }
          // too far left -> align with left edge
        } else if (overLeft > 0) {
          position.left += overLeft;
          // too far right -> align with right edge
        } else if (overRight > 0) {
          position.left -= overRight;
          // adjust based on position and margin
        } else {
          position.left = max(position.left - collisionPosLeft, position.left);
        }
      },
      top: function (position, data) {
        var within = data.within,
          withinOffset = within.isWindow ? within.scrollTop : within.offset.top,
          outerHeight = data.within.height,
          collisionPosTop = position.top - data.collisionPosition.marginTop,
          overTop = withinOffset - collisionPosTop,
          overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset,
          newOverBottom;

        // element is taller than within
        if (data.collisionHeight > outerHeight) {
          // element is initially over the top of within
          if (overTop > 0 && overBottom <= 0) {
            newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset;
            position.top += overTop - newOverBottom;
            // element is initially over bottom of within
          } else if (overBottom > 0 && overTop <= 0) {
            position.top = withinOffset;
            // element is initially over both top and bottom of within
          } else {
            if (overTop > overBottom) {
              position.top = withinOffset + outerHeight - data.collisionHeight;
            } else {
              position.top = withinOffset;
            }
          }
          // too far up -> align with top
        } else if (overTop > 0) {
          position.top += overTop;
          // too far down -> align with bottom edge
        } else if (overBottom > 0) {
          position.top -= overBottom;
          // adjust based on position and margin
        } else {
          position.top = max(position.top - collisionPosTop, position.top);
        }
      }
    },
    flip: {
      left: function (position, data) {
        debugger

        var within = data.within,
          withinOffset = within.offset.left + within.scrollLeft,
          outerWidth = within.width,
          offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left,
          collisionPosLeft = position.left - data.collisionPosition.marginLeft,

          overLeft = collisionPosLeft - offsetLeft, //judgement
          overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft,//judgement

          myOffset = data.my[ 0 ] === "left" ? -data.elemWidth : data.my[ 0 ] === "right" ? data.elemWidth : 0,
          atOffset = data.at[ 0 ] === "left" ? data.targetWidth : data.at[ 0 ] === "right" ? -data.targetWidth : 0,
          offset = -2 * data.offset[ 0 ],
          newOverRight,
          newOverLeft;

        if (overLeft < 0) {
          newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset;
          if (newOverRight < 0 || newOverRight < abs(overLeft)) {
            position.left += myOffset + atOffset + offset;
          }
        }
        else if (overRight > 0) {
          newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft;
          if (newOverLeft > 0 || abs(newOverLeft) < overRight) {
            position.left += myOffset + atOffset + offset;
          }
        }
      },
      top: function (position, data) {

        debugger

        var within = data.within,
          withinOffset = within.offset.top + within.scrollTop,
          outerHeight = within.height,
          offsetTop = within.isWindow ? within.scrollTop : within.offset.top,
          collisionPosTop = position.top - data.collisionPosition.marginTop,

          overTop = collisionPosTop - offsetTop,
          overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop,

          top = data.my[ 1 ] === "top", myOffset = top ? -data.elemHeight : data.my[ 1 ] === "bottom" ? data.elemHeight : 0,
          atOffset = data.at[ 1 ] === "top" ? data.targetHeight : data.at[ 1 ] === "bottom" ? -data.targetHeight : 0,

          offset = -2 * data.offset[ 1 ],
          newOverTop,
          newOverBottom;

        if (overTop < 0) {
          newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset;
          if (( position.top + myOffset + atOffset + offset) > overTop && ( newOverBottom < 0 || newOverBottom < abs(overTop) )) {
            position.top += myOffset + atOffset + offset;
          }
        }
        else if (overBottom > 0) {
          newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop;
          if (( position.top + myOffset + atOffset + offset) > overBottom && ( newOverTop > 0 || abs(newOverTop) < overBottom )) {
            position.top += myOffset + atOffset + offset;
          }
        }
      }
    },
    flipfit: {
      left: function () {
        $.ui.position.flip.left.apply(this, arguments);
        $.ui.position.fit.left.apply(this, arguments);
      },
      top: function () {
        $.ui.position.flip.top.apply(this, arguments);
        $.ui.position.fit.top.apply(this, arguments);
      }
    }
  };

// fraction support test
  (function () {
    var testElement, testElementParent, testElementStyle, offsetLeft, i,
      body = document.getElementsByTagName("body")[ 0 ],
      div = document.createElement("div");

    //Create a "fake body" for testing based on method used in jQuery.support
    testElement = document.createElement(body ? "div" : "body");
    testElementStyle = {
      visibility: "hidden",
      width: 0,
      height: 0,
      border: 0,
      margin: 0,
      background: "none"
    };
    if (body) {
      $.extend(testElementStyle, {
        position: "absolute",
        left: "-1000px",
        top: "-1000px"
      });
    }
    for (i in testElementStyle) {
      testElement.style[ i ] = testElementStyle[ i ];
    }
    testElement.appendChild(div);
    testElementParent = body || document.documentElement;
    testElementParent.insertBefore(testElement, testElementParent.firstChild);

    div.style.cssText = "position: absolute; left: 10.7432222px;";

    offsetLeft = $(div).offset().left;
    $.support.offsetFractions = offsetLeft > 10 && offsetLeft < 11;

    testElement.innerHTML = "";
    testElementParent.removeChild(testElement);
  })();

}(jQuery) );






Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐