bootstrap-suggest.js 35 KB

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