商丘12345 前端

jquery.combo.select.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. /*jshint asi:true, expr:true */
  2. /**
  3. * Plugin Name: Combo Select
  4. * Author : Vinay@Pebbleroad
  5. * Date: 23/11/2014
  6. * Description:
  7. * Converts a select box into a searchable and keyboard friendly interface. Fallbacks to native select on mobile and tablets
  8. */
  9. /*
  10. * 第220行 zhangshuangnan 注释
  11. * */
  12. // Expose plugin as an AMD module if AMD loader is present:
  13. (function (factory) {
  14. 'use strict';
  15. if (typeof define === 'function' && define.amd) {
  16. // AMD. Register as an anonymous module.
  17. define(['jquery'], factory);
  18. } else if (typeof exports === 'object' && typeof require === 'function') {
  19. // Browserify
  20. factory(require('jquery'));
  21. } else {
  22. // Browser globals
  23. factory(jQuery);
  24. }
  25. }(function ( $, undefined ) {
  26. var pluginName = "comboSelect",
  27. dataKey = 'comboselect';
  28. var defaults = {
  29. comboClass : 'combo-select',
  30. comboArrowClass : 'combo-arrow',
  31. comboDropDownClass : 'combo-dropdown',
  32. inputClass : 'combo-input text-input',
  33. disabledClass : 'option-disabled',
  34. hoverClass : 'option-hover',
  35. selectedClass : 'option-selected',
  36. markerClass : 'combo-marker',
  37. themeClass : '',
  38. maxHeight : 200,
  39. extendStyle : true,
  40. focusInput : true
  41. };
  42. /**
  43. * Utility functions
  44. */
  45. var keys = {
  46. ESC: 27,
  47. TAB: 9,
  48. RETURN: 13,
  49. LEFT: 37,
  50. UP: 38,
  51. RIGHT: 39,
  52. DOWN: 40,
  53. ENTER: 13,
  54. SHIFT: 16
  55. },
  56. isMobile = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()));
  57. /**
  58. * Constructor
  59. * @param {[Node]} element [Select element]
  60. * @param {[Object]} options [Option object]
  61. */
  62. function Plugin ( element, options ) {
  63. /* Name of the plugin */
  64. this._name = pluginName;
  65. /* Reverse lookup */
  66. this.el = element
  67. /* Element */
  68. this.$el = $(element)
  69. /* If multiple select: stop */
  70. if(this.$el.prop('multiple')) return;
  71. /* Settings */
  72. this.settings = $.extend( {}, defaults, options, this.$el.data() );
  73. /* Defaults */
  74. this._defaults = defaults;
  75. /* Options */
  76. this.$options = this.$el.find('option, optgroup');
  77. /* Initialize */
  78. this.init();
  79. /* Instances */
  80. $.fn[ pluginName ].instances.push(this);
  81. }
  82. $.extend(Plugin.prototype, {
  83. init: function () {
  84. /* Construct the comboselect */
  85. this._construct();
  86. /* Add event bindings */
  87. this._events();
  88. },
  89. _construct: function(){
  90. var self = this
  91. /**
  92. * Add negative TabIndex to `select`
  93. * Preserves previous tabindex
  94. */
  95. this.$el.data('plugin_'+ dataKey + '_tabindex', this.$el.prop('tabindex'))
  96. /* Add a tab index for desktop browsers */
  97. !isMobile && this.$el.prop("tabIndex", -1)
  98. /**
  99. * Wrap the Select
  100. */
  101. this.$container = this.$el.wrapAll('<div class="' + this.settings.comboClass + ' '+ this.settings.themeClass + '" />').parent();
  102. /**
  103. * Check if select has a width attribute
  104. */
  105. if(this.settings.extendStyle && this.$el.attr('style')){
  106. this.$container.attr('style', this.$el.attr("style"))
  107. }
  108. /**
  109. * Append dropdown arrow
  110. */
  111. this.$arrow = $('<div class="'+ this.settings.comboArrowClass+ '" />').appendTo(this.$container)
  112. /**
  113. * Append dropdown
  114. */
  115. this.$dropdown = $('<ul class="'+this.settings.comboDropDownClass+'" />').appendTo(this.$container)
  116. /**
  117. * Create dropdown options
  118. */
  119. var o = '', k = 0, p = '';
  120. this.selectedIndex = this.$el.prop('selectedIndex')
  121. this.$options.each(function(i, e){
  122. if(e.nodeName.toLowerCase() == 'optgroup'){
  123. return o+='<li class="option-group">'+this.label+'</li>'
  124. }
  125. if(!e.value) p = e.innerHTML
  126. o+='<li class="'+(this.disabled? self.settings.disabledClass : "option-item") + ' ' +(k == self.selectedIndex? self.settings.selectedClass : '')+ '" data-index="'+(k)+'" data-value="'+this.value+'">'+ (this.innerHTML) + '</li>'
  127. k++;
  128. })
  129. this.$dropdown.html(o)
  130. /**
  131. * Items
  132. */
  133. this.$items = this.$dropdown.children();
  134. /**
  135. * Append Input
  136. */
  137. this.$input = $('<input placeholder="请选择" type="text"' + (isMobile? 'tabindex="-1"': '') + ' value="'+p+'" class="'+ this.settings.inputClass + '" autocomplete="off" id="'+ this.$el.attr("id")+'_input">').appendTo(this.$container)
  138. /* Update input text */
  139. this._updateInput()
  140. },
  141. _events: function(){
  142. /* Input: focus */
  143. this.$container.on('focus.input', 'input', $.proxy(this._focus, this))
  144. /**
  145. * Input: mouseup
  146. * For input select() event to function correctly
  147. */
  148. this.$container.on('mouseup.input', 'input', function(e){
  149. e.preventDefault()
  150. })
  151. /* Input: blur */
  152. //20180803 修改
  153. this.$container.on('blur.input', 'input', $.proxy(this._blur, this))
  154. /* Select: change */
  155. this.$el.on('change.select', $.proxy(this._change, this))
  156. /* Select: focus */
  157. this.$el.on('focus.select', $.proxy(this._focus, this))
  158. /* Select: blur */
  159. this.$el.on('blur.select', $.proxy(this._blurSelect, this))
  160. /* Dropdown Arrow: click */
  161. this.$container.on('click.arrow', '.'+this.settings.comboArrowClass , $.proxy(this._toggle, this))
  162. /* Dropdown: close */
  163. this.$container.on('comboselect:close', $.proxy(this._close, this))
  164. /* Dropdown: open */
  165. this.$container.on('comboselect:open', $.proxy(this._open, this))
  166. /* HTML Click */
  167. $('html').off('click.comboselect').on('click.comboselect', function(){
  168. $.each($.fn[ pluginName ].instances, function(i, plugin){
  169. plugin.$container.trigger('comboselect:close')
  170. })
  171. });
  172. /* Stop `event:click` bubbling */
  173. this.$container.on('click.comboselect', function(e){
  174. e.stopPropagation();
  175. })
  176. /* Input: keydown */
  177. this.$container.on('keydown', 'input', $.proxy(this._keydown, this))
  178. /* Input: keyup */
  179. this.$container.on('keyup', 'input', $.proxy(this._keyup, this))
  180. /* Dropdown item: click */
  181. this.$container.on('click.item', '.option-item', $.proxy(this._select, this))
  182. },
  183. _keydown: function(event){
  184. switch(event.which){
  185. case keys.UP:
  186. this._move('up', event)
  187. break;
  188. case keys.DOWN:
  189. this._move('down', event)
  190. break;
  191. case keys.TAB:
  192. this._enter(event)
  193. break;
  194. case keys.RIGHT:
  195. this._autofill(event);
  196. break;
  197. case keys.ENTER:
  198. this._enter(event);
  199. break;
  200. default:
  201. break;
  202. }
  203. },
  204. _keyup: function(event){
  205. switch(event.which){
  206. case keys.ESC:
  207. this.$container.trigger('comboselect:close')
  208. break;
  209. case keys.ENTER:
  210. case keys.UP:
  211. case keys.DOWN:
  212. case keys.LEFT:
  213. case keys.RIGHT:
  214. case keys.TAB:
  215. case keys.SHIFT:
  216. break;
  217. default:
  218. this._filter(event.target.value)
  219. break;
  220. }
  221. },
  222. _enter: function(event){
  223. var item = this._getHovered()
  224. item.length && this._select(item);
  225. /* Check if it enter key */
  226. if(event && event.which == keys.ENTER){
  227. if(!item.length) {
  228. /* Check if its illegal value */
  229. this._blur();
  230. return true;
  231. }
  232. event.preventDefault();
  233. }
  234. },
  235. _move: function(dir){
  236. var items = this._getVisible(),
  237. current = this._getHovered(),
  238. index = current.prevAll('.option-item').filter(':visible').length,
  239. total = items.length
  240. switch(dir){
  241. case 'up':
  242. index--;
  243. (index < 0) && (index = (total - 1));
  244. break;
  245. case 'down':
  246. index++;
  247. (index >= total) && (index = 0);
  248. break;
  249. }
  250. items
  251. .removeClass(this.settings.hoverClass)
  252. .eq(index)
  253. .addClass(this.settings.hoverClass)
  254. if(!this.opened) this.$container.trigger('comboselect:open');
  255. this._fixScroll()
  256. },
  257. _select: function(event){
  258. var item = event.currentTarget? $(event.currentTarget) : $(event);
  259. if(!item.length) return;
  260. /**
  261. * 1. get Index
  262. */
  263. var index = item.data('index');
  264. this._selectByIndex(index);
  265. this.$container.trigger('comboselect:close')
  266. },
  267. _selectByIndex: function(index){
  268. /**
  269. * Set selected index and trigger change
  270. * @type {[type]}
  271. */
  272. if(typeof index == 'undefined'){
  273. index = -1
  274. }
  275. if(this.$el.prop('selectedIndex') != index){
  276. this.$el.prop('selectedIndex', index).trigger('change');
  277. }
  278. },
  279. _autofill: function(){
  280. var item = this._getHovered();
  281. if(item.length){
  282. var index = item.data('index')
  283. this._selectByIndex(index)
  284. }
  285. },
  286. _filter: function(search){
  287. var self = this,
  288. items = this._getAll();
  289. needle = $.trim(search).toLowerCase(),
  290. reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g'),
  291. pattern = '(' + search.replace(reEscape, '\\$1') + ')';
  292. /**
  293. * Unwrap all markers
  294. */
  295. $('.'+self.settings.markerClass, items).contents().unwrap();
  296. /* Search */
  297. if(needle){
  298. /* Hide Disabled and optgroups */
  299. this.$items.filter('.option-group, .option-disabled').hide();
  300. var thatThis=this;
  301. items
  302. .hide()
  303. .filter(function(){
  304. var $this = $(this),
  305. text = $.trim($this.text()).toLowerCase();
  306. // console.log($(this));
  307. /* Found */
  308. if(text.toString().indexOf(needle) != -1){
  309. /**
  310. * Wrap the selection
  311. */
  312. $this
  313. .html(function(index, oldhtml){
  314. return oldhtml.replace(new RegExp(pattern, 'gi'), '<span class="'+self.settings.markerClass+'">$1</span>')
  315. })
  316. return true
  317. }
  318. })
  319. .show()
  320. }else{
  321. this.$items.show();
  322. }
  323. /* Open the comboselect */
  324. this.$container.trigger('comboselect:open')
  325. },
  326. _highlight: function(){
  327. /*
  328. 1. Check if there is a selected item
  329. 2. Add hover class to it
  330. 3. If not add hover class to first item
  331. */
  332. var visible = this._getVisible().removeClass(this.settings.hoverClass),
  333. $selected = visible.filter('.'+this.settings.selectedClass)
  334. if($selected.length){
  335. $selected.addClass(this.settings.hoverClass);
  336. }else{
  337. visible
  338. .removeClass(this.settings.hoverClass)
  339. .first()
  340. .addClass(this.settings.hoverClass)
  341. }
  342. },
  343. _updateInput: function(){
  344. var selected = this.$el.prop('selectedIndex')
  345. // console.log(this)
  346. if(this.$el.val()){
  347. text = this.$el.find('option').eq(selected).text()
  348. this.$input.val(text)
  349. }else if(selected=='0'){
  350. this.$input.val('');
  351. this.$input.attr('placeholder',this.$el.find('option').eq(selected).text())
  352. }
  353. return this._getAll()
  354. .removeClass(this.settings.selectedClass)
  355. .filter(function(){
  356. return $(this).data('index') == selected
  357. })
  358. .addClass(this.settings.selectedClass)
  359. },
  360. _blurSelect: function(){
  361. this.$container.removeClass('combo-focus');
  362. },
  363. _focus: function(event){
  364. /* Toggle focus class */
  365. this.$container.toggleClass('combo-focus', !this.opened);
  366. /* If mobile: stop */
  367. if(isMobile) return;
  368. /* Open combo */
  369. if(!this.opened) this.$container.trigger('comboselect:open');
  370. /* Select the input */
  371. // this.settings.focusInput && event && event.currentTarget && event.currentTarget.nodeName == 'INPUT' && event.currentTarget.select()
  372. this.settings.focusInput && event && event.currentTarget && event.currentTarget.nodeName == 'INPUT'
  373. },
  374. _blur: function(){
  375. /**
  376. * 1. Get hovered item
  377. * 2. If not check if input value == select option
  378. * 3. If none
  379. */
  380. var val = $.trim(this.$input.val().toLowerCase()),
  381. isNumber = !isNaN(val);
  382. // console.log(val) ;
  383. var index = this.$options.filter(function(){
  384. if(isNumber){
  385. return parseInt($.trim(this.innerHTML).toLowerCase()) == val
  386. }
  387. return $.trim(this.innerHTML).toLowerCase() == val
  388. }).prop('index');
  389. // console.log(index);
  390. // if(index){
  391. /* Select by Index */
  392. this._selectByIndex(index)
  393. // }
  394. },
  395. _change: function(){
  396. this._updateInput();
  397. },
  398. _getAll: function(){
  399. return this.$items.filter('.option-item')
  400. },
  401. _getVisible: function(){
  402. return this.$items.filter('.option-item').filter(':visible')
  403. },
  404. _getHovered: function(){
  405. return this._getVisible().filter('.' + this.settings.hoverClass);
  406. },
  407. _open: function(){
  408. var self = this
  409. this.$container.addClass('combo-open')
  410. this.opened = true
  411. /* Focus input field */
  412. // this.settings.focusInput && setTimeout(function(){ !self.$input.is(':focus') && self.$input.focus(); });
  413. /* Highligh the items */
  414. this._highlight()
  415. /* Fix scroll */
  416. this._fixScroll()
  417. /* Close all others */
  418. $.each($.fn[ pluginName ].instances, function(i, plugin){
  419. if(plugin != self && plugin.opened) plugin.$container.trigger('comboselect:close')
  420. })
  421. },
  422. _toggle: function(){
  423. this.opened? this._close.call(this) : this._open.call(this)
  424. },
  425. _close: function(){
  426. this.$container.removeClass('combo-open combo-focus')
  427. this.$container.trigger('comboselect:closed')
  428. this.opened = false
  429. /* Show all items */
  430. this.$items.show();
  431. },
  432. _fixScroll: function(){
  433. /**
  434. * If dropdown is hidden
  435. */
  436. if(this.$dropdown.is(':hidden')) return;
  437. /**
  438. * Else
  439. */
  440. var item = this._getHovered();
  441. if(!item.length) return;
  442. /**
  443. * Scroll
  444. */
  445. var offsetTop,
  446. upperBound,
  447. lowerBound,
  448. heightDelta = item.outerHeight()
  449. offsetTop = item[0].offsetTop;
  450. upperBound = this.$dropdown.scrollTop();
  451. lowerBound = upperBound + this.settings.maxHeight - heightDelta;
  452. if (offsetTop < upperBound) {
  453. this.$dropdown.scrollTop(offsetTop);
  454. } else if (offsetTop > lowerBound) {
  455. this.$dropdown.scrollTop(offsetTop - this.settings.maxHeight + heightDelta);
  456. }
  457. },
  458. /**
  459. * Destroy API
  460. */
  461. dispose: function(){
  462. /* Remove combo arrow, input, dropdown */
  463. this.$arrow.remove()
  464. this.$input.remove()
  465. this.$dropdown.remove()
  466. /* Remove tabindex property */
  467. this.$el
  468. .removeAttr("tabindex")
  469. /* Check if there is a tabindex set before */
  470. if(!!this.$el.data('plugin_'+ dataKey + '_tabindex')){
  471. this.$el.prop('tabindex', this.$el.data('plugin_'+ dataKey + '_tabindex'))
  472. }
  473. /* Unwrap */
  474. this.$el.unwrap()
  475. /* Remove data */
  476. this.$el.removeData('plugin_'+dataKey)
  477. /* Remove tabindex data */
  478. this.$el.removeData('plugin_'+dataKey + '_tabindex')
  479. /* Remove change event on select */
  480. this.$el.off('change.select focus.select blur.select');
  481. }
  482. });
  483. // A really lightweight plugin wrapper around the constructor,
  484. // preventing against multiple instantiations
  485. $.fn[ pluginName ] = function ( options, args ) {
  486. this.each(function() {
  487. var $e = $(this),
  488. instance = $e.data('plugin_'+dataKey)
  489. if (typeof options === 'string') {
  490. if (instance && typeof instance[options] === 'function') {
  491. instance[options](args);
  492. }
  493. }else{
  494. if (instance && instance.dispose) {
  495. instance.dispose();
  496. }
  497. $.data( this, "plugin_" + dataKey, new Plugin( this, options ) );
  498. }
  499. });
  500. // chain jQuery functions
  501. return this;
  502. };
  503. $.fn[ pluginName ].instances = [];
  504. }));