bootstrap-suggest.min.js 38 KB

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