jquery.dropdown.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. ;
  2. (function ($) {
  3. 'use strict';
  4. function throttle(func, wait, options) {
  5. var context, args, result;
  6. var timeout = null;
  7. // 上次执行时间点
  8. var previous = 0;
  9. if (!options) options = {};
  10. // 延迟执行函数
  11. var later = function () {
  12. // 若设定了开始边界不执行选项,上次执行时间始终为0
  13. previous = options.leading === false ? 0 : new Date().getTime();
  14. timeout = null;
  15. result = func.apply(context, args);
  16. if (!timeout) context = args = null;
  17. };
  18. return function () {
  19. var now = new Date().getTime();
  20. // 首次执行时,如果设定了开始边界不执行选项,将上次执行时间设定为当前时间。
  21. if (!previous && options.leading === false) previous = now;
  22. // 延迟执行时间间隔
  23. var remaining = wait - (now - previous);
  24. context = this;
  25. args = arguments;
  26. // 延迟时间间隔remaining小于等于0,表示上次执行至此所间隔时间已经超过一个时间窗口
  27. // remaining大于时间窗口wait,表示客户端系统时间被调整过
  28. if (remaining <= 0 || remaining > wait) {
  29. clearTimeout(timeout);
  30. timeout = null;
  31. previous = now;
  32. result = func.apply(context, args);
  33. if (!timeout) context = args = null;
  34. //如果延迟执行不存在,且没有设定结尾边界不执行选项
  35. } else if (!timeout && options.trailing !== false) {
  36. timeout = setTimeout(later, remaining);
  37. }
  38. return result;
  39. };
  40. }
  41. var isSafari = function () {
  42. var ua = navigator.userAgent.toLowerCase();
  43. if (ua.indexOf('safari') !== -1) {
  44. return ua.indexOf('chrome') > -1 ? false : true;
  45. }
  46. }();
  47. var settings = {
  48. readonly: false,
  49. limitCount: Infinity,
  50. input: '<input type="text" maxLength="20" placeholder="搜索关键词或ID">',
  51. data: [],
  52. searchable: true,
  53. searchNoData: '<li style="color:#ddd">查无数据</li>',
  54. choice: function choice() {}
  55. };
  56. var KEY_CODE = {
  57. up: 38,
  58. down: 40,
  59. enter: 13
  60. };
  61. var EVENT_SPACE = {
  62. click: 'click.iui-dropdown',
  63. focus: 'focus.iui-dropdown',
  64. keydown: 'keydown.iui-dropdown',
  65. keyup: 'keyup.iui-dropdown'
  66. };
  67. // 创建模板
  68. function createTemplate() {
  69. var isLabelMode = this.isLabelMode;
  70. var searchable = this.config.searchable;
  71. var templateSearch = searchable ? '<span class="dropdown-search">' + this.config.input + '</span>' : '';
  72. return isLabelMode ? '<div class="dropdown-display-label"><div class="dropdown-chose-list">' + templateSearch + '</div></div><div class="dropdown-main">{{ul}}</div>' : '<a href="javascript:;" class="dropdown-display"><span class="dropdown-chose-list"></span><a href="javascript:;" class="dropdown-clear-all">\xD7</a></a><div class="dropdown-main">' + templateSearch + '{{ul}}</div>';
  73. }
  74. // 超出限制提示
  75. function maxItemAlert() {
  76. var _dropdown = this;
  77. var _config = _dropdown.config;
  78. var $el = _dropdown.$el;
  79. var $alert = $el.find('.dropdown-maxItem-alert');
  80. clearTimeout(_dropdown.maxItemAlertTimer);
  81. if ($alert.length === 0) {
  82. $alert = $('<div class="dropdown-maxItem-alert">\u6700\u591A\u53EF\u9009\u62E9' + _config.limitCount + '\u4E2A</div>');
  83. }
  84. $el.append($alert);
  85. _dropdown.maxItemAlertTimer = setTimeout(function () {
  86. $el.find('.dropdown-maxItem-alert').remove();
  87. }, 1000);
  88. }
  89. // select-option 转 ul-li
  90. function selectToDiv(str) {
  91. var result = str || '';
  92. // 移除select标签
  93. result = result.replace(/<select[^>]*>/gi, '').replace('</select>', '');
  94. // 移除 optgroup 结束标签
  95. result = result.replace(/<\/optgroup>/gi, '');
  96. result = result.replace(/<optgroup[^>]*>/gi, function (matcher) {
  97. var groupName = /label="(.[^"]*)"(\s|>)/.exec(matcher);
  98. var groupId = /data\-group\-id="(.[^"]*)"(\s|>)/.exec(matcher);
  99. return '<li class="dropdown-group" data-group-id="' + (groupId ? groupId[1] : '') + '">' + (groupName ? groupName[1] : '') + '</li>';
  100. });
  101. result = result.replace(/<option(.*?)<\/option>/gi, function (matcher) {
  102. var value = /value="?([\w\u4E00-\u9FA5\uF900-\uFA2D]+)"?/.exec(matcher);
  103. var name = />(.*)<\//.exec(matcher);
  104. // 强制要求html中使用selected/disabled,而不是selected="selected","disabled="disabled"
  105. var isSelected = matcher.indexOf('selected') > -1 ? true : false;
  106. var isDisabled = matcher.indexOf('disabled') > -1 ? true : false;
  107. return '<li ' + (isDisabled ? ' disabled' : ' tabindex="0"') + ' data-value="' + (value ? value[1] : '') + '" class="dropdown-option ' + (isSelected ? 'dropdown-chose' : '') + '">' + (name ? name[1] : '') + '</li>';
  108. });
  109. return result;
  110. }
  111. // object-data 转 select-option
  112. function objectToSelect(data) {
  113. var map = {};
  114. var result = '';
  115. var name = [];
  116. var selectAmount = 0;
  117. if (!data || !data.length) {
  118. return false;
  119. }
  120. $.each(data, function (index, val) {
  121. // disable 权重高于 selected
  122. var hasGroup = val.groupId;
  123. var isDisabled = val.disabled ? ' disabled' : '';
  124. var isSelected = val.selected && !isDisabled ? ' selected' : '';
  125. var temp = '<option' + isDisabled + isSelected + ' value="' + val.id + '">' + val.name + '</option>';
  126. if (isSelected) {
  127. name.push('<span class="dropdown-selected">' + val.name + '<i class="del" data-id="' + val.id + '"></i></span>');
  128. selectAmount++;
  129. }
  130. // 判断是否有分组
  131. if (hasGroup) {
  132. if (map[val.groupId]) {
  133. map[val.groupId] += temp;
  134. } else {
  135. // &janking& just a separator
  136. map[val.groupId] = val.groupName + '&janking&' + temp;
  137. }
  138. } else {
  139. map[index] = temp;
  140. }
  141. });
  142. $.each(map, function (index, val) {
  143. var option = val.split('&janking&');
  144. // 判断是否有分组
  145. if (option.length === 2) {
  146. var groupName = option[0];
  147. var items = option[1];
  148. result += '<optgroup label="' + groupName + '" data-group-id="' + index + '">' + items + '</optgroup>';
  149. } else {
  150. result += val;
  151. }
  152. });
  153. return [result, name, selectAmount];
  154. }
  155. // select-option 转 object-data
  156. //
  157. function selectToObject(el) {
  158. var $select = el;
  159. var result = [];
  160. function readOption(key, el) {
  161. var $option = $(el);
  162. this.id = $option.prop('value');
  163. this.name = $option.text();
  164. this.disabled = $option.prop('disabled');
  165. this.selected = $option.prop('selected');
  166. }
  167. $.each($select.children(), function (key, el) {
  168. var tmp = {};
  169. var tmpGroup = {};
  170. var $el = $(el);
  171. if (el.nodeName === 'OPTGROUP') {
  172. tmpGroup.groupId = $el.data('groupId');
  173. tmpGroup.groupName = $el.attr('label');
  174. $.each($el.children(), $.proxy(readOption, tmp));
  175. $.extend(tmp, tmpGroup);
  176. } else {
  177. $.each($el, $.proxy(readOption, tmp));
  178. }
  179. result.push(tmp);
  180. });
  181. return result;
  182. }
  183. var action = {
  184. show: function show(event) {
  185. event.stopPropagation();
  186. var _dropdown = this;
  187. $(document).trigger('click.dropdown');
  188. _dropdown.$el.toggleClass('active');
  189. },
  190. search: throttle(function (event) {
  191. var _dropdown = this;
  192. var _config = _dropdown.config;
  193. var $el = _dropdown.$el;
  194. var $input = $(event.target);
  195. var intputValue = $input.val();
  196. var data = _dropdown.config.data;
  197. var result = [];
  198. if (event.keyCode > 36 && event.keyCode < 41) {
  199. return;
  200. }
  201. $.each(data, function (key, value) {
  202. if (value.name.toLowerCase().indexOf(intputValue) > -1 || '' + value.id === '' + intputValue) {
  203. result.push(value);
  204. }
  205. });
  206. $el.find('ul').html(selectToDiv(objectToSelect(result)[0]) || _config.searchNoData);
  207. }, 300),
  208. control: function control(event) {
  209. var keyCode = event.keyCode;
  210. var KC = KEY_CODE;
  211. var index = 0;
  212. var direct;
  213. var itemIndex;
  214. var $items;
  215. if (keyCode === KC.down || keyCode === KC.up) {
  216. // 方向
  217. direct = keyCode === KC.up ? -1 : 1;
  218. $items = this.$el.find('[tabindex]');
  219. itemIndex = $items.index($(document.activeElement));
  220. // 初始
  221. if (itemIndex === -1) {
  222. index = direct + 1 ? -1 : 0;
  223. } else {
  224. index = itemIndex;
  225. }
  226. // 确认位序
  227. index = index + direct;
  228. // 最后位循环
  229. if (index === $items.length) {
  230. index = 0;
  231. }
  232. $items.eq(index).focus();
  233. event.preventDefault();
  234. }
  235. },
  236. multiChoose: function multiChoose(event) {
  237. var _dropdown = this;
  238. var _config = _dropdown.config;
  239. var $select = _dropdown.$select;
  240. var $target = $(event.target);
  241. var value = $target.attr('data-value');
  242. var hasSelected = $target.hasClass('dropdown-chose');
  243. var selectedName = [];
  244. if (hasSelected) {
  245. $target.removeClass('dropdown-chose');
  246. _dropdown.selectAmount--;
  247. } else {
  248. if (_dropdown.selectAmount < _config.limitCount) {
  249. $target.addClass('dropdown-chose');
  250. _dropdown.selectAmount++;
  251. } else {
  252. maxItemAlert.call(_dropdown);
  253. return false;
  254. }
  255. }
  256. _dropdown.name = [];
  257. $.each(_config.data, function (key, item) {
  258. if ('' + item.id === '' + value) {
  259. item.selected = hasSelected ? false : true;
  260. }
  261. if (item.selected) {
  262. selectedName.push(item.name);
  263. _dropdown.name.push('<span class="dropdown-selected">' + item.name + '<i class="del" data-id="' + item.id + '"></i></span>');
  264. }
  265. });
  266. $select.find('option[value="' + value + '"]').prop('selected', hasSelected ? false : true);
  267. _dropdown.$choseList.find('.dropdown-selected').remove();
  268. _dropdown.$choseList.prepend(_dropdown.name.join(''));
  269. _dropdown.$el.find('.dropdown-display').attr('title', selectedName.join(','));
  270. _config.choice.call(_dropdown, event);
  271. },
  272. singleChoose: function singleChoose(event) {
  273. var _dropdown = this;
  274. var _config = _dropdown.config;
  275. var $el = _dropdown.$el;
  276. var $select = _dropdown.$select;
  277. var $target = $(event.target);
  278. var value = $target.attr('data-value');
  279. var hasSelected = $target.hasClass('dropdown-chose');
  280. _dropdown.name = [];
  281. if ($target.hasClass('dropdown-chose')) {
  282. return false;
  283. }
  284. $el.removeClass('active').find('li').not($target).removeClass('dropdown-chose');
  285. $target.toggleClass('dropdown-chose');
  286. $.each(_config.data, function (key, item) {
  287. // id 有可能是数字也有可能是字符串,强制全等有弊端 2017-03-20 22:19:21
  288. item.selected = false;
  289. if ('' + item.id === '' + value) {
  290. item.selected = hasSelected ? 0 : 1;
  291. if (item.selected) {
  292. _dropdown.name.push('<span class="dropdown-selected">' + item.name + '<i class="del" data-id="' + item.id + '"></i></span>');
  293. }
  294. }
  295. });
  296. $select.find('option[value="' + value + '"]').prop('selected', true);
  297. _dropdown.name.push('<span class="placeholder">' + _dropdown.placeholder + '</span>');
  298. _dropdown.$choseList.html(_dropdown.name.join(''));
  299. _config.choice.call(_dropdown, event);
  300. },
  301. del: function del(event) {
  302. var _dropdown = this;
  303. var $target = $(event.target);
  304. var id = $target.data('id');
  305. // 2017-03-23 15:58:50 测试
  306. // 10000条数据测试删除,耗时 ~3ms
  307. $.each(_dropdown.name, function (key, value) {
  308. if (value.indexOf('data-id="' + id + '"') !== -1) {
  309. _dropdown.name.splice(key, 1);
  310. return false;
  311. }
  312. });
  313. $.each(_dropdown.config.data, function (key, item) {
  314. if ('' + item.id == '' + id) {
  315. item.selected = false;
  316. return false;
  317. }
  318. });
  319. _dropdown.selectAmount--;
  320. _dropdown.$el.find('[data-value="' + id + '"]').removeClass('dropdown-chose');
  321. _dropdown.$el.find('[value="' + id + '"]').prop('selected', false).removeAttr('selected');
  322. $target.closest('.dropdown-selected').remove();
  323. return false;
  324. },
  325. clearAll: function clearAll(event) {
  326. event.preventDefault();
  327. this.$choseList.find('.del').each(function (index, el) {
  328. $(el).trigger('click');
  329. });
  330. this.$el.find('.dropdown-display').removeAttr('title');
  331. return false;
  332. }
  333. };
  334. function Dropdown(options, el) {
  335. this.$el = $(el);
  336. this.$select = this.$el.find('select');
  337. this.placeholder = this.$select.attr('placeholder');
  338. this.config = options;
  339. this.name = [];
  340. this.isSingleSelect = !this.$select.prop('multiple');
  341. this.selectAmount = 0;
  342. this.maxItemAlertTimer = null;
  343. this.isLabelMode = this.config.multipleMode === 'label';
  344. this.init();
  345. }
  346. Dropdown.prototype = {
  347. init: function init() {
  348. var _this = this;
  349. var _config = _this.config;
  350. var $el = _this.$el;
  351. _this.$select.hide();
  352. // 判断dropdown是否单选,是否token模式
  353. $el.addClass(_this.isSingleSelect ? 'dropdown-single' : _this.isLabelMode ? 'dropdown-multiple-label' : 'dropdown-multiple');
  354. if (_config.data.length === 0) {
  355. _config.data = selectToObject(_this.$select);
  356. }
  357. var processResult = objectToSelect(_config.data);
  358. _this.name = processResult[1];
  359. _this.selectAmount = processResult[2];
  360. _this.$select.html(processResult[0]);
  361. _this.renderSelect();
  362. // disabled权重高于readonly
  363. _this.changeStatus(_config.disabled ? 'disabled' : _config.readonly ? 'readonly' : false);
  364. },
  365. // 渲染 select 为 dropdown
  366. renderSelect: function renderSelect(isUpdate, isCover) {
  367. var _this = this;
  368. var $el = _this.$el;
  369. var $select = _this.$select;
  370. var elemLi = selectToDiv($select.prop('outerHTML'));
  371. var template;
  372. if (isUpdate) {
  373. $el.find('ul')[isCover ? 'html' : 'append'](elemLi);
  374. } else {
  375. template = createTemplate.call(_this).replace('{{ul}}', '<ul>' + elemLi + '</ul>');
  376. $el.append(template).find('ul').removeAttr('style class');
  377. }
  378. if (isCover) {
  379. _this.name = [];
  380. _this.$el.find('.dropdown-selected').remove();
  381. _this.$select.val('');
  382. }
  383. _this.$choseList = $el.find('.dropdown-chose-list');
  384. if (!_this.isLabelMode) {
  385. _this.$choseList.html($('<span class="placeholder"></span>').text(_this.placeholder));
  386. }
  387. _this.$choseList.prepend(_this.name.join(''));
  388. },
  389. bindEvent: function bindEvent() {
  390. var _this = this;
  391. var $el = _this.$el;
  392. var openHandle = isSafari ? EVENT_SPACE.click : EVENT_SPACE.focus;
  393. $el.on(EVENT_SPACE.click, function (event) {
  394. event.stopPropagation();
  395. });
  396. $el.on(EVENT_SPACE.click, '.del', $.proxy(action.del, _this));
  397. // show
  398. if (_this.isLabelMode) {
  399. $el.on(EVENT_SPACE.click, '.dropdown-display-label', function () {
  400. $el.find('input').focus();
  401. });
  402. $el.on(EVENT_SPACE.focus, 'input', $.proxy(action.show, _this));
  403. $el.on(EVENT_SPACE.keydown, 'input', function (event) {
  404. if (event.keyCode === 8 && this.value === '' && _this.name.length) {
  405. $el.find('.del').eq(-1).trigger('click');
  406. }
  407. });
  408. } else {
  409. $el.on(openHandle, '.dropdown-display', $.proxy(action.show, _this));
  410. $el.on(openHandle, '.dropdown-clear-all', $.proxy(action.clearAll, _this));
  411. }
  412. // 搜索
  413. $el.on(EVENT_SPACE.keyup, 'input', $.proxy(action.search, _this));
  414. // 按下enter键设置token
  415. $el.on(EVENT_SPACE.keyup, function (event) {
  416. var keyCode = event.keyCode;
  417. var KC = KEY_CODE;
  418. if (keyCode === KC.enter) {
  419. $.proxy(_this.isSingleSelect ? action.singleChoose : action.multiChoose, _this, event)();
  420. }
  421. });
  422. // 按下上下键切换token
  423. $el.on(EVENT_SPACE.keydown, $.proxy(action.control, _this));
  424. $el.on(EVENT_SPACE.click, '[tabindex]', $.proxy(_this.isSingleSelect ? action.singleChoose : action.multiChoose, _this));
  425. },
  426. unbindEvent: function unbindEvent() {
  427. var _this = this;
  428. var $el = _this.$el;
  429. var openHandle = isSafari ? EVENT_SPACE.click : EVENT_SPACE.focus;
  430. $el.off(EVENT_SPACE.click);
  431. $el.off(EVENT_SPACE.click, '.del');
  432. // show
  433. if (_this.isLabelMode) {
  434. $el.off(EVENT_SPACE.click, '.dropdown-display-label');
  435. $el.off(EVENT_SPACE.focus, 'input');
  436. $el.off(EVENT_SPACE.keydown, 'input');
  437. } else {
  438. $el.off(openHandle, '.dropdown-display');
  439. $el.off(openHandle, '.dropdown-clear-all');
  440. }
  441. // 搜索
  442. $el.off(EVENT_SPACE.keyup, 'input');
  443. // 按下enter键设置token
  444. $el.off(EVENT_SPACE.keyup);
  445. // 按下上下键切换token
  446. $el.off(EVENT_SPACE.keydown);
  447. $el.off(EVENT_SPACE.click, '[tabindex]');
  448. },
  449. changeStatus: function changeStatus(status) {
  450. var _this = this;
  451. if (status === 'readonly') {
  452. _this.unbindEvent();
  453. } else if (status === 'disabled') {
  454. _this.$select.prop('disabled', true);
  455. _this.unbindEvent();
  456. } else {
  457. _this.$select.prop('disabled', false);
  458. _this.bindEvent();
  459. }
  460. },
  461. update: function (data, isCover) {
  462. var _this = this;
  463. var _config = _this.config;
  464. var $el = _this.$el;
  465. var _isCover = isCover || false;
  466. if (Object.prototype.toString.call(data) !== '[object Array]') {
  467. return;
  468. }
  469. _config.data = _isCover ? data.slice(0) : _config.data.concat(data);
  470. var processResult = objectToSelect(_config.data);
  471. _this.name = processResult[1];
  472. _this.selectAmount = processResult[2];
  473. _this.$select.html(processResult[0]);
  474. _this.renderSelect(true, _isCover);
  475. },
  476. destroy: function () {
  477. this.unbindEvent();
  478. this.$el.children().not('select').remove();
  479. this.$el.removeClass('dropdown-single dropdown-multiple-label dropdown-multiple');
  480. this.$select.show();
  481. }
  482. };
  483. $(document).on('click.dropdown', function () {
  484. $('.dropdown-single,.dropdown-multiple,.dropdown-multiple-label').removeClass('active');
  485. });
  486. $.fn.dropdown = function (options) {
  487. this.each(function (index, el) {
  488. $(el).data('dropdown', new Dropdown($.extend(true, {}, settings, options), el));
  489. });
  490. return this;
  491. }
  492. })(jQuery);