if(typeof TRSWAS === "undefined" || !TRSWAS) { TRSWAS = {}; } (function(NS) { var Y = YAHOO.util, Dom = Y.Dom, Event = Y.Event, Lang = YAHOO.lang, head = document.getElementsByTagName("head")[0], ie = YAHOO.env.ua.ie, ie6 = (ie === 6), CALLBACK_STR = "TRSWAS.Suggest.callback", // 注意 TRSWAS 在这里是写死的 STYLE_ID = "suggest-style", // 样式 style 元素的 id BEFORE_DATA_REQUEST = "beforeDataRequest", ON_DATA_RETURN = "onDataReturn", BEFORE_SHOW = "beforeShow", ON_ITEM_SELECT = "onItemSelect", /** * Suggest的默认配置 */ defaultConfig = { /** * 悬浮提示层的class * 提示层的默认结构如下: *
*
    *
  1. * ... * ... *
  2. *
*
* ... *
*
* @type String */ containerClassName: "suggest-container", /** * 提示层的宽度 * 注意:默认情况下,提示层的宽度和input输入框的宽度保持一致 * 示范取值:"200px", "10%"等,必须带单位 * @type String */ containerWidth: "auto", /** * 提示层中,key元素的class * @type String */ keyElClassName: "suggest-key", /** * 提示层中,result元素的class * @type String */ resultElClassName: "suggest-result", /** * result的格式 * @type String */ resultFormat: "约%result%条结果", /** * 提示层中,选中项的class * @type String */ selectedItemClassName: "selected", /** * 提示层底部的class * @type String */ bottomClassName: "suggest-bottom", /** * 是否显示关闭按钮 * @type Boolean */ showCloseBtn: false, /** * 关闭按钮上的文字 * @type String */ closeBtnText: "关闭", /** * 关闭按钮的class * @type String */ closeBtnClassName: "suggest-close-btn", /** * 是否需要iframe shim * @type Boolean */ useShim: ie6, /** * iframe shim的class * @type String */ shimClassName: "suggest-shim", /** * 定时器的延时 * @type Number */ timerDelay: 200, /** * 初始化后,自动激活 * @type Boolean */ autoFocus: false, /** * 鼠标点击完成选择时,是否自动提交表单 * @type Boolean */ submitFormOnClickSelect: true }; /** * 提示补全组件 * @class Suggest * @requires YAHOO.util.Dom * @requires YAHOO.util.Event * @constructor * @param {String|HTMLElement} textInput * @param {String} dataSource * @param {Object} config */ NS.Suggest = function(textInput, dataSource, config) { /** * 文本输入框 * @type HTMLElement */ this.textInput = Dom.get(textInput); /** * 获取数据的URL 或 JSON格式的静态数据 * @type {String|Object} */ this.dataSource = dataSource; /** * JSON静态数据源 * @type Object 格式为 {"query1" : [["key1", "result1"], []], "query2" : [[], []]} */ this.JSONDataSource = Lang.isObject(dataSource) ? dataSource : null; /** * 通过jsonp返回的数据 * @type Object */ this.returnedData = null; /** * 配置参数 * @type Object */ this.config = Lang.merge(defaultConfig, config || {}); /** * 存放提示信息的容器 * @type HTMLElement */ this.container = null; /** * 输入框的值 * @type String */ this.query = ""; /** * 获取数据时的参数 * @type String */ this.queryParams = ""; /** * 内部定时器 * @private * @type Object */ this._timer = null; /** * 计时器是否处于运行状态 * @private * @type Boolean */ this._isRunning = false; /** * 获取数据的script元素 * @type HTMLElement */ this.dataScript = null; /** * 数据缓存 * @private * @type Object */ this._dataCache = {}; /** * 最新script的时间戳 * @type String */ this._latestScriptTime = ""; /** * script返回的数据是否已经过期 * @type Boolean */ this._scriptDataIsOut = false; /** * 是否处于键盘选择状态 * @private * @type Boolean */ this._onKeyboardSelecting = false; /** * 提示层的当前选中项 * @type Boolean */ this.selectedItem = null; // init this._init(); }; NS.Suggest.prototype = { /** * 初始化方法 * @protected */ _init: function() { // init DOM this._initTextInput(); this._initContainer(); if (this.config.useShim) this._initShim(); this._initStyle(); // create events this.createEvent(BEFORE_DATA_REQUEST); this.createEvent(ON_DATA_RETURN); this.createEvent(BEFORE_SHOW); this.createEvent(ON_ITEM_SELECT); // window resize event this._initResizeEvent(); }, /** * 初始化输入框 * @protected */ _initTextInput: function() { var instance = this; // turn off autocomplete this.textInput.setAttribute("autocomplete", "off"); // focus Event.on(this.textInput, "focus", function() { instance.start(); }); // blur Event.on(this.textInput, "blur", function() { instance.stop(); instance.hide(); }); // auto focus if (this.config.autoFocus) this.textInput.focus(); // keydown // 注:截至目前,在Opera9.64中,输入法开启时,依旧不会触发任何键盘事件 var pressingCount = 0; // 持续按住某键时,连续触发的keydown次数。注意Opera只会触发一次。 Event.on(this.textInput, "keydown", function(ev) { var keyCode = ev.charCode || ev.keyCode; //console.log("keydown " + keyCode); switch (keyCode) { case 27: // ESC键,隐藏提示层并还原初始输入 instance.hide(); instance.textInput.value = instance.query; break; case 13: // ENTER键 // 提交表单前,先隐藏提示层并停止计时器 instance.textInput.blur(); // 这一句还可以阻止掉浏览器的默认提交事件 // 如果是键盘选中某项后回车,触发onItemSelect事件 if (instance._onKeyboardSelecting) { if (instance.textInput.value == instance._getSelectedItemKey()) { // 确保值匹配 instance.fireEvent(ON_ITEM_SELECT, instance.textInput.value); } } // 提交表单 instance._submitForm(); break; case 40: // DOWN键 case 38: // UP键 // 按住键不动时,延时处理 if (pressingCount++ == 0) { if (instance._isRunning) instance.stop(); instance._onKeyboardSelecting = true; instance.selectItem(keyCode == 40); } else if (pressingCount == 3) { pressingCount = 0; } break; } // 非 DOWN/UP 键时,开启计时器 if (keyCode != 40 && keyCode != 38) { if (!instance._isRunning) { // 1. 当网速较慢,js还未下载完时,用户可能就已经开始输入 // 这时,focus事件已经不会触发,需要在keyup里触发定时器 // 2. 非DOWN/UP键时,需要激活定时器 instance.start(); } instance._onKeyboardSelecting = false; } }); // reset pressingCount Event.on(this.textInput, "keyup", function() { //console.log("keyup"); pressingCount = 0; }); }, /** * 初始化提示层容器 * @protected */ _initContainer: function() { // create var container = document.createElement("div"); container.className = this.config.containerClassName; container.style.position = "absolute"; container.style.visibility = "hidden"; this.container = container; this._setContainerRegion(); this._initContainerEvent(); // append document.body.insertBefore(container, document.body.firstChild); }, /** * 设置容器的left, top, width * @protected */ _setContainerRegion: function() { var r = Dom.getRegion(this.textInput); var left = r.left, w = r.right - left - 2; // 减去border的2px // ie8兼容模式 // document.documentMode: // 5 - Quirks Mode // 7 - IE7 Standards // 8 - IE8 Standards var docMode = document.documentMode; if (docMode === 7 && (ie === 7 || ie === 8)) { left -= 2; } else if (YAHOO.env.ua.gecko) { // firefox下左偏一像素 注:当 input 所在的父级容器有 margin: auto 时会出现 left++; } this.container.style.left = left + "px"; this.container.style.top = r.bottom + "px"; if (this.config.containerWidth == "auto") { this.container.style.width = w + "px"; } else { this.container.style.width = this.config.containerWidth; } }, /** * 初始化容器事件 * 子元素都不用设置事件,冒泡到这里统一处理 * @protected */ _initContainerEvent: function() { var instance = this; // 鼠标事件 Event.on(this.container, "mousemove", function(ev) { //console.log("mouse move"); var target = Event.getTarget(ev); if (target.nodeName != "LI") { target = Dom.getAncestorByTagName(target, "li"); } if (Dom.isAncestor(instance.container, target)) { if (target != instance.selectedItem) { // 移除老的 instance._removeSelectedItem(); // 设置新的 instance._setSelectedItem(target); } } }); var mouseDownItem = null; this.container.onmousedown = function(e) { e = e || window.event; // 鼠标按下处的item mouseDownItem = e.target || e.srcElement; // 鼠标按下时,让输入框不会失去焦点 // 1. for IE instance.textInput.onbeforedeactivate = function() { window.event.returnValue = false; instance.textInput.onbeforedeactivate = null; }; // 2. for W3C return false; }; // mouseup事件 Event.on(this.container, "mouseup", function(ev) { // 当mousedown在提示层,但mouseup在提示层外时,点击无效 if (!instance._isInContainer(Event.getXY(ev))) return; var target = Event.getTarget(ev); // 在提示层A项处按下鼠标,移动到B处释放,不触发onItemSelect if (target != mouseDownItem) return; // 点击在关闭按钮上 if (target.className == instance.config.closeBtnClassName) { instance.hide(); return; } // 可能点击在li的子元素上 if (target.nodeName != "LI") { target = Dom.getAncestorByTagName(target, "li"); } // 必须点击在container内部的li上 if (Dom.isAncestor(instance.container, target)) { instance._updateInputFromSelectItem(target); // 触发选中事件 //console.log("on item select"); instance.fireEvent(ON_ITEM_SELECT, instance.textInput.value); // 提交表单前,先隐藏提示层并停止计时器 instance.textInput.blur(); // 提交表单 instance._submitForm(); } }); }, /** * click选择 or enter后,提交表单 */ _submitForm: function() { // 注:对于键盘控制enter选择的情况,由html自身决定是否提交。否则会导致输入法开启时,用enter选择英文时也触发提交 if (this.config.submitFormOnClickSelect) { var form = this.textInput.form; if (!form) return; if (Lang.trim(this.textInput.value) == "") { this.textInput.focus(); return; } // 通过js提交表单时,不会触发onsubmit事件 // 需要js自己触发 if (document.createEvent) { // ie var evObj = document.createEvent("MouseEvents"); evObj.initEvent("submit", true, false); form.dispatchEvent(evObj); } else if (document.createEventObject) { // w3c form.fireEvent("onsubmit"); } form.submit(); } }, /** * 判断p是否在提示层内 * @param {Array} p [x, y] */ _isInContainer: function(p) { var r = Dom.getRegion(this.container); return p[0] >= r.left && p[0] <= r.right && p[1] >= r.top && p[1] <= r.bottom; }, /** * 给容器添加iframe shim层 * @protected */ _initShim: function() { var iframe = document.createElement("iframe"); iframe.src = "about:blank"; iframe.className = this.config.shimClassName; iframe.style.position = "absolute"; iframe.style.visibility = "hidden"; iframe.style.border = "none"; this.container.shim = iframe; this._setShimRegion(); document.body.insertBefore(iframe, document.body.firstChild); }, /** * 设置shim的left, top, width * @protected */ _setShimRegion: function() { var container = this.container, shim = container.shim; if (shim) { shim.style.left = (parseInt(container.style.left) - 2) + "px"; // 解决吞边线bug shim.style.top = container.style.top; shim.style.width = (parseInt(container.style.width) + 2) + "px"; } }, /** * 初始化样式 * @protected */ _initStyle: function() { var styleEl = Dom.get(STYLE_ID); if (styleEl) return; // 防止多个实例时重复添加 var style = ".suggest-container{background:white;border:1px solid #999;z-index:99999}"; style += ".suggest-shim{z-index:99998}"; style += ".suggest-container li{color:#404040;padding:1px 0 2px;font-size:14px;line-height:18px;float:left;width:100%}"; style += ".suggest-container ol{margin:0;padding:0;list-style-type:none}"; style += ".suggest-container li.selected{background-color:#39F;cursor:default}"; style += ".suggest-key{float:left;text-align:left;padding-left:5px}"; style += ".suggest-result{float:right;text-align:right;padding-right:5px;color:green}"; style += ".suggest-container li.selected span{color:#FFF;cursor:default}"; //style += ".suggest-container li.selected .suggest-result{color:green}"; style += ".suggest-bottom{padding:0 5px 5px}"; style += ".suggest-close-btn{float:right}"; style += ".suggest-container li,.suggest-bottom{overflow:hidden;zoom:1;clear:both}"; /* hacks */ style += ".suggest-container{*margin-left:2px;_margin-left:-2px;_margin-top:-3px}"; styleEl = document.createElement("style"); styleEl.id = STYLE_ID; styleEl.type = "text/css"; head.appendChild(styleEl); // 先添加到DOM树中,都在cssText里的hack会失效 if (styleEl.styleSheet) { // IE styleEl.styleSheet.cssText = style; } else { // W3C styleEl.appendChild(document.createTextNode(style)); } }, /** * window.onresize时,调整提示层的位置 * @protected */ _initResizeEvent: function() { var instance = this, resizeTimer; Event.on(window, "resize", function() { if (resizeTimer) { clearTimeout(resizeTimer); } resizeTimer = setTimeout(function() { instance._setContainerRegion(); instance._setShimRegion(); }, 50); }); }, /** * 启动计时器,开始监听用户输入 */ start: function() { NS.Suggest.focusInstance = this; var instance = this; instance._timer = setTimeout(function() { instance.updateData(); instance._timer = setTimeout(arguments.callee, instance.config.timerDelay); }, instance.config.timerDelay); this._isRunning = true; }, /** * 停止计时器 */ stop: function() { NS.Suggest.focusInstance = null; clearTimeout(this._timer); this._isRunning = false; }, /** * 显示提示层 */ show: function() { if (this.isVisible()) return; var container = this.container, shim = container.shim; container.style.visibility = ""; if (shim) { if (!shim.style.height) { // 第一次显示时,需要设定高度 var r = Dom.getRegion(container); shim.style.height = (r.bottom - r.top - 2) + "px"; } shim.style.visibility = ""; } }, /** * 隐藏提示层 */ hide: function() { if (!this.isVisible()) return; var container = this.container, shim = container.shim; //console.log("hide"); if (shim) shim.style.visibility = "hidden"; container.style.visibility = "hidden"; }, /** * 提示层是否显示 */ isVisible: function() { return this.container.style.visibility != "hidden"; }, /** * 更新提示层的数据 */ updateData: function() { if (!this._needUpdate()) return; //console.log("update data"); this._updateQueryValueFromInput(); var q = this.query; // 1. 输入为空时,隐藏提示层 if (!Lang.trim(q).length) { this._fillContainer(""); this.hide(); return; } if (typeof this._dataCache[q] != "undefined") { // 2. 使用缓存数据 //console.log("use cache"); this.returnedData = "using cache"; this._fillContainer(this._dataCache[q]); this._displayContainer(); } else if (this.JSONDataSource) { // 3. 使用JSON静态数据源 this.handleResponse(this.JSONDataSource[q]); } else { // 4. 请求服务器数据 this.requestData(); } }, /** * 是否需要更新数据 * @protected * @return Boolean */ _needUpdate: function() { // 注意:加入空格也算有变化 return this.textInput.value != this.query; }, /** * 通过script元素加载数据 */ requestData: function() { //console.log("request data via script"); this.dataScript = null; // IE不需要重新创建script元素 if (!this.dataScript) { var script = document.createElement("script"); script.type = "text/javascript"; script.charset = "utf-8"; // jQuery ajax.js line 275: // Use insertBefore instead of appendChild to circumvent an IE6 bug. // This arises when a base node is used. head.insertBefore(script, head.firstChild); this.dataScript = script; if (!ie) { var t = new Date().getTime(); this._latestScriptTime = t; script.setAttribute("time", t); Event.on(script, "load", function() { //console.log("on load"); // 判断返回的数据是否已经过期 this._scriptDataIsOut = script.getAttribute("time") != this._latestScriptTime; }, this, true); } } // 注意:没必要加时间戳,是否缓存由服务器返回的Header头控制 this.queryParams = "q=" + encodeURIComponent(this.query) + "&code=utf-8&callback=" + CALLBACK_STR; this.fireEvent(BEFORE_DATA_REQUEST, this.query); if (this.dataSource.indexOf("?") != -1) this.dataScript.src = this.dataSource + "&" + this.queryParams; else this.dataScript.src = this.dataSource + "?" + this.queryParams; }, /** * 处理获取的数据 * @param {Object} data */ handleResponse: function(data) { //console.log("handle response"); if (this._scriptDataIsOut) return; // 抛弃过期数据,否则会导致bug:1. 缓存key值不对; 2. 过期数据导致的闪屏 this.returnedData = data; this.fireEvent(ON_DATA_RETURN, data); // 格式化数据 this.returnedData = this.formatData(this.returnedData); // 填充数据 var content = ""; var len = this.returnedData.length; if (len > 0) { var list = document.createElement("ol"); var prefix = this.textInput.value; for (var i = 0; i < len; ++i) { var itemData = this.returnedData[i]; var tmp = itemData["key"]; if (tmp.indexOf(prefix, 0) != -1) tmp = prefix + "" + tmp.substring(prefix.length) + ""; var li = this.formatItem(tmp, itemData["result"]); // 缓存key值到attribute上 li.setAttribute("key", itemData["key"]); list.appendChild(li); } content = list; } this._fillContainer(content); // 有内容时才添加底部 if (len > 0) this.appendBottom(); // fire event if (Lang.trim(this.container.innerHTML)) { // 实际上是beforeCache,但从用户的角度看,是beforeShow this.fireEvent(BEFORE_SHOW, this.container); } // cache this._dataCache[this.query] = this.container.innerHTML; // 显示容器 this._displayContainer(); }, /** * 格式化输入的数据对象为标准格式 * @param {Object} data 格式可以有3种: * 1. {"result" : [["key1", "result1"], ["key2", "result2"], ...]} * 2. {"result" : ["key1", "key2", ...]} * 3. 1和2的组合 * 4. 标准格式 * 5. 上面1-4中,直接取o["result"]的值 * @return Object 标准格式的数据: * [{"key" : "key1", "result" : "result1"}, {"key" : "key2", "result" : "result2"}, ...] */ formatData: function(data) { var arr = []; if (!data) return arr; if (Lang.isArray(data["result"])) data = data["result"]; var len = data.length; if (!len) return arr; var item; for (var i = 0; i < len; ++i) { item = data[i]; if (Lang.isString(item)) { // 只有key值时 arr[i] = {"key" : item}; } else if (Lang.isArray(item) && item.length >= 2) { // ["key", "result"] 取数组前2个 arr[i] = {"key" : item[0], "result" : item[1]}; } else { arr[i] = item; } } return arr; }, /** * 格式化输出项 * @param {String} key 查询字符串 * @param {Number} result 结果 可不设 * @return {HTMLElement} */ formatItem: function(key, result) { var li = document.createElement("li"); var keyEl = document.createElement("span"); keyEl.className = this.config.keyElClassName; keyEl.innerHTML = key; li.appendChild(keyEl); if (typeof result != "undefined") { // 可以没有 var resultText = this.config.resultFormat.replace("%result%", result); if (Lang.trim(resultText)) { // 有值时才创建 var resultEl = document.createElement("span"); resultEl.className = this.config.resultElClassName; resultEl.appendChild(document.createTextNode(resultText)); li.appendChild(resultEl); } } return li; }, /** * 添加提示层底部 */ appendBottom: function() { var bottom = document.createElement("div"); bottom.className = this.config.bottomClassName; if (this.config.showCloseBtn) { var closeBtn = document.createElement("a"); closeBtn.href = "javascript: void(0)"; closeBtn.setAttribute("target", "_self"); // bug fix: 覆盖,否则会弹出空白页面 closeBtn.className = this.config.closeBtnClassName; closeBtn.appendChild(document.createTextNode(this.config.closeBtnText)); // 没必要,点击时,输入框失去焦点,自动就关闭了 /* Event.on(closeBtn, "click", function(ev) { Event.stopEvent(ev); this.hidden(); }, this, true); */ bottom.appendChild(closeBtn); } // 仅当有内容时才添加 if (Lang.trim(bottom.innerHTML)) { this.container.appendChild(bottom); } }, /** * 填充提示层 * @protected * @param {String|HTMLElement} content innerHTML or Child Node */ _fillContainer: function(content) { if (content.nodeType == 1) { this.container.innerHTML = ""; this.container.appendChild(content); } else { this.container.innerHTML = content; } // 一旦重新填充了,selectedItem就没了,需要重置 this.selectedItem = null; }, /** * 根据contanier的内容,显示或隐藏容器 */ _displayContainer: function() { if (Lang.trim(this.container.innerHTML)) { this.show(); } else { this.hide(); } }, /** * 选中提示层中的上/下一个条 * @param {Boolean} down true表示down,false表示up */ selectItem: function(down) { //console.log("select item " + down); var items = this.container.getElementsByTagName("li"); if (items.length == 0) return; // 有可能用ESC隐藏了,直接显示即可 if (!this.isVisible()){ this.show(); return; // 保留原来的选中状态 } var newSelectedItem; // 没有选中项时,选中第一/最后项 if (!this.selectedItem) { newSelectedItem = items[down ? 0 : items.length - 1]; } else { // 选中下/上一项 newSelectedItem = Dom[down ? "getNextSibling" : "getPreviousSibling"](this.selectedItem); // 已经到了最后/前一项时,归位到输入框,并还原输入值 if (!newSelectedItem) { this.textInput.value = this.query; } } // 移除当前选中项 this._removeSelectedItem(); // 选中新项 if (newSelectedItem) { this._setSelectedItem(newSelectedItem); this._updateInputFromSelectItem(); } }, /** * 移除选中项 * @protected */ _removeSelectedItem: function() { //console.log("remove selected item"); Dom.removeClass(this.selectedItem, this.config.selectedItemClassName); this.selectedItem = null; }, /** * 设置当前选中项 * @protected * @param {HTMLElement} item */ _setSelectedItem: function(item) { //console.log("set selected item"); Dom.addClass((item), this.config.selectedItemClassName); this.selectedItem = (item); }, /** * 获取提示层中选中项的key字符串 * @protected */ _getSelectedItemKey: function() { if (!this.selectedItem) return ""; // getElementsByClassName比较损耗性能,改用缓存数据到attribute上方法 //var keyEl = Dom.getElementsByClassName(this.config.keyElClassName, "*", this.selectedItem)[0]; //return keyEl.innerHTML; return this.selectedItem.getAttribute("key"); }, /** * 将textInput的值更新到this.query * @protected */ _updateQueryValueFromInput: function() { this.query = this.textInput.value; }, /** * 将选中项的值更新到textInput * @protected */ _updateInputFromSelectItem: function() { this.textInput.value = this._getSelectedItemKey(this.selectedItem); } }; Lang.augmentProto(NS.Suggest, Y.EventProvider); /** * 当前激活的实例 * @static */ NS.Suggest.focusInstance = null; /** * 从jsonp中获取数据 * @method callback */ NS.Suggest.callback = function(data) { if (!NS.Suggest.focusInstance) return; // 使得先运行script.onload事件,然后再执行callback函数 setTimeout(function() { NS.Suggest.focusInstance.handleResponse(data); }, 0); }; })(TRSWAS);