最近要写前端组件了,狂砍各种组件源码,这里分析一款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) );