Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

798 linhas
15 KiB

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