现在的位置: 首页 > easyui > Form > combo > 正文
jQuery Easyui 源码分析之combo组件
2012年12月27日 combo, 源码分析 ⁄ 共 12370字 评论数 12 ⁄ 被围观 25,134 views+

combo作为较为基础类的组件,在jQuery Easyui体系中也有非常重要的地位,combobox,datebox等组件都依赖combo组件。话不多说,直接上带有注释的源码:


/** 
 * jQuery EasyUI 1.3.1 
 * 
 * Licensed under the GPL terms To use it on other terms please contact us 
 * 
 * Copyright(c) 2009-2012 stworthy [ stworthy@gmail.com ] 
 * 注释由小雪完成,更多内容参见www.easyui.info
 * 该源码完全由压缩码翻译而来,并非网络上放出的源码,请勿索要。
 */
(function($) {
	function setSize(target, width) {
		var opts = $.data(target, "combo").options;
		var combo = $.data(target, "combo").combo;
		var panel = $.data(target, "combo").panel;
		if (width) {
			opts.width = width;
		}
		combo.appendTo("body");
		if (isNaN(opts.width)) {
			opts.width = combo.find("input.combo-text").outerWidth();
		}
		var arrowWidth = 0;
		if (opts.hasDownArrow) {
			arrowWidth = combo.find(".combo-arrow").outerWidth();
		}
		combo.find("input.combo-text").width(0);
		combo._outerWidth(opts.width);
		combo.find("input.combo-text").width(combo.width() - arrowWidth);
		panel.panel("resize", {
					width : (opts.panelWidth ? opts.panelWidth : combo
							.outerWidth()),
					height : opts.panelHeight
				});
		combo.insertAfter(target);
	};
	/** 
	 * 初始化下拉框按钮(设置其是否显示) 
	 */
	function initArrow(target) {
		var opts = $.data(target, "combo").options;
		var combo = $.data(target, "combo").combo;
		if (opts.hasDownArrow) {
			combo.find(".combo-arrow").show();
		} else {
			combo.find(".combo-arrow").hide();
		}
	};
	/** 
	 * 初始化函数,生成整个combo组件的DOM结构 
	 */
	function init(target) {
		$(target).addClass("combo-f").hide();
		var span = $("<span class=\"combo\"></span>").insertAfter(target);
		var input = $("<input type=\"text\" class=\"combo-text\">")
				.appendTo(span);
		$("<span><span class=\"combo-arrow\"></span></span>").appendTo(span);
		$("<input type=\"hidden\" class=\"combo-value\">").appendTo(span);
		var panel = $("<div class=\"combo-panel\"></div>").appendTo("body");
		panel.panel({
					doSize : false,
					closed : true,
					cls : "combo-p",
					style : {
						position : "absolute",
						zIndex : 10
					},
					onOpen : function() {
						$(this).panel("resize");
					}
				});
		var name = $(target).attr("name");
		if (name) {
			span.find("input.combo-value").attr("name", name);
			$(target).removeAttr("name").attr("comboName", name);
		}
		input.attr("autocomplete", "off");
		return {
			combo : span,
			panel : panel
		};
	};
	/** 
	 * 销毁combo组件init方法构造出来的DOM,同时移除用户定义用于依附combo的DOM 
	 */
	function destroy(target) {
		var input = $.data(target, "combo").combo.find("input.combo-text");
		//销毁校验
		input.validatebox("destroy");
		//销毁下拉面板 
		$.data(target, "combo").panel.panel("destroy");
		//移除combox构造的DOM 
		$.data(target, "combo").combo.remove();
		//移除用户定义用于依附combo的DOM 
		$(target).remove();
	};
	function bindEvents(target) {
		var data = $.data(target, "combo");
		var opts = data.options;
		var combo = $.data(target, "combo").combo;
		var panel = $.data(target, "combo").panel;
		var input = combo.find(".combo-text");
		var arrow = combo.find(".combo-arrow");
		//委托mousedown事件到document上,只要不是点击panel区域,就关闭所有下拉面板 
		//特别注意的是这地方用的事件委托,故如某个触发元素阻止了事件冒泡,这个委托在document上的事件是没机会执行的。 
		$(document).unbind(".combo").bind("mousedown.combo", function(e) {//① 
					var panels = $("body>div.combo-p>div.combo-panel");
					var p = $(e.target).closest("div.combo-panel", panels);
					if (p.length) {
						//如果mousedown事件发生在下拉面板内,则不做任何操作 
						return;
					}
					//关闭面板 
					panels.panel("close");
				});
		combo.unbind(".combo");
		panel.unbind(".combo");
		input.unbind(".combo");
		arrow.unbind(".combo");
		//未禁用才会绑定相应事件 
		if (!opts.disabled) {
			input.bind("mousedown.combo", function(e) {
						//看到了吧,入框的mousedown事件直接阻止事件冒泡了 
						//所以鼠标在输入框内按下时不会触发到①处委托的事件的 
						e.stopPropagation();
					}).bind("keydown.combo", function(e) {
				//绑定键盘事件,这地方基本上是预留了事件接口
				//开发者可以通过定义相关事件,在combo的基础上灵活其它功
				switch (e.keyCode) {
					case 38 ://向上预留事件接口 
						opts.keyHandler.up.call(target);
						break;
					case 40 ://向下预留事件接口 
						opts.keyHandler.down.call(target);
						break;
					case 13 ://回车,阻止默认行为预留事件接口 
						e.preventDefault();
						opts.keyHandler.enter.call(target);
						return false;
					case 9 ://tab 隐藏下拉面板 
					case 27 ://esc 隐藏下拉面板 
						hidePanel(target);
						break;
					default ://维护data.previousValue值;预留query事件接口;校验input
						if (opts.editable) {
							if (data.timer) {
								clearTimeout(data.timer);
							}
							data.timer = setTimeout(function() {
										var q = input.val();
										if (data.previousValue != q) {
											data.previousValue = q;
											showPanel(target);
											//预留query事件接口 
											opts.keyHandler.query.call(target,
													input.val());
											//校验input 
											validate(target, true);//② 
										}
									}, opts.delay);
						}
				}
			});
			//绑定下拉按?时? 
			arrow.bind("click.combo", function() {
						//??显示的?显示??的。 
						if (panel.is(":visible")) {
							hidePanel(target);
						} else {
							$("div.combo-panel").panel("close");
							showPanel(target);
						}
						//焦点放到input上,这地方究竟是为了啥呢,知道validatebox实现原理的童鞋就知道原因了 
						//因为validatebox的实现是基于onfocus的,所以②处想触发校验,这地方就先focus到input上了。 
						//但是,我们再仔细想想的话,为什么不把㈠ 代码放到②处的validate方法内部呢?这样是不是更合理? 
						//不知到作者是出于什么目的,总之个人觉得放到②的validate方法内部更为合理. 
						input.focus();//㈠ 
					}).bind("mouseenter.combo", function() {
						//只是处理样式,没什么好说的 
						$(this).addClass("combo-arrow-hover");
					}).bind("mouseleave.combo", function() {
						//只是处理样式,没什么好说的 
						$(this).removeClass("combo-arrow-hover");
					}).bind("mousedown.combo", function() {
						//为何返回false,止事件冒泡和默认行为。阻止冒泡不难理解是为了避免①处冲突
						//可是为何又阻止默认行为,百思不得其解,经测测使用e.stopPropagation();也就可以了。 
						return false;//㈡ 
					});
		}
	};
	/** 
	 * 显示下拉面板 
	 * @param {Object} target 
	 */
	function showPanel(target) {
		var opts = $.data(target, "combo").options;
		var combo = $.data(target, "combo").combo;
		var panel = $.data(target, "combo").panel;
		if ($.fn.window) {
			//如果项目中使用了window组件,则跟window共同维护和使用$.fn.window.defaults.zIndex 
			panel.panel("panel").css("z-index", $.fn.window.defaults.zIndex++);
		}
		panel.panel("move", {
					//这地方为何不直接fixedLeft(),说实话没看出任何用途
					left : combo.offset().left,
					top : fixedTop()
				});
		panel.panel("open");
		opts.onShowPanel.call(target);
		/**
		 * 搞了个匿名函数,只要下拉面板是可见的,我了个去,这个匿名函数会一直运行! 为什么要这样呢,岂不是很好资源?想来想去只有一个勉强的理由,
		 * 那就是用户手工调整浏览器大小的时候,面板能自动调整位置,
		 * 如果仅仅出于这个原因,我们使用$(window).resize方式监控会不会更好点呢?
		 */
		(function() {//㈢
			if (panel.is(":visible")) {
				panel.panel("move", {
							left : fixedLeft(),
							top : fixedTop()
						});
				setTimeout(arguments.callee, 200);
			}
		})();
		/**
		 * 纠正下拉面板left
		 * 原理参照fixedTop的分析图
		 */
		function fixedLeft() {
			var left = combo.offset().left;
			if (left + panel._outerWidth() > $(window)._outerWidth()
					+ $(document).scrollLeft()) {
				left = $(window)._outerWidth() + $(document).scrollLeft()
						- panel._outerWidth();
			}
			if (left < 0) {
				left = 0;
			}
			return left;
		};
		/**
		 * 纠正下拉面板top
		 * 其原理在文章中我用图形说明名了③
		 */
		function fixedTop() {
			var top = combo.offset().top + combo._outerHeight();
			if (top + panel._outerHeight() > $(window)._outerHeight()
					+ $(document).scrollTop()) {
				top = combo.offset().top - panel._outerHeight();
			}
			if (top < $(document).scrollTop()) {
				top = combo.offset().top + combo._outerHeight();
			}
			return top;
		};
	};
	/**
	 * 隐藏下拉面板,这个没什么可说的,预留了一个onHidePanel事件接口
	 */
	function hidePanel(target) {
		var opts = $.data(target, "combo").options;
		var panel = $.data(target, "combo").panel;
		panel.panel("close");
		opts.onHidePanel.call(target);
	};
	/**
	 * 做校验,没啥好说的,easyui理解
	 */
	function validate(target, doit) {
		var opts = $.data(target, "combo").options;
		var input = $.data(target, "combo").combo.find("input.combo-text");
		input.validatebox(opts);
		if (doit) {
			input.validatebox("validate");
		}
	};
	/** 
	 * 设置置combo控件输入框是否可编
	 */
	function setDisabled(target, disabled) {
		var ops = $.data(target, "combo").options;
		var combo = $.data(target, "combo").combo;
		if (disabled) {
			ops.disabled = true;
			$(target).attr("disabled", true);
			combo.find(".combo-value").attr("disabled", true);
			combo.find(".combo-text").attr("disabled", true);
		} else {
			ops.disabled = false;
			$(target).removeAttr("disabled");
			combo.find(".combo-value").removeAttr("disabled");
			combo.find(".combo-text").removeAttr("disabled");
		}
	};
	/**
	 * 清空值
	 */
	function clear(target) {
		var ops = $.data(target, "combo").options;
		var combo = $.data(target, "combo").combo;
		if (ops.multiple) {
			combo.find("input.combo-value").remove();
		} else {
			combo.find("input.combo-value").val("");
		}
		combo.find("input.combo-text").val("");
	};
	/**
	 * 获取Text
	 */
	function getText(target) {
		var combo = $.data(target, "combo").combo;
		return combo.find("input.combo-text").val();
	};
	/**
	 * 设置Text,同时维护$.data(target, "combo").previousValue变量
	 */
	function setText(target, text) {
		var combo = $.data(target, "combo").combo;
		combo.find("input.combo-text").val(text);
		validate(target, true);
		$.data(target, "combo").previousValue = text;
	};
	/**
	 * 获取Texts,注意多值模式的时候input.combo-value是有多个的
	 */
	function getValues(target) {
		var values = [];
		var combo = $.data(target, "combo").combo;
		combo.find("input.combo-value").each(function() {
					values.push($(this).val());
				});
		return values;
	};
	/**
	 * 设置Texts,也没什么难度,注意是多个隐藏的文本域对应多值就行了
	 */
	function setValues(target, values) {
		var opts = $.data(target, "combo").options;
		var oldValues = getValues(target);
		var combo = $.data(target, "combo").combo;
		combo.find("input.combo-value").remove();
		var comboName = $(target).attr("comboName");
		for (var i = 0; i < values.length; i++) {
			var comboValue = $("<input type=\"hidden\" class=\"combo-value\">")
					.appendTo(combo);
			if (comboName) {
				comboValue.attr("name", comboName);
			}
			comboValue.val(values[i]);
		}
		var tmp = [];
		for (var i = 0; i < oldValues.length; i++) {
			tmp[i] = oldValues[i];
		}
		var aa = [];
		for (var i = 0; i < values.length; i++) {
			for (var j = 0; j < tmp.length; j++) {
				if (values[i] == tmp[j]) {
					aa.push(values[i]);
					tmp.splice(j, 1);
					break;
				}
			}
		}
		if (aa.length != values.length || values.length != oldValues.length) {
			if (opts.multiple) {
				opts.onChange.call(target, values, oldValues);
			} else {
				opts.onChange.call(target, values[0], oldValues[0]);
			}
		}
	};
	/**
	 * 获取单值
	 */
	function getValue(target) {
		var values = getValues(target);
		return values[0];
	};
	/**
	 * 设置但值
	 */
	function setValue(target, value) {
		setValues(target, [value]);
	};
	/**
	 * 根据multiple初始化值
	 */
	function initValue(target) {
		var opts = $.data(target, "combo").options;
		var fn = opts.onChange;
		opts.onChange = function() {
		};
		if (opts.multiple) {
			if (opts.value) {
				if (typeof opts.value == "object") {
					setValues(target, opts.value);
				} else {
					setValue(target, opts.value);
				}
			} else {
				setValues(target, []);
			}
		} else {
			setValue(target, opts.value);
		}
		opts.onChange = fn;
	};
	/**
	 * 构造函数
	 */
	$.fn.combo = function(options, param) {
		if (typeof options == "string") {//如果是字符串,调用函数
			return $.fn.combo.methods[options](this, param);
		}
		options = options || {};
		//初始化构造每个combo组件
		return this.each(function() {
					var state = $.data(this, "combo");
					if (state) {
						$.extend(state.options, options);
					} else {
						var r = init(this);
						state = $.data(this, "combo", {
									options : $.extend({}, $.fn.combo.defaults,
											$.fn.combo.parseOptions(this), options),
									combo : r.combo,
									panel : r.panel,
									previousValue : null
								});
						$(this).removeAttr("disabled");
					}
					$("input.combo-text", state.combo).attr("readonly",
							!state.options.editable);
					initArrow(this);
					setDisabled(this, state.options.disabled);
					setSize(this);
					bindEvents(this);
					validate(this);
					initValue(this);
				});
	};
	/**
	 * 对外接口
	 */
	$.fn.combo.methods = {
		options : function(jq) {
			return $.data(jq[0], "combo").options;
		},
		panel : function(jq) {
			return $.data(jq[0], "combo").panel;
		},
		textbox : function(jq) {
			return $.data(jq[0], "combo").combo.find("input.combo-text");
		},
		destroy : function(jq) {
			return jq.each(function() {
						destroy(this);
					});
		},
		resize : function(jq, width) {
			return jq.each(function() {
						setSize(this, width);
					});
		},
		showPanel : function(jq) {
			return jq.each(function() {
						showPanel(this);
					});
		},
		hidePanel : function(jq) {
			return jq.each(function() {
						hidePanel(this);
					});
		},
		disable : function(jq) {
			return jq.each(function() {
						setDisabled(this, true);
						bindEvents(this);
					});
		},
		enable : function(jq) {
			return jq.each(function() {
						setDisabled(this, false);
						bindEvents(this);
					});
		},
		validate : function(jq) {
			return jq.each(function() {
						validate(this, true);
					});
		},
		isValid : function(jq) {
			var input = $.data(jq[0], "combo").combo.find("input.combo-text");
			return input.validatebox("isValid");
		},
		clear : function(jq) {
			return jq.each(function() {
						clear(this);
					});
		},
		getText : function(jq) {
			return getText(jq[0]);
		},
		setText : function(jq, text) {
			return jq.each(function() {
						setText(this, text);
					});
		},
		getValues : function(jq) {
			return getValues(jq[0]);
		},
		setValues : function(jq, values) {
			return jq.each(function() {
						setValues(this, values);
					});
		},
		getValue : function(jq) {
			return getValue(jq[0]);
		},
		setValue : function(jq, value) {
			return jq.each(function() {
						setValue(this, value);
					});
		}
	};
	/**
	 * 属性转换器
	 */
	$.fn.combo.parseOptions = function(target) {
		var t = $(target);
		return $.extend({}, $.fn.validatebox.parseOptions(target), $.parser
						.parseOptions(target, ["width", "separator", {
											panelWidth : "number",
											editable : "boolean",
											hasDownArrow : "boolean",
											delay : "number"
										}]), {
					panelHeight : (t.attr("panelHeight") == "auto"
							? "auto"
							: parseInt(t.attr("panelHeight")) || undefined),
					multiple : (t.attr("multiple") ? true : undefined),
					disabled : (t.attr("disabled") ? true : undefined),
					value : (t.val() || undefined)
				});
	};
	/**
	 * 默认值
	 */
	$.fn.combo.defaults = $.extend({}, $.fn.validatebox.defaults, {
				width : "auto",
				panelWidth : null,
				panelHeight : 200,
				multiple : false,
				separator : ",",
				editable : true,
				disabled : false,
				hasDownArrow : true,
				value : "",
				delay : 200,
				keyHandler : {
					up : function() {
					},
					down : function() {
					},
					enter : function() {
					},
					query : function(q) {
					}
				},
				onShowPanel : function() {
				},
				onHidePanel : function() {
				},
				onChange : function(_5d, _5e) {
				}
			});
})(jQuery);

combo组件生成的典型DOM结构如下:

<!--用户定义DOM-->
<select id="cc" class="combo-f" style="display:none;"></select>
<!--combo组件操作区 -->
<span class="combo" style="width:125px;">
	<!--输入框-->
	<input type="text" class="combo-text validatebox-text" autocomplete="off" readonly="readonly" style="width: 107px;">
	<!--下拉按钮-->
	<span>
		<span class="combo-arrow"></span>
	</span>
	<!--隐藏域-->
	<input type="hidden" class="combo-value" value="">
</span>
<!--combo组件下拉面板 -->
<div class="panel combo-p" style="position: absolute;">
    <div class="combo-panel panel-body panel-body-noheader">
        <div id="sp">
            <div>Select a language</div>
            <input type="radio" name="lang" value="01"><span>Java</span>
        </div>
    </div>
</div>

代码中的fixedTop函数是纠正下拉面板的位置的,为什么要纠正,我画了一幅图,希望大家能看懂,说白了,就是下拉面板被截断时,要能够自动调整显示位置:

下拉面板位置调整与计算

目前有 12 条留言 其中:访客:6 条, 博主:6 条

  1. pigslin : 2012年12月28日13:23:55  -49楼 @回复 回复

    小雪,建议你在以后写源码分析时可以先来个整体的介绍,比如介绍下这个组件有哪些特性,画个功能结果图之类的,这样看上去就一目了然了!呵呵……


    • 管理员
      世纪之光 : 2012年12月28日13:46:08  地下1层 @回复 回复

      建议不错,可是功能图这些怎么画,相当耗时,我并没有那么长时间去做这个事情啊。

  2. peislin : 2013年01月11日14:50:13  -48楼 @回复 回复

    //如果项目中使用了window组件,则跟window共同维护和使用$.fn.window.defaults.zIndex

    panel.panel(“panel”).css(“z-index”,$.fn.window.defaults.zIndex++);

    这里貌似有误!
    应该是把window的z-index赋值给panel,而window的z-index加1.


    • 管理员
      世纪之光 : 2013年01月11日15:10:03  地下1层 @回复 回复

      panel.panel(“panel”).css(“z-index”,$.fn.window.defaults.zIndex++); 这句代码跟你描述的不是一个意思么?
      先将$.fn.window.defaults.zIndex付给panel;然后$.fn.window.defaults.zIndex递增一个

      • walle : 2016年07月13日22:38:54  地下2层 @回复 回复

        combotree 的内容如果太长的话怎么让他换行啊,combobox 就可以自动换行,我说的事panel里面的选项,而不是input里面的值


        • 管理员
          世纪之光 : 2016年07月15日09:32:42  地下3层 @回复 回复

          tree组件的文本是不好换行的,因为它的文本的display值为inline-block

  3. Willikin : 2017年06月19日18:03:42  -47楼 @回复 回复

    本人小白一个,遇到一个问题,就是easyui的combobox下拉框,在ctrl+v粘贴时跟鼠标右键粘贴时显示下拉效果是不同的,debug却发现遇到setTimeout(arguments.callee, 200);时候跳过去,不知道楼主有没有办法和建议?谢谢


    • 管理员
      世纪之光 : 2017年06月19日19:33:11  地下1层 @回复 回复

      你比较细心啊,不过我不太明白你说的ctrl+v和右键粘贴的效果区别在哪里?

      • Willikin : 2017年06月21日09:25:11  地下2层 @回复 回复

        就是鼠标事件跟键盘时间的区别。键盘事件就能触发下拉面板提示,鼠标的粘贴就不行,一直想改,但是老改不对。我用的是1.8.2。


        • 管理员
          世纪之光 : 2017年06月22日15:17:02  地下3层 @回复 回复

          我那天用在官网上测试,鼠标粘贴也是可以触发下拉面板的显示的,你到官网的demo上面看看。

  4. Willikin : 2017年06月23日14:11:02  -46楼 @回复 回复

    我解决了,在bindEvents(target)函数里面加了setTimeout,就可以了。可能我用的是旧版的原因,谢谢你啊

给我留言

留言无头像?


×