PageSourceSearch

https://inegi.org.mx/componentes/biinegi/cmbSelect/plugin/jquery.combo.select.js

js inegi.org.mx collected 2026-10-02 09:48:45 UTC 15,736 bytes, 797 lines download raw bytes

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// Expose plugin as an AMD module if AMD loader is present:
11(function (factory) {
12	'use strict';
13	if (typeof define === 'function' && define.amd) {
14		// AMD. Register as an anonymous module.
15		define(['jquery'], factory);
16	} else if (typeof exports === 'object' && typeof require === 'function') {
17		// Browserify
18		factory(require('jquery'));
19	} else {
20		// Browser globals
21		factory(jQuery);
22	}
23}(function ( $, undefined ) {
24
25	var pluginName = "comboSelect",
26		dataKey = 'comboselect';
27	var defaults = {			
28		comboClass         : 'combo-select',
29		comboArrowClass    : 'combo-arrow',
30		comboDropDownClass : 'combo-dropdown',
31		inputClass         : 'combo-input text-input',
32		disabledClass      : 'option-disabled',
33		hoverClass         : 'option-hover',
34		selectedClass      : 'option-selected',
35		markerClass        : 'combo-marker',
36		themeClass         : '',
37		maxHeight          : 200,
38		extendStyle        : true,
39		focusInput         : true
40	};
41
42	/**
43	 * Utility functions
44	 */
45
46	var keys = {
47		ESC: 27,
48		TAB: 9,
49		RETURN: 13,
50		LEFT: 37,
51		UP: 38,
52		RIGHT: 39,
53		DOWN: 40,
54		ENTER: 13,
55		SHIFT: 16
56	},	
57	isMobile = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()));
58
59	/**
60	 * Constructor
61	 * @param {[Node]} element [Select element]
62	 * @param {[Object]} options [Option object]
63	 */
64	function Plugin ( element, options ) {
65			
66		/* Name of the plugin */
67
68		this._name = pluginName;
69
70		/* Reverse lookup */
71
72		this.el = element
73
74		/* Element */
75
76		this.$el = $(element)
77
78		/* If multiple select: stop */
79		
80		if(this.$el.prop('multiple')) return;
81
82		/* Settings */
83
84		this.settings = $.extend( {}, defaults, options, this.$el.data() );
85
86		/* Defaults */
87
88		this._defaults = defaults;
89
90		/* Options */
91
92		this.$options = this.$el.find('option, optgroup')
93
94		/* Initialize */
95
96		this.init();
97
98		/* Instances */
99
100		$.fn[ pluginName ].instances.push(this);
101
102	}
103
104	$.extend(Plugin.prototype, {
105		init: function () {
106
107			/* Construct the comboselect */
108
109			this._construct();
110
111
112			/* Add event bindings */          
113
114			this._events();
115
116
117		},
118		_construct: function(){
119
120			var self = this
121
122			/**
123			 * Add negative TabIndex to `select`
124			 * Preserves previous tabindex
125			 */
126			
127			this.$el.data('plugin_'+ dataKey + '_tabindex', this.$el.prop('tabindex'))
128
129			/* Add a tab index for desktop browsers */
130
131			!isMobile && this.$el.prop("tabIndex", -1)
132
133			/**
134			 * Wrap the Select
135			 */
136
137			this.$container = this.$el.wrapAll('<div class="' + this.settings.comboClass + ' '+ this.settings.themeClass + '" />').parent();
138			
139			/**
140			 * Check if select has a width attribute
141			 */
142			if(this.settings.extendStyle && this.$el.attr('style')){
143
144				this.$container.attr('style', this.$el.attr("style"))
145				
146			}
147			
148
149			/**
150			 * Append dropdown arrow
151			 */
152
153			this.$arrow = $('<div class="'+ this.settings.comboArrowClass+ '" />').appendTo(this.$container)
154
155
156			/**
157			 * Append dropdown
158			 */
159
160			this.$dropdown = $('<ul class="'+this.settings.comboDropDownClass+'" />').appendTo(this.$container)
161
162
163			/**
164			 * Create dropdown options
165			 */
166
167			var o = '', k = 0, p = '';
168
169			this.selectedIndex = this.$el.prop('selectedIndex')
170
171			this.$options.each(function(i, e){
172
173				if(e.nodeName.toLowerCase() == 'optgroup'){
174
175					return o+='<li class="option-group">'+this.label+'</li>'
176				}
177
178				if(!e.value) p = e.innerHTML
179
180				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>'
181
182				k++;
183			})
184
185			this.$dropdown.html(o)
186
187			/**
188			 * Items
189			 */
190
191			this.$items = this.$dropdown.children();
192
193
194			/**
195			 * Append Input
196			 */
197
198			this.$input = $('<input type="text"' + (isMobile? 'tabindex="-1"': '') + ' placeholder="'+p+'" class="'+ this.settings.inputClass + '">').appendTo(this.$container)
199
200			/* Update input text */
201
202			this._updateInput()
203
204		},
205
206		_events: function(){
207
208			/* Input: focus */
209
210			this.$container.on('focus.input', 'input', $.proxy(this._focus, this))
211
212			/**
213			 * Input: mouseup
214			 * For input select() event to function correctly
215			 */
216			this.$container.on('mouseup.input', 'input', function(e){
217				e.preventDefault()
218			})
219
220			/* Input: blur */
221
222			this.$container.on('blur.input', 'input', $.proxy(this._blur, this))
223
224			/* Select: change */
225
226			this.$el.on('change.select', $.proxy(this._change, this))
227
228			/* Select: focus */
229
230			this.$el.on('focus.select', $.proxy(this._focus, this))
231
232			/* Select: blur */
233
234			this.$el.on('blur.select', $.proxy(this._blurSelect, this))
235
236			/* Dropdown Arrow: click */
237
238			this.$container.on('click.arrow', '.'+this.settings.comboArrowClass , $.proxy(this._toggle, this))
239
240			/* Dropdown: close */
241
242			this.$container.on('comboselect:close', $.proxy(this._close, this))
243
244			/* Dropdown: open */
245
246			this.$container.on('comboselect:open', $.proxy(this._open, this))
247
248
249			/* HTML Click */
250
251			$('html').off('click.comboselect').on('click.comboselect', function(){
252				
253				$.each($.fn[ pluginName ].instances, function(i, plugin){
254
255					plugin.$container.trigger('comboselect:close')
256
257				})
258			});
259
260			/* Stop `event:click` bubbling */
261
262			this.$container.on('click.comboselect', function(e){
263				e.stopPropagation();
264			})
265
266
267			/* Input: keydown */
268
269			this.$container.on('keydown', 'input', $.proxy(this._keydown, this))
270
271			/* Input: keyup */
272			
273			this.$container.on('keyup', 'input', $.proxy(this._keyup, this))
274
275			/* Dropdown item: click */
276
277			this.$container.on('click.item', '.option-item', $.proxy(this._select, this))
278
279		},
280
281		_keydown: function(event){
282
283			
284
285			switch(event.which){
286
287				case keys.UP:
288					this._move('up', event)
289					break;
290
291				case keys.DOWN:
292					this._move('down', event)
293					break;
294				
295				case keys.TAB:
296					this._enter(event)
297					break;
298
299				case keys.RIGHT:
300					this._autofill(event);
301					break;
302
303				case keys.ENTER:
304					this._enter(event);
305					break;
306
307				default:							
308					break;
309
310
311			}
312
313		},
314		
315
316		_keyup: function(event){
317			
318			switch(event.which){
319				case keys.ESC:													
320					this.$container.trigger('comboselect:close')
321					break;
322
323				case keys.ENTER:
324				case keys.UP:
325				case keys.DOWN:
326				case keys.LEFT:
327				case keys.RIGHT:
328				case keys.TAB:
329				case keys.SHIFT:							
330					break;
331				
332				default:							
333					this._filter(event.target.value)
334					break;
335			}
336		},
337		
338		_enter: function(event){
339
340			var item = this._getHovered()
341
342			item.length && this._select(item);
343
344			/* Check if it enter key */
345			if(event && event.which == keys.ENTER){
346
347				if(!item.length) {
348					
349					/* Check if its illegal value */
350
351					this._blur();
352
353					return true;
354				}
355
356				event.preventDefault();
357			}
358			
359
360		},
361		_move: function(dir){
362
363			var items = this._getVisible(),
364				current = this._getHovered(),
365				index = current.prevAll('.option-item').filter(':visible').length,
366				total = items.length
367
368			
369			switch(dir){
370				case 'up':
371					index--;
372					(index < 0) && (index = (total - 1));
373					break;
374
375				case 'down':							
376					index++;
377					(index >= total) && (index = 0);							
378					break;
379			}
380
381			
382			items
383				.removeClass(this.settings.hoverClass)
384				.eq(index)
385				.addClass(this.settings.hoverClass)
386
387
388			if(!this.opened) this.$container.trigger('comboselect:open');
389
390			this._fixScroll()
391		},
392
393		_select: function(event){
394
395			var item = event.currentTarget? $(event.currentTarget) : $(event);
396
397			if(!item.length) return;
398
399			/**
400			 * 1. get Index
401			 */
402			
403			var index = item.data('index');
404
405			this._selectByIndex(index);
406
407			this.$container.trigger('comboselect:close')					
408
409		},
410
411		_selectByIndex: function(index){
412
413			/**
414			 * Set selected index and trigger change
415			 * @type {[type]}
416			 */
417			if(typeof index == 'undefined'){
418				
419				index = 0
420
421			}
422			
423			if(this.$el.prop('selectedIndex') != index){
424
425				this.$el.prop('selectedIndex', index).trigger('change');
426			}
427
428		},
429
430		_autofill: function(){
431
432			var item = this._getHovered();
433
434			if(item.length){
435
436				var index = item.data('index')
437
438				this._selectByIndex(index)
439
440			}
441
442		},
443		
444
445		_filter: function(search){
446
447			var self = this,
448				items = this._getAll();
449				needle = $.trim(search).toLowerCase(),
450				reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g'),
451				pattern = '(' + search.replace(reEscape, '\\$1') + ')';
452
453
454			/**
455			 * Unwrap all markers
456			 */
457			
458			$('.'+self.settings.markerClass, items).contents().unwrap();
459
460			/* Search */
461			
462			if(needle){
463
464				/* Hide Disabled and optgroups */
465
466				this.$items.filter('.option-group, .option-disabled').hide();
467			
468				
469				items							
470					.hide()
471					.filter(function(){
472
473						var $this = $(this),
474							text = $.trim($this.text()).toLowerCase();
475						
476						/* Found */
477						if(text.toString().indexOf(needle) != -1){
478																
479							/**
480							 * Wrap the selection
481							 */									
482							
483							$this
484								.html(function(index, oldhtml){
485								return oldhtml.replace(new RegExp(pattern, 'gi'), '<span class="'+self.settings.markerClass+'">$1</span>')
486							})									
487
488							return true
489						}
490
491					})
492					.show()
493			}else{
494
495								
496				this.$items.show();
497			}
498
499			/* Open the comboselect */
500
501			this.$container.trigger('comboselect:open')
502			
503
504		},
505
506		_highlight: function(){
507
508			/* 
509			1. Check if there is a selected item 
510			2. Add hover class to it
511			3. If not add hover class to first item
512			*/
513		
514			var visible = this._getVisible().removeClass(this.settings.hoverClass),
515				$selected = visible.filter('.'+this.settings.selectedClass)
516
517			if($selected.length){
518				
519				$selected.addClass(this.settings.hoverClass);
520
521			}else{
522
523				visible
524					.removeClass(this.settings.hoverClass)
525					.first()
526					.addClass(this.settings.hoverClass)
527			}
528
529		},
530
531		_updateInput: function(){
532
533			var selected = this.$el.prop('selectedIndex')
534			
535			if(this.$el.val()){
536				
537				text = this.$el.find('option').eq(selected).text()
538
539				this.$input.val(text)
540
541			}else{
542				
543				this.$input.val('')
544
545			}
546
547			return this._getAll()
548				.removeClass(this.settings.selectedClass)
549				.filter(function(){
550
551					return $(this).data('index') == selected
552				})
553				.addClass(this.settings.selectedClass)
554		
555		},
556		_blurSelect: function(){
557
558			this.$container.removeClass('combo-focus');
559
560		},
561		_focus: function(event){
562			
563			/* Toggle focus class */
564
565			this.$container.toggleClass('combo-focus', !this.opened);
566
567			/* If mobile: stop */
568			
569			if(isMobile) return;
570
571			/* Open combo */
572
573			if(!this.opened) this.$container.trigger('comboselect:open');
574			
575			/* Select the input */
576			
577			this.settings.focusInput && event && event.currentTarget && event.currentTarget.nodeName == 'INPUT' && event.currentTarget.select()
578		},
579
580		_blur: function(){
581
582			/**
583			 * 1. Get hovered item
584			 * 2. If not check if input value == select option
585			 * 3. If none
586			 */
587			
588			var val = $.trim(this.$input.val().toLowerCase()),
589				isNumber = !isNaN(val);
590			
591			var index = this.$options.filter(function(){
592				
593				if(isNumber){
594					return parseInt($.trim(this.innerHTML).toLowerCase()) == val
595				}
596
597				return $.trim(this.innerHTML).toLowerCase() == val
598
599			}).prop('index')
600		
601			/* Select by Index */
602						
603			this._selectByIndex(index)
604			
605		},
606
607		_change: function(){
608
609
610			this._updateInput();
611
612		},
613
614		_getAll: function(){
615
616			return this.$items.filter('.option-item')
617
618		},
619		_getVisible: function(){
620
621			return this.$items.filter('.option-item').filter(':visible')
622
623		},
624
625		_getHovered: function(){
626
627			return this._getVisible().filter('.' + this.settings.hoverClass);
628
629		},
630
631		_open: function(){
632
633			var self = this
634
635			this.$container.addClass('combo-open')			
636
637			this.opened = true
638			
639			/* Focus input field */			
640
641			this.settings.focusInput && setTimeout(function(){ !self.$input.is(':focus') && self.$input.focus(); });
642
643			/* Highligh the items */
644
645			this._highlight()
646
647			/* Fix scroll */
648
649			this._fixScroll()
650
651			/* Close all others */
652
653
654			$.each($.fn[ pluginName ].instances, function(i, plugin){
655
656				if(plugin != self && plugin.opened) plugin.$container.trigger('comboselect:close')
657			})
658
659		},
660
661		_toggle: function(){
662
663			this.opened? this._close.call(this) : this._open.call(this)
664		},
665
666		_close: function(){				
667
668			this.$container.removeClass('combo-open combo-focus')
669
670			this.$container.trigger('comboselect:closed')
671
672			this.opened = false
673
674			/* Show all items */
675
676			this.$items.show();
677
678		},
679		_fixScroll: function(){
680	
681			/**
682			 * If dropdown is hidden
683			 */
684			if(this.$dropdown.is(':hidden')) return;
685
686			
687			/**
688			 * Else					 
689			 */
690			var item = this._getHovered();
691
692			if(!item.length) return;					
693
694			/**
695			 * Scroll
696			 */
697			
698			var offsetTop,
699				upperBound,
700				lowerBound,
701				heightDelta = item.outerHeight()
702
703			offsetTop = item[0].offsetTop;
704			
705			upperBound = this.$dropdown.scrollTop();
706
707			lowerBound = upperBound + this.settings.maxHeight - heightDelta;
708			
709			if (offsetTop < upperBound) {
710					
711				this.$dropdown.scrollTop(offsetTop);
712
713			} else if (offsetTop > lowerBound) {
714					
715				this.$dropdown.scrollTop(offsetTop - this.settings.maxHeight + heightDelta);
716			}
717
718		},
719		/**
720		 * Destroy API
721		 */
722		
723		dispose: function(){
724
725			/* Remove combo arrow, input, dropdown */
726
727			this.$arrow.remove()
728
729			this.$input.remove()
730
731			this.$dropdown.remove()
732
733			/* Remove tabindex property */
734			this.$el
735				.removeAttr("tabindex")
736
737			/* Check if there is a tabindex set before */
738
739			if(!!this.$el.data('plugin_'+ dataKey + '_tabindex')){
740				this.$el.prop('tabindex', this.$el.data('plugin_'+ dataKey + '_tabindex'))
741			}
742
743			/* Unwrap */
744
745			this.$el.unwrap()
746
747			/* Remove data */
748
749			this.$el.removeData('plugin_'+dataKey)
750
751			/* Remove tabindex data */
752
753			this.$el.removeData('plugin_'+dataKey + '_tabindex')
754
755			/* Remove change event on select */
756
757			this.$el.off('change.select focus.select blur.select');
758
759		}	
760	});
761
762
763
764	// A really lightweight plugin wrapper around the constructor,
765	// preventing against multiple instantiations
766	$.fn[ pluginName ] = function ( options, args ) {
767
768		this.each(function() {
769
770			var $e = $(this),
771				instance = $e.data('plugin_'+dataKey)
772
773			if (typeof options === 'string') {
774				
775				if (instance && typeof instance[options] === 'function') {
776						instance[options](args);
777				}
778
779			}else{
780
781				if (instance && instance.dispose) {
782						instance.dispose();
783				}
784
785				$.data( this, "plugin_" + dataKey, new Plugin( this, options ) );
786
787			}
788
789		});
790
791		// chain jQuery functions
792		return this;
793	};
794
795	$.fn[ pluginName ].instances = [];
796
797}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.