bootstrap-suggest.min.js 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013
  1. /**
  2. * Bootstrap Search Suggest
  3. * @desc 这是一个基于 bootstrap 按钮式下拉菜单组件的搜索建议插件,必须使用于按钮式下拉菜单组件上。
  4. * @author renxia <lzwy0820#qq.com>
  5. * @github https://github.com/lzwme/bootstrap-suggest-plugin.git
  6. * @since 2014-10-09
  7. *===============================================================================
  8. * (c) Copyright 2014-2016 http://lzw.me All Rights Reserved.
  9. ********************************************************************************/
  10. (function (factory) {
  11. if (typeof define === "function" && define.amd) {
  12. define(['jquery'], factory);
  13. } else if (typeof exports === 'object' && typeof module === 'object') {
  14. factory(require('jquery'));
  15. } else if (window.jQuery) {
  16. factory(window.jQuery);
  17. } else {
  18. throw new Error('Not found jQuery.');
  19. }
  20. })(function($) {
  21. var VERSION = 'VERSION_PLACEHOLDER';
  22. var $window = $(window);
  23. var isIe = 'ActiveXObject' in window; // 用于对 IE 的兼容判断
  24. var inputLock; // 用于中文输入法输入时锁定搜索
  25. // ie 下和 chrome 51 以上浏览器版本,出现滚动条时不计算 padding
  26. var chromeVer = navigator.userAgent.match(/Chrome\/(\d+)/);
  27. if (chromeVer) {
  28. chromeVer = +chromeVer[1];
  29. }
  30. var notNeedCalcPadding = isIe || chromeVer > 51;
  31. // 一些常量
  32. var BSSUGGEST = 'bsSuggest';
  33. var onDataRequestSuccess = 'onDataRequestSuccess';
  34. var DISABLED = 'disabled';
  35. var TRUE = true;
  36. var FALSE = false;
  37. /**
  38. * 错误处理
  39. */
  40. function handleError(e1, e2) {
  41. if (!window.console || !window.console.trace) {
  42. return;
  43. }
  44. console.trace(e1);
  45. if (e2) {
  46. console.trace(e2);
  47. }
  48. }
  49. /**
  50. * 获取当前 tr 列的关键字数据
  51. */
  52. function getPointKeyword($list) {
  53. return $list.data();
  54. }
  55. /**
  56. * 设置或获取输入框的 alt 值
  57. */
  58. function setOrGetAlt($input, val) {
  59. return val !== undefined ? $input.attr('alt', val) : $input.attr('alt');
  60. }
  61. /**
  62. * 设置或获取输入框的 data-json 值
  63. */
  64. function setJson($input, val) {
  65. return val !== undefined ? $input.attr('data-json', val) : $input.attr('data-json');
  66. }
  67. /**
  68. * 设置或获取输入框的 data-id 值
  69. */
  70. function setOrGetDataId($input, val) {
  71. return val !== (void 0) ? $input.attr('data-id', val) : $input.attr('data-id');
  72. }
  73. /**
  74. * 设置选中的值
  75. */
  76. function setValue($input, keywords, options) {
  77. if (!keywords || !keywords.key) {
  78. return;
  79. }
  80. var separator = options.separator || ',',
  81. inputValList,
  82. inputIdList,
  83. dataId = setOrGetDataId($input);
  84. if (options && options.multiWord) {
  85. inputValList = $input.val().split(separator);
  86. inputValList[inputValList.length - 1] = keywords.key;
  87. //多关键字检索支持设置id --- 存在 bug,不建议使用
  88. if (!dataId) {
  89. inputIdList = [keywords.id];
  90. } else {
  91. inputIdList = dataId.split(separator);
  92. inputIdList.push(keywords.id);
  93. }
  94. setOrGetDataId($input, inputIdList.join(separator))
  95. .val(inputValList.join(separator))
  96. .focus();
  97. } else {
  98. setOrGetDataId($input, keywords.id || '').val(keywords.key).focus();
  99. }
  100. $input.data('pre-val', $input.val())
  101. .trigger('onSetSelectValue', [keywords, (options.data.value || options._lastData.value)[keywords.index]]);
  102. }
  103. /**
  104. * 调整选择菜单位置
  105. * @param {Object} $input
  106. * @param {Object} $dropdownMenu
  107. * @param {Object} options
  108. */
  109. function adjustDropMenuPos($input, $dropdownMenu, options) {
  110. if (!$dropdownMenu.is(':visible')) {
  111. return;
  112. }
  113. var $parent = $input.parent();
  114. var parentHeight = $parent.height();
  115. var parentWidth = $parent.width();
  116. if (options.autoDropup) {
  117. setTimeout(function() {
  118. var offsetTop = $input.offset().top;
  119. var winScrollTop = $window.scrollTop();
  120. var menuHeight = $dropdownMenu.height();
  121. if ( // 自动判断菜单向上展开
  122. ($window.height() + winScrollTop - offsetTop) < menuHeight && // 假如向下会撑长页面
  123. offsetTop > (menuHeight + winScrollTop) // 而且向上不会撑到顶部
  124. ) {
  125. $parent.addClass('dropup');
  126. } else {
  127. $parent.removeClass('dropup');
  128. }
  129. }, 10);
  130. }
  131. // 列表对齐方式
  132. var dmcss = {};
  133. if (options.listAlign === 'left') {
  134. dmcss = {
  135. 'left': $input.siblings('div').width() - parentWidth,
  136. 'right': 'auto'
  137. };
  138. } else if (options.listAlign === 'right') {
  139. dmcss = {
  140. 'left': 'auto',
  141. 'right': 0
  142. };
  143. }
  144. // ie 下,不显示按钮时的 top/bottom
  145. if (isIe && !options.showBtn) {
  146. if (!$parent.hasClass('dropup')) {
  147. dmcss.top = parentHeight;
  148. dmcss.bottom = 'auto';
  149. } else {
  150. dmcss.top = 'auto';
  151. dmcss.bottom = parentHeight;
  152. }
  153. }
  154. // 是否自动最小宽度
  155. if (!options.autoMinWidth) {
  156. dmcss.minWidth = parentWidth;
  157. }
  158. /* else {
  159. dmcss['width'] = 'auto';
  160. }*/
  161. $dropdownMenu.css(dmcss);
  162. return $input;
  163. }
  164. /**
  165. * 设置输入框背景色
  166. * 当设置了 indexId,而输入框的 data-id 为空时,输入框加载警告色
  167. */
  168. function setBackground($input, options) {
  169. var inputbg, bg, warnbg;
  170. if ((options.indexId === -1 && !options.idField) || options.multiWord) {
  171. return $input;
  172. }
  173. bg = options.inputBgColor;
  174. warnbg = options.inputWarnColor;
  175. var curVal = $input.val();
  176. var preVal = $input.data('pre-val');
  177. if (setOrGetDataId($input) || !curVal) {
  178. $input.css('background', bg || '');
  179. if (!curVal && preVal) {
  180. $input.trigger('onUnsetSelectValue').data('pre-val', '');
  181. }
  182. return $input;
  183. }
  184. inputbg = $input.css('backgroundColor').replace(/ /g, '').split(',', 3).join(',');
  185. // 自由输入的内容,设置背景色
  186. if (!~warnbg.indexOf(inputbg)) {
  187. $input.trigger('onUnsetSelectValue') // 触发取消data-id事件
  188. .data('pre-val', '')
  189. .css('background', warnbg);
  190. }
  191. return $input;
  192. }
  193. /**
  194. * 调整滑动条
  195. */
  196. function adjustScroll($input, $dropdownMenu, options) {
  197. // 控制滑动条
  198. var $hover = $input.parent().find('tbody tr.' + options.listHoverCSS),
  199. pos, maxHeight;
  200. if ($hover.length) {
  201. pos = ($hover.index() + 3) * $hover.height();
  202. maxHeight = +$dropdownMenu.css('maxHeight').replace('px', '');
  203. if (pos > maxHeight || $dropdownMenu.scrollTop() > maxHeight) {
  204. pos = pos - maxHeight;
  205. } else {
  206. pos = 0;
  207. }
  208. $dropdownMenu.scrollTop(pos);
  209. }
  210. }
  211. /**
  212. * 解除所有列表 hover 样式
  213. */
  214. function unHoverAll($dropdownMenu, options) {
  215. $dropdownMenu.find('tr.' + options.listHoverCSS).removeClass(options.listHoverCSS);
  216. }
  217. /**
  218. * 验证 $input 对象是否符合条件
  219. * 1. 必须为 bootstrap 下拉式菜单
  220. * 2. 必须未初始化过
  221. */
  222. function checkInput($input, $dropdownMenu, options) {
  223. if (
  224. !$dropdownMenu.length || // 过滤非 bootstrap 下拉式菜单对象
  225. $input.data(BSSUGGEST) // 是否已经初始化的检测
  226. ) {
  227. return FALSE;
  228. }
  229. $input.data(BSSUGGEST, {
  230. options: options
  231. });
  232. return TRUE;
  233. }
  234. /**
  235. * 数据格式检测
  236. * 检测 ajax 返回成功数据或 data 参数数据是否有效
  237. * data 格式:{"value": [{}, {}...]}
  238. */
  239. function checkData(data) {
  240. var isEmpty = TRUE, o;
  241. for (o in data) {
  242. if (o === 'value') {
  243. isEmpty = FALSE;
  244. break;
  245. }
  246. }
  247. if (isEmpty) {
  248. handleError('返回数据格式错误!');
  249. return FALSE;
  250. }
  251. if (!data.value.length) {
  252. // handleError('返回数据为空!');
  253. return FALSE;
  254. }
  255. return data;
  256. }
  257. /**
  258. * 判断字段名是否在 options.effectiveFields 配置项中
  259. * @param {String} field 要判断的字段名
  260. * @param {Object} options
  261. * @return {Boolean} effectiveFields 为空时始终返回 true
  262. */
  263. function inEffectiveFields(field, options) {
  264. var effectiveFields = options.effectiveFields;
  265. return !(field === '__index' ||
  266. effectiveFields.length &&
  267. !~$.inArray(field, effectiveFields));
  268. }
  269. /**
  270. * 判断字段名是否在 options.searchFields 搜索字段配置中
  271. */
  272. function inSearchFields(field, options) {
  273. return ~$.inArray(field, options.searchFields);
  274. }
  275. /**
  276. * 通过下拉菜单显示提示文案
  277. */
  278. function showTip(tip, $input, $dropdownMenu, options) {
  279. $dropdownMenu.html('<div style="padding:10px 5px 5px">' + tip + '</div>').show();
  280. adjustDropMenuPos($input, $dropdownMenu, options);
  281. }
  282. /**
  283. * 下拉列表刷新
  284. * 作为 fnGetData 的 callback 函数调用
  285. */
  286. function refreshDropMenu($input, data, options) {
  287. var $dropdownMenu = $input.parent().find('ul:eq(0)'),
  288. len, i, field, index = 0,
  289. tds,
  290. html = ['<table class="table table-condensed table-sm" style="margin:0">'],
  291. idValue, keyValue; // 作为输入框 data-id 和内容的字段值
  292. var dataList = data.value;
  293. if (!data || !(len = dataList.length)) {
  294. if (options.emptyTip) {
  295. showTip(options.emptyTip, $input, $dropdownMenu, options);
  296. } else {
  297. $dropdownMenu.empty().hide();
  298. }
  299. return $input;
  300. }
  301. // 相同数据,不用继续渲染了
  302. if (
  303. options._lastData &&
  304. JSON.stringify(options._lastData) === JSON.stringify(data) &&
  305. $dropdownMenu.find('tr').length === len
  306. ) {
  307. $dropdownMenu.show();
  308. return adjustDropMenuPos($input, $dropdownMenu, options);
  309. // return $input;
  310. }
  311. options._lastData = data;
  312. // 生成表头
  313. if (options.showHeader) {
  314. html.push('<thead><tr>');
  315. for (field in dataList[0]) {
  316. if (!inEffectiveFields(field, options)) {
  317. continue;
  318. }
  319. html.push('<th>', (options.effectiveFieldsAlias[field] || field),
  320. index === 0 ? ('(' + len + ')') : '' , // 表头第一列记录总数
  321. '</th>');
  322. index++;
  323. }
  324. html.push('</tr></thead>');
  325. }
  326. html.push('<tbody>');
  327. // console.log(data, len);
  328. // 按列加数据
  329. var dataI;
  330. for (i = 0; i < len; i++) {
  331. index = 0;
  332. tds = [];
  333. dataI = dataList[i];
  334. idValue = dataI[options.idField] || '';
  335. keyValue = dataI[options.keyField] || '';
  336. for (field in dataI) {
  337. // 标记作为 value 和 作为 id 的值
  338. if (!keyValue && options.indexKey === index) {
  339. keyValue = dataI[field];
  340. }
  341. if (!idValue && options.indexId === index) {
  342. idValue = dataI[field];
  343. }
  344. index++;
  345. // 列表中只显示有效的字段
  346. if (inEffectiveFields(field, options)) {
  347. tds.push('<td data-name="', field, '">', dataI[field], '</td>');
  348. }
  349. }
  350. html.push('<tr data-index="', (dataI.__index || i),
  351. '" data-id="', idValue,
  352. '" data-key="', keyValue,
  353. '" data-json="', encodeURIComponent(JSON.stringify(dataI)), '">',
  354. tds.join(''), '</tr>');
  355. }
  356. html.push('</tbody></table>');
  357. $dropdownMenu.html(html.join('')).show();
  358. // scrollbar 存在时,延时到动画结束时调整 padding
  359. setTimeout(function() {
  360. if (notNeedCalcPadding) {
  361. return;
  362. }
  363. var $table = $dropdownMenu.find('table:eq(0)'),
  364. pdr = 0,
  365. mgb = 0;
  366. if (
  367. $dropdownMenu.height() < $table.height() &&
  368. +$dropdownMenu.css('minWidth').replace('px', '') < $dropdownMenu.width()
  369. ) {
  370. pdr = 18;
  371. mgb = 20;
  372. }
  373. $dropdownMenu.css('paddingRight', pdr);
  374. $table.css('marginBottom', mgb);
  375. }, 301);
  376. adjustDropMenuPos($input, $dropdownMenu, options);
  377. return $input;
  378. }
  379. /**
  380. * ajax 获取数据
  381. * @param {Object} options
  382. * @return {Object} $.Deferred
  383. */
  384. function ajax(options, keyword) {
  385. keyword = keyword || '';
  386. var preAjax = options._preAjax;
  387. if (preAjax && preAjax.abort && preAjax.readyState !== 4) {
  388. // console.log('abort pre ajax');
  389. preAjax.abort();
  390. }
  391. var ajaxParam = {
  392. type: 'GET',
  393. dataType: options.jsonp ? 'jsonp' : 'json',
  394. timeout: 5000,
  395. };
  396. // jsonp
  397. if (options.jsonp) {
  398. ajaxParam.jsonp = options.jsonp;
  399. }
  400. // 自定义 ajax 请求参数生成方法
  401. var adjustAjaxParam,
  402. fnAdjustAjaxParam = options.fnAdjustAjaxParam;
  403. if ($.isFunction(fnAdjustAjaxParam)) {
  404. adjustAjaxParam = fnAdjustAjaxParam(keyword, options);
  405. // options.fnAdjustAjaxParam 返回false,则终止 ajax 请求
  406. if (FALSE === adjustAjaxParam) {
  407. return;
  408. }
  409. $.extend(ajaxParam, adjustAjaxParam);
  410. }
  411. // url 调整
  412. ajaxParam.url = function() {
  413. if (!keyword || ajaxParam.data) {
  414. return ajaxParam.url || options.url;
  415. }
  416. var type = '?';
  417. if (/=$/.test(options.url)) {
  418. type = '';
  419. } else if (/\?/.test(options.url)) {
  420. type = '&';
  421. }
  422. return options.url + type + encodeURIComponent(keyword);
  423. }();
  424. return options._preAjax = $.ajax(ajaxParam).done(function(result) {
  425. options.data = options.fnProcessData(result);
  426. }).fail(function(err) {
  427. if (options.fnAjaxFail) {
  428. options.fnAjaxFail(err, options);
  429. }
  430. });
  431. }
  432. /**
  433. * 检测 keyword 与 value 是否存在互相包含
  434. * @param {String} keyword 用户输入的关键字
  435. * @param {String} key 匹配字段的 key
  436. * @param {String} value key 字段对应的值
  437. * @param {Object} options
  438. * @return {Boolean} 包含/不包含
  439. */
  440. function isInWord(keyword, key, value, options) {
  441. value = $.trim(value);
  442. if (options.ignorecase) {
  443. keyword = keyword.toLocaleLowerCase();
  444. value = value.toLocaleLowerCase();
  445. }
  446. return value &&
  447. (inEffectiveFields(key, options) || inSearchFields(key, options)) && // 必须在有效的搜索字段中
  448. (
  449. ~value.indexOf(keyword) || // 匹配值包含关键字
  450. options.twoWayMatch && ~keyword.indexOf(value) // 关键字包含匹配值
  451. );
  452. }
  453. /**
  454. * 通过 ajax 或 json 参数获取数据
  455. */
  456. function getData(keyword, $input, callback, options) {
  457. var data, validData, filterData = {
  458. value: []
  459. },
  460. i, key, len,
  461. fnPreprocessKeyword = options.fnPreprocessKeyword;
  462. keyword = keyword || '';
  463. // 获取数据前对关键字预处理方法
  464. if ($.isFunction(fnPreprocessKeyword)) {
  465. keyword = fnPreprocessKeyword(keyword, options);
  466. }
  467. // 给了url参数,则从服务器 ajax 请求
  468. // console.log(options.url + keyword);
  469. if (options.url) {
  470. var timer;
  471. if (options.searchingTip) {
  472. timer = setTimeout(function() {
  473. showTip(options.searchingTip, $input, $input.parent().find('ul'), options);
  474. }, 600);
  475. }
  476. ajax(options, keyword).done(function(result) {
  477. callback($input, options.data, options); // 为 refreshDropMenu
  478. $input.trigger(onDataRequestSuccess, result);
  479. if (options.getDataMethod === 'firstByUrl') {
  480. options.url = null;
  481. }
  482. }).always(function() {
  483. timer && clearTimeout(timer);
  484. });
  485. } else {
  486. // 没有给出 url 参数,则从 data 参数获取
  487. data = options.data;
  488. validData = checkData(data);
  489. // 本地的 data 数据,则在本地过滤
  490. if (validData) {
  491. if (keyword) {
  492. // 输入不为空时则进行匹配
  493. len = data.value.length;
  494. for (i = 0; i < len; i++) {
  495. for (key in data.value[i]) {
  496. if (
  497. data.value[i][key] &&
  498. isInWord(keyword, key, data.value[i][key] + '', options)
  499. ) {
  500. filterData.value.push(data.value[i]);
  501. filterData.value[filterData.value.length - 1].__index = i;
  502. break;
  503. }
  504. }
  505. }
  506. } else {
  507. filterData = data;
  508. }
  509. }
  510. callback($input, filterData, options);
  511. } // else
  512. }
  513. /**
  514. * 数据处理
  515. * url 获取数据时,对数据的处理,作为 fnGetData 之后的回调处理
  516. */
  517. function processData(data) {
  518. return checkData(data);
  519. }
  520. /**
  521. * 取得 clearable 清除按钮
  522. */
  523. function getIClear($input, options) {
  524. var $iClear = $input.prev('i.clearable');
  525. // 是否可清除已输入的内容(添加清除按钮)
  526. if (options.clearable && !$iClear.length) {
  527. $iClear = $('<i class="clearable glyphicon glyphicon-remove"></i>')
  528. .prependTo($input.parent());
  529. }
  530. return $iClear.css({
  531. position: 'absolute',
  532. top: 12,
  533. right: options.showBtn ? ($input.next('.input-group-btn').width() || 33) + 2 : 12,
  534. zIndex: 4,
  535. cursor: 'pointer',
  536. fontSize: 12
  537. }).hide();
  538. }
  539. /**
  540. * 默认的配置选项
  541. * @type {Object}
  542. */
  543. var defaultOptions = {
  544. url: null, // 请求数据的 URL 地址
  545. jsonp: null, // 设置此参数名,将开启jsonp功能,否则使用json数据结构
  546. data: {
  547. value: []
  548. }, // 提示所用的数据,注意格式
  549. indexId: 0, // 每组数据的第几个数据,作为input输入框的 data-id,设为 -1 且 idField 为空则不设置此值
  550. indexKey: 0, // 每组数据的第几个数据,作为input输入框的内容
  551. idField: '', // 每组数据的哪个字段作为 data-id,优先级高于 indexId 设置(推荐)
  552. keyField: '', // 每组数据的哪个字段作为输入框内容,优先级高于 indexKey 设置(推荐)
  553. /* 搜索相关 */
  554. autoSelect: TRUE, // 键盘向上/下方向键时,是否自动选择值
  555. allowNoKeyword: TRUE, // 是否允许无关键字时请求数据
  556. getDataMethod: 'firstByUrl', // 获取数据的方式,url:一直从url请求;data:从 options.data 获取;firstByUrl:第一次从Url获取全部数据,之后从options.data获取
  557. delayUntilKeyup: FALSE, // 获取数据的方式 为 firstByUrl 时,是否延迟到有输入时才请求数据
  558. ignorecase: FALSE, // 前端搜索匹配时,是否忽略大小写
  559. effectiveFields: [], // 有效显示于列表中的字段,非有效字段都会过滤,默认全部有效。
  560. effectiveFieldsAlias: {}, // 有效字段的别名对象,用于 header 的显示
  561. searchFields: [], // 有效搜索字段,从前端搜索过滤数据时使用,但不一定显示在列表中。effectiveFields 配置字段也会用于搜索过滤
  562. twoWayMatch: TRUE, // 是否双向匹配搜索。为 true 即输入关键字包含或包含于匹配字段均认为匹配成功,为 false 则输入关键字包含于匹配字段认为匹配成功
  563. multiWord: FALSE, // 以分隔符号分割的多关键字支持
  564. separator: ',', // 多关键字支持时的分隔符,默认为半角逗号
  565. delay: 300, // 搜索触发的延时时间间隔,单位毫秒
  566. emptyTip: '', // 查询为空时显示的内容,可为 html
  567. searchingTip: '搜索中...', // ajax 搜索时显示的提示内容,当搜索时间较长时给出正在搜索的提示
  568. /* UI */
  569. autoDropup: FALSE, // 选择菜单是否自动判断向上展开。设为 true,则当下拉菜单高度超过窗体,且向上方向不会被窗体覆盖,则选择菜单向上弹出
  570. autoMinWidth: FALSE, // 是否自动最小宽度,设为 false 则最小宽度不小于输入框宽度
  571. showHeader: FALSE, // 是否显示选择列表的 header。为 true 时,有效字段大于一列则显示表头
  572. showBtn: TRUE, // 是否显示下拉按钮
  573. inputBgColor: '', // 输入框背景色,当与容器背景色不同时,可能需要该项的配置
  574. inputWarnColor: 'rgba(255,0,0,.1)', // 输入框内容不是下拉列表选择时的警告色
  575. listStyle: {
  576. 'padding-top': 0,
  577. 'max-height': '375px',
  578. 'max-width': '800px',
  579. 'overflow': 'auto',
  580. 'width': 'auto',
  581. 'transition': '0.3s',
  582. '-webkit-transition': '0.3s',
  583. '-moz-transition': '0.3s',
  584. '-o-transition': '0.3s'
  585. }, // 列表的样式控制
  586. listAlign: 'left', // 提示列表对齐位置,left/right/auto
  587. listHoverStyle: 'background: #07d; color:#fff', // 提示框列表鼠标悬浮的样式
  588. listHoverCSS: 'jhover', // 提示框列表鼠标悬浮的样式名称
  589. clearable: FALSE, // 是否可清除已输入的内容
  590. /* key */
  591. keyLeft: 37, // 向左方向键,不同的操作系统可能会有差别,则自行定义
  592. keyUp: 38, // 向上方向键
  593. keyRight: 39, // 向右方向键
  594. keyDown: 40, // 向下方向键
  595. keyEnter: 13, // 回车键
  596. /* methods */
  597. fnProcessData: processData, // 格式化数据的方法,返回数据格式参考 data 参数
  598. fnGetData: getData, // 获取数据的方法,无特殊需求一般不作设置
  599. fnAdjustAjaxParam: null, // 调整 ajax 请求参数方法,用于更多的请求配置需求。如对请求关键字作进一步处理、修改超时时间等
  600. fnPreprocessKeyword: null, // 搜索过滤数据前,对输入关键字作进一步处理方法。注意,应返回字符串
  601. fnAjaxFail: null, // ajax 失败时回调方法
  602. };
  603. var methods = {
  604. init: function(options) {
  605. // 参数设置
  606. var self = this;
  607. options = options || {};
  608. // 默认配置有效显示字段多于一个,则显示列表表头,否则不显示
  609. if (undefined === options.showHeader && options.effectiveFields && options.effectiveFields.length > 1) {
  610. options.showHeader = TRUE;
  611. }
  612. options = $.extend(TRUE, {}, defaultOptions, options);
  613. // 旧的方法兼容
  614. if (options.processData) {
  615. options.fnProcessData = options.processData;
  616. }
  617. if (options.getData) {
  618. options.fnGetData = options.getData;
  619. }
  620. if (options.getDataMethod === 'firstByUrl' && options.url && !options.delayUntilKeyup) {
  621. ajax(options).done(function(result) {
  622. options.url = null;
  623. self.trigger(onDataRequestSuccess, result);
  624. });
  625. }
  626. // 鼠标滑动到条目样式
  627. if (!$('#' + BSSUGGEST).length) {
  628. $('head:eq(0)').append('<style id="' + BSSUGGEST + '">.' + options.listHoverCSS + '{' + options.listHoverStyle + '}</style>');
  629. }
  630. return self.each(function() {
  631. var $input = $(this),
  632. $parent = $input.parent(),
  633. $iClear = getIClear($input, options),
  634. isMouseenterMenu,
  635. keyupTimer, // keyup 与 input 事件延时定时器
  636. $dropdownMenu = $parent.find('ul:eq(0)');
  637. // 验证输入框对象是否符合条件
  638. if (!checkInput($input, $dropdownMenu, options)) {
  639. console.warn('不是一个标准的 bootstrap 下拉式菜单或已初始化:', $input);
  640. return;
  641. }
  642. // 是否显示 button 按钮
  643. if (!options.showBtn) {
  644. $input.css('borderRadius', 4);
  645. $parent.css('width', '100%')
  646. .find('.btn:eq(0)').hide();
  647. }
  648. // 移除 disabled 类,并禁用自动完成
  649. $input.removeClass(DISABLED).prop(DISABLED, FALSE).attr('autocomplete', 'off');
  650. // dropdown-menu 增加修饰
  651. $dropdownMenu.css(options.listStyle);
  652. // 默认背景色
  653. if (!options.inputBgColor) {
  654. options.inputBgColor = $input.css('backgroundColor');
  655. }
  656. // 开始事件处理
  657. $input.on('keydown', function(event) {
  658. var currentList, tipsKeyword; // 提示列表上被选中的关键字
  659. // 当提示层显示时才对键盘事件处理
  660. if (!$dropdownMenu.is(':visible')) {
  661. setOrGetDataId($input, '');
  662. return;
  663. }
  664. currentList = $dropdownMenu.find('.' + options.listHoverCSS);
  665. tipsKeyword = ''; // 提示列表上被选中的关键字
  666. unHoverAll($dropdownMenu, options);
  667. if (event.keyCode === options.keyDown) { // 如果按的是向下方向键
  668. if (!currentList.length) {
  669. // 如果提示列表没有一个被选中,则将列表第一个选中
  670. tipsKeyword = getPointKeyword($dropdownMenu.find('tbody tr:first').mouseover());
  671. } else if (!currentList.next().length) {
  672. // 如果是最后一个被选中,则取消选中,即可认为是输入框被选中,并恢复输入的值
  673. if (options.autoSelect) {
  674. setOrGetDataId($input, '').val(setOrGetAlt($input));
  675. }
  676. } else {
  677. // 选中下一行
  678. tipsKeyword = getPointKeyword(currentList.next().mouseover());
  679. }
  680. // 控制滑动条
  681. adjustScroll($input, $dropdownMenu, options);
  682. if (!options.autoSelect) {
  683. return;
  684. }
  685. } else if (event.keyCode === options.keyUp) { // 如果按的是向上方向键
  686. if (!currentList.length) {
  687. tipsKeyword = getPointKeyword($dropdownMenu.find('tbody tr:last').mouseover());
  688. } else if (!currentList.prev().length) {
  689. if (options.autoSelect) {
  690. setOrGetDataId($input, '').val(setOrGetAlt($input));
  691. }
  692. } else {
  693. // 选中前一行
  694. tipsKeyword = getPointKeyword(currentList.prev().mouseover());
  695. }
  696. // 控制滑动条
  697. adjustScroll($input, $dropdownMenu, options);
  698. if (!options.autoSelect) {
  699. return;
  700. }
  701. } else if (event.keyCode === options.keyEnter) {
  702. tipsKeyword = getPointKeyword(currentList);
  703. $dropdownMenu.hide(); // .empty();
  704. } else {
  705. setOrGetDataId($input, '');
  706. }
  707. // 设置值 tipsKeyword
  708. // console.log(tipsKeyword);
  709. setValue($input, tipsKeyword, options);
  710. }).on('compositionstart', function(event) {
  711. // 中文输入开始,锁定
  712. // console.log('compositionstart');
  713. inputLock = TRUE;
  714. }).on('compositionend', function(event) {
  715. // 中文输入结束,解除锁定
  716. // console.log('compositionend');
  717. inputLock = FALSE;
  718. }).on('keyup input paste', function(event) {
  719. var word;
  720. if (event.keyCode) {
  721. setBackground($input, options);
  722. }
  723. // 如果弹起的键是回车、向上或向下方向键则返回
  724. if (~$.inArray(event.keyCode, [options.keyDown, options.keyUp, options.keyEnter])) {
  725. $input.val($input.val()); // 让鼠标输入跳到最后
  726. return;
  727. }
  728. clearTimeout(keyupTimer);
  729. keyupTimer = setTimeout(function() {
  730. // console.log('input keyup', event);
  731. // 锁定状态,返回
  732. if (inputLock) {
  733. return;
  734. }
  735. word = $input.val();
  736. // 若输入框值没有改变则返回
  737. if ($.trim(word) && word === setOrGetAlt($input)) {
  738. return;
  739. }
  740. // 当按下键之前记录输入框值,以方便查看键弹起时值有没有变
  741. setOrGetAlt($input, word);
  742. if (options.multiWord) {
  743. word = word.split(options.separator).reverse()[0];
  744. }
  745. // 是否允许空数据查询
  746. if (!word.length && !options.allowNoKeyword) {
  747. return;
  748. }
  749. if (options.url) {
  750. options.fnGetData($.trim(word), $input, refreshDropMenu, options);
  751. }
  752. }, options.delay || 300);
  753. }).on('focus', function() {
  754. // console.log('input focus');
  755. adjustDropMenuPos($input, $dropdownMenu, options);
  756. }).on('blur', function() {
  757. if (!isMouseenterMenu) { // 不是进入下拉列表状态,则隐藏列表
  758. $dropdownMenu.css('display', '');
  759. }
  760. }).on('click', function() {
  761. // console.log('input click');
  762. var word = $input.val();
  763. if (
  764. $.trim(word) &&
  765. word === setOrGetAlt($input) &&
  766. $dropdownMenu.find('table tr').length
  767. ) {
  768. return $dropdownMenu.show();
  769. }
  770. // if ($dropdownMenu.css('display') !== 'none') {
  771. if ($dropdownMenu.is(':visible')) {
  772. return;
  773. }
  774. if (options.multiWord) {
  775. word = word.split(options.separator).reverse()[0];
  776. }
  777. // 是否允许空数据查询
  778. if (!word.length && !options.allowNoKeyword) {
  779. return;
  780. }
  781. // console.log('word', word);
  782. options.fnGetData($.trim(word), $input, refreshDropMenu, options);
  783. });
  784. // 下拉按钮点击时
  785. $parent.find('.btn:eq(0)').attr('data-toggle', '').click(function() {
  786. var display = 'none';
  787. // if ($dropdownMenu.is(':visible')) {
  788. if ($dropdownMenu.css('display') === display) {
  789. display = 'block';
  790. if (options.url) {
  791. $input.click().focus();
  792. if (!$dropdownMenu.find('tr').length) {
  793. display = 'none';
  794. }
  795. } else {
  796. // 不以 keyword 作为过滤,展示所有的数据
  797. refreshDropMenu($input, options.data, options);
  798. }
  799. }
  800. $dropdownMenu.css('display', display);
  801. return FALSE;
  802. });
  803. // 列表中滑动时,输入框失去焦点
  804. $dropdownMenu.mouseenter(function() {
  805. // console.log('mouseenter')
  806. isMouseenterMenu = 1;
  807. $input.blur();
  808. }).mouseleave(function() {
  809. // console.log('mouseleave')
  810. isMouseenterMenu = 0;
  811. $input.focus();
  812. }).on('mouseenter', 'tbody tr', function() {
  813. // 行上的移动事件
  814. unHoverAll($dropdownMenu, options);
  815. $(this).addClass(options.listHoverCSS);
  816. return FALSE; // 阻止冒泡
  817. })
  818. .on('mousedown', 'tbody tr', function() {
  819. var keywords = getPointKeyword($(this));
  820. setValue($input, keywords, options);
  821. setOrGetAlt($input, keywords.key);
  822. setJson($input, keywords.json);
  823. setBackground($input, options);
  824. $dropdownMenu.hide();
  825. });
  826. // 存在清空按钮
  827. if ($iClear.length) {
  828. $iClear.click(function () {
  829. setOrGetDataId($input, '').val('');
  830. setBackground($input, options);
  831. });
  832. $parent.mouseenter(function() {
  833. if (!$input.prop(DISABLED)) {
  834. $iClear.show();
  835. }
  836. }).mouseleave(function() {
  837. $iClear.hide();
  838. });
  839. }
  840. });
  841. },
  842. show: function() {
  843. return this.each(function() {
  844. $(this).click();
  845. });
  846. },
  847. hide: function() {
  848. return this.each(function() {
  849. $(this).parent().find('ul:eq(0)').css('display', '');
  850. });
  851. },
  852. disable: function() {
  853. return this.each(function() {
  854. $(this).attr(DISABLED, TRUE)
  855. .parent().find('.btn:eq(0)').prop(DISABLED, TRUE);
  856. });
  857. },
  858. enable: function() {
  859. return this.each(function() {
  860. $(this).attr(DISABLED, FALSE)
  861. .parent().find('.btn:eq(0)').prop(DISABLED, FALSE);
  862. });
  863. },
  864. destroy: function() {
  865. return this.each(function() {
  866. $(this).off().removeData(BSSUGGEST).removeAttr('style')
  867. .parent().find('.btn:eq(0)').off().show().attr('data-toggle', 'dropdown').prop(DISABLED, FALSE) // .addClass(DISABLED);
  868. .next().css('display', '').off();
  869. });
  870. },
  871. version: function() {
  872. return VERSION;
  873. }
  874. };
  875. $.fn[BSSUGGEST] = function(options) {
  876. // 方法判断
  877. if (typeof options === 'string' && methods[options]) {
  878. var inited = TRUE;
  879. this.each(function() {
  880. if (!$(this).data(BSSUGGEST)) {
  881. return inited = FALSE;
  882. }
  883. });
  884. // 只要有一个未初始化,则全部都不执行方法,除非是 init 或 version
  885. if (!inited && 'init' !== options && 'version' !== options) {
  886. return this;
  887. }
  888. // 如果是方法,则参数第一个为函数名,从第二个开始为函数参数
  889. return methods[options].apply(this, [].slice.call(arguments, 1));
  890. } else {
  891. // 调用初始化方法
  892. return methods.init.apply(this, arguments);
  893. }
  894. }
  895. });