安图前端代码

jquery.combo.select.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  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 + '" 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. // this.$container.on('blur.input', 'input', $.proxy(this._blur, this))
  153. /* Select: change */
  154. this.$el.on('change.select', $.proxy(this._change, this))
  155. /* Select: focus */
  156. this.$el.on('focus.select', $.proxy(this._focus, this))
  157. /* Select: blur */
  158. this.$el.on('blur.select', $.proxy(this._blurSelect, this))
  159. /* Dropdown Arrow: click */
  160. this.$container.on('click.arrow', '.'+this.settings.comboArrowClass , $.proxy(this._toggle, this))
  161. /* Dropdown: close */
  162. this.$container.on('comboselect:close', $.proxy(this._close, this))
  163. /* Dropdown: open */
  164. this.$container.on('comboselect:open', $.proxy(this._open, this))
  165. /* HTML Click */
  166. $('html').off('click.comboselect').on('click.comboselect', function(){
  167. $.each($.fn[ pluginName ].instances, function(i, plugin){
  168. plugin.$container.trigger('comboselect:close')
  169. })
  170. });
  171. /* Stop `event:click` bubbling */
  172. this.$container.on('click.comboselect', function(e){
  173. e.stopPropagation();
  174. })
  175. /* Input: keydown */
  176. this.$container.on('keydown', 'input', $.proxy(this._keydown, this))
  177. /* Input: keyup */
  178. this.$container.on('keyup', 'input', $.proxy(this._keyup, this))
  179. /* Dropdown item: click */
  180. this.$container.on('click.item', '.option-item', $.proxy(this._select, this))
  181. },
  182. _keydown: function(event){
  183. switch(event.which){
  184. case keys.UP:
  185. this._move('up', event)
  186. break;
  187. case keys.DOWN:
  188. this._move('down', event)
  189. break;
  190. case keys.TAB:
  191. this._enter(event)
  192. break;
  193. case keys.RIGHT:
  194. this._autofill(event);
  195. break;
  196. case keys.ENTER:
  197. this._enter(event);
  198. break;
  199. default:
  200. break;
  201. }
  202. },
  203. _keyup: function(event){
  204. switch(event.which){
  205. case keys.ESC:
  206. this.$container.trigger('comboselect:close')
  207. break;
  208. case keys.ENTER:
  209. case keys.UP:
  210. case keys.DOWN:
  211. case keys.LEFT:
  212. case keys.RIGHT:
  213. case keys.TAB:
  214. case keys.SHIFT:
  215. break;
  216. default:
  217. this._filter(event.target.value)
  218. break;
  219. }
  220. },
  221. _enter: function(event){
  222. var item = this._getHovered()
  223. item.length && this._select(item);
  224. /* Check if it enter key */
  225. if(event && event.which == keys.ENTER){
  226. if(!item.length) {
  227. /* Check if its illegal value */
  228. this._blur();
  229. return true;
  230. }
  231. event.preventDefault();
  232. }
  233. },
  234. _move: function(dir){
  235. var items = this._getVisible(),
  236. current = this._getHovered(),
  237. index = current.prevAll('.option-item').filter(':visible').length,
  238. total = items.length
  239. switch(dir){
  240. case 'up':
  241. index--;
  242. (index < 0) && (index = (total - 1));
  243. break;
  244. case 'down':
  245. index++;
  246. (index >= total) && (index = 0);
  247. break;
  248. }
  249. items
  250. .removeClass(this.settings.hoverClass)
  251. .eq(index)
  252. .addClass(this.settings.hoverClass)
  253. if(!this.opened) this.$container.trigger('comboselect:open');
  254. this._fixScroll()
  255. },
  256. _select: function(event){
  257. var item = event.currentTarget? $(event.currentTarget) : $(event);
  258. if(!item.length) return;
  259. /**
  260. * 1. get Index
  261. */
  262. var index = item.data('index');
  263. this._selectByIndex(index);
  264. this.$container.trigger('comboselect:close')
  265. },
  266. _selectByIndex: function(index){
  267. /**
  268. * Set selected index and trigger change
  269. * @type {[type]}
  270. */
  271. if(typeof index == 'undefined'){
  272. index = 0
  273. }
  274. if(this.$el.prop('selectedIndex') != index){
  275. this.$el.prop('selectedIndex', index).trigger('change');
  276. }
  277. },
  278. _autofill: function(){
  279. var item = this._getHovered();
  280. if(item.length){
  281. var index = item.data('index')
  282. this._selectByIndex(index)
  283. }
  284. },
  285. _filter: function(search){
  286. var self = this,
  287. items = this._getAll();
  288. needle = $.trim(search).toLowerCase(),
  289. reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g'),
  290. pattern = '(' + search.replace(reEscape, '\\$1') + ')';
  291. /**
  292. * Unwrap all markers
  293. */
  294. $('.'+self.settings.markerClass, items).contents().unwrap();
  295. /* Search */
  296. if(needle){
  297. /* Hide Disabled and optgroups */
  298. this.$items.filter('.option-group, .option-disabled').hide();
  299. var thatThis=this;
  300. items
  301. .hide()
  302. .filter(function(){
  303. var $this = $(this),
  304. text = $.trim($this.text()).toLowerCase();
  305. // console.log($(this));
  306. /* Found */
  307. if(text.toString().indexOf(needle) != -1){
  308. /**
  309. * Wrap the selection
  310. */
  311. $this
  312. .html(function(index, oldhtml){
  313. return oldhtml.replace(new RegExp(pattern, 'gi'), '<span class="'+self.settings.markerClass+'">$1</span>')
  314. })
  315. return true
  316. }
  317. })
  318. .show()
  319. }else{
  320. this.$items.show();
  321. }
  322. /* Open the comboselect */
  323. this.$container.trigger('comboselect:open')
  324. },
  325. _highlight: function(){
  326. /*
  327. 1. Check if there is a selected item
  328. 2. Add hover class to it
  329. 3. If not add hover class to first item
  330. */
  331. var visible = this._getVisible().removeClass(this.settings.hoverClass),
  332. $selected = visible.filter('.'+this.settings.selectedClass)
  333. if($selected.length){
  334. $selected.addClass(this.settings.hoverClass);
  335. }else{
  336. visible
  337. .removeClass(this.settings.hoverClass)
  338. .first()
  339. .addClass(this.settings.hoverClass)
  340. }
  341. },
  342. _updateInput: function(){
  343. var selected = this.$el.prop('selectedIndex')
  344. // console.log(this)
  345. if(this.$el.val()){
  346. text = this.$el.find('option').eq(selected).text()
  347. this.$input.val(text)
  348. }else{
  349. this.$input.val('')
  350. // this.$input.val(this.$el.find('option').eq(selected).text())
  351. }
  352. return this._getAll()
  353. .removeClass(this.settings.selectedClass)
  354. .filter(function(){
  355. return $(this).data('index') == selected
  356. })
  357. .addClass(this.settings.selectedClass)
  358. },
  359. _blurSelect: function(){
  360. this.$container.removeClass('combo-focus');
  361. },
  362. _focus: function(event){
  363. /* Toggle focus class */
  364. this.$container.toggleClass('combo-focus', !this.opened);
  365. /* If mobile: stop */
  366. if(isMobile) return;
  367. /* Open combo */
  368. if(!this.opened) this.$container.trigger('comboselect:open');
  369. /* Select the input */
  370. this.settings.focusInput && event && event.currentTarget && event.currentTarget.nodeName == 'INPUT' && event.currentTarget.select()
  371. },
  372. _blur: function(){
  373. /**
  374. * 1. Get hovered item
  375. * 2. If not check if input value == select option
  376. * 3. If none
  377. */
  378. var val = $.trim(this.$input.val().toLowerCase()),
  379. isNumber = !isNaN(val);
  380. var index = this.$options.filter(function(){
  381. if(isNumber){
  382. return parseInt($.trim(this.innerHTML).toLowerCase()) == val
  383. }
  384. return $.trim(this.innerHTML).toLowerCase() == val
  385. }).prop('index')
  386. /* Select by Index */
  387. this._selectByIndex(index)
  388. },
  389. _change: function(){
  390. this._updateInput();
  391. },
  392. _getAll: function(){
  393. return this.$items.filter('.option-item')
  394. },
  395. _getVisible: function(){
  396. return this.$items.filter('.option-item').filter(':visible')
  397. },
  398. _getHovered: function(){
  399. return this._getVisible().filter('.' + this.settings.hoverClass);
  400. },
  401. _open: function(){
  402. var self = this
  403. this.$container.addClass('combo-open')
  404. this.opened = true
  405. /* Focus input field */
  406. this.settings.focusInput && setTimeout(function(){ !self.$input.is(':focus') && self.$input.focus(); });
  407. /* Highligh the items */
  408. this._highlight()
  409. /* Fix scroll */
  410. this._fixScroll()
  411. /* Close all others */
  412. $.each($.fn[ pluginName ].instances, function(i, plugin){
  413. if(plugin != self && plugin.opened) plugin.$container.trigger('comboselect:close')
  414. })
  415. },
  416. _toggle: function(){
  417. this.opened? this._close.call(this) : this._open.call(this)
  418. },
  419. _close: function(){
  420. this.$container.removeClass('combo-open combo-focus')
  421. this.$container.trigger('comboselect:closed')
  422. this.opened = false
  423. /* Show all items */
  424. this.$items.show();
  425. },
  426. _fixScroll: function(){
  427. /**
  428. * If dropdown is hidden
  429. */
  430. if(this.$dropdown.is(':hidden')) return;
  431. /**
  432. * Else
  433. */
  434. var item = this._getHovered();
  435. if(!item.length) return;
  436. /**
  437. * Scroll
  438. */
  439. var offsetTop,
  440. upperBound,
  441. lowerBound,
  442. heightDelta = item.outerHeight()
  443. offsetTop = item[0].offsetTop;
  444. upperBound = this.$dropdown.scrollTop();
  445. lowerBound = upperBound + this.settings.maxHeight - heightDelta;
  446. if (offsetTop < upperBound) {
  447. this.$dropdown.scrollTop(offsetTop);
  448. } else if (offsetTop > lowerBound) {
  449. this.$dropdown.scrollTop(offsetTop - this.settings.maxHeight + heightDelta);
  450. }
  451. },
  452. /**
  453. * Destroy API
  454. */
  455. dispose: function(){
  456. /* Remove combo arrow, input, dropdown */
  457. this.$arrow.remove()
  458. this.$input.remove()
  459. this.$dropdown.remove()
  460. /* Remove tabindex property */
  461. this.$el
  462. .removeAttr("tabindex")
  463. /* Check if there is a tabindex set before */
  464. if(!!this.$el.data('plugin_'+ dataKey + '_tabindex')){
  465. this.$el.prop('tabindex', this.$el.data('plugin_'+ dataKey + '_tabindex'))
  466. }
  467. /* Unwrap */
  468. this.$el.unwrap()
  469. /* Remove data */
  470. this.$el.removeData('plugin_'+dataKey)
  471. /* Remove tabindex data */
  472. this.$el.removeData('plugin_'+dataKey + '_tabindex')
  473. /* Remove change event on select */
  474. this.$el.off('change.select focus.select blur.select');
  475. }
  476. });
  477. // A really lightweight plugin wrapper around the constructor,
  478. // preventing against multiple instantiations
  479. $.fn[ pluginName ] = function ( options, args ) {
  480. this.each(function() {
  481. var $e = $(this),
  482. instance = $e.data('plugin_'+dataKey)
  483. if (typeof options === 'string') {
  484. if (instance && typeof instance[options] === 'function') {
  485. instance[options](args);
  486. }
  487. }else{
  488. if (instance && instance.dispose) {
  489. instance.dispose();
  490. }
  491. $.data( this, "plugin_" + dataKey, new Plugin( this, options ) );
  492. }
  493. });
  494. // chain jQuery functions
  495. return this;
  496. };
  497. $.fn[ pluginName ].instances = [];
  498. }));