bootstrap-suggest.min.js 38 KB

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