PageSourceSearch

https://www.mom.gov.sg/html/global/vendor/js/combo-select.js

js mom.gov.sg collected 2026-09-24 08:26:46 UTC 23,177 bytes, 871 lines download raw bytes

1/**
2 * Plugin Name: Combo Select
3 * Author : Vinay@Pebbleroad
4 * Date: 23/11/2014
5 * Description:https://github.com/PebbleRoad/combo-select/blob/master/jquery.combo.select.js
6 * Converts a select box into a searchable and keyboard friendly interface. Fallbacks to native select on mobile and tablets
7 */
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
24    var pluginName = "comboSelect",
25        dataKey = 'comboselect';
26    var defaults = {            
27        comboClass        : 'combo-select',
28        comboArrowClass   : 'combo-arrow',
29        comboDropDownClass: 'combo-dropdown',
30        inputClass        : 'combo-input text-input',
31        disabledClass     : 'option-disabled',
32        hoverClass        : 'option-hover',
33        selectedClass     : 'option-selected',
34        markerClass       : 'combo-marker',
35        themeClass        : '',
36        maxHeight         : 200,
37        extendStyle       : true,
38        focusInput        : true,
39        showLabel         : false
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            this._build();
168
169            /**
170             * Append Input
171             */
172
173            this.$input = $('<input type="text"' + (isMobile? 'tabindex="-1"': '') + ' placeholder="'+ this.getPlaceholder() +'" class="'+ this.settings.inputClass + '">').appendTo(this.$container)
174
175            /**
176             * Append Label
177             */
178            
179            if(this.settings.showLabel){
180
181                this.$container.addClass('combo-has-label');
182                
183                this.$label = $('<input />').attr({
184                    'class' : 'combo-label-input text-input',
185                    tabIndex: '-1'
186                })
187                .val(this.getPlaceholder())
188                .appendTo(this.$container)
189            }
190
191            /* Update input text */
192
193            this._updateInput()
194
195        },
196        getPlaceholder: function(){
197
198            var p = '';
199
200            this.$options.filter(function(idx, opt){
201                
202                return opt.nodeName == 'OPTION'
203            }).each(function(idx, e){
204                
205                if(e.value == '') p = e.innerHTML
206            });
207
208            return p
209        },
210        _build: function(){
211
212            var self = this;
213
214            var o = '', k = 0;
215
216            this.$options.each(function(i, e){
217
218                if(e.nodeName.toLowerCase() == 'optgroup'){
219
220                    return o+='<li class="option-group">'+this.label+'</li>'
221                }
222                
223                o+='<li class="'+(this.disabled? self.settings.disabledClass : "option-item") + ' ' +(k == self.$el.prop('selectedIndex')? self.settings.selectedClass : '')+ '" data-index="'+(k)+'" data-value="'+this.value+'">'+ (this.innerHTML) + '</li>'
224
225                k++;
226            })
227
228            this.$dropdown.html(o)
229
230            /**
231             * Items
232             */
233
234            this.$items = this.$dropdown.children();
235        },
236
237        _events: function(){
238
239            /* Input: focus */
240
241            this.$container.on('focus.input', 'input', $.proxy(this._focus, this))
242
243            /**
244             * Input: mouseup
245             * For input select() event to function correctly
246             */
247            this.$container.on('mouseup.input', 'input', function(e){
248                e.preventDefault()
249            })
250
251            /* Input: blur */
252
253            this.$container.on('blur.input', 'input', $.proxy(this._blur, this))
254
255            /* Select: change */
256
257            this.$el.on('change.select', $.proxy(this._change, this))
258
259            /* Select: focus */
260
261            this.$el.on('focus.select', $.proxy(this._focus, this))
262
263            /* Select: blur */
264
265            this.$el.on('blur.select', $.proxy(this._blurSelect, this))
266
267            /* Dropdown Arrow: click */
268
269            this.$container.on('click.arrow', '.'+this.settings.comboArrowClass , $.proxy(this._toggle, this))
270
271            /* Dropdown: close */
272
273            this.$container.on('comboselect:close', $.proxy(this._close, this))
274
275            /* Dropdown: open */
276
277            this.$container.on('comboselect:open', $.proxy(this._open, this))
278
279            /* Dropdown: update */
280
281            this.$container.on('comboselect:update', $.proxy(this._update, this));
282
283
284            /* HTML Click */
285
286            $('html').off('click.comboselect').on('click.comboselect', function(){
287                
288                $.each($.fn[ pluginName ].instances, function(i, plugin){
289
290                    plugin.$container.trigger('comboselect:close')
291
292                })
293            });
294
295            /* Stop `event:click` bubbling */
296
297            this.$container.on('click.comboselect', function(e){
298                e.stopPropagation();
299            })
300
301
302            /* Input: keydown */
303
304            this.$container.on('keydown', 'input', $.proxy(this._keydown, this))
305
306            /* Input: keyup */
307            
308            this.$container.on('keyup', 'input', $.proxy(this._keyup, this))
309
310            /* Dropdown item: click */
311
312            this.$container.on('click.item', '.option-item', $.proxy(this._select, this))
313
314        },
315
316        _keydown: function(event){
317
318            
319
320            switch(event.which){
321
322                case keys.UP:
323                    this._move('up', event)
324                    break;
325
326                case keys.DOWN:
327                    this._move('down', event)
328                    break;
329                
330                case keys.TAB:
331                    this._enter(event)
332                    break;
333
334                case keys.RIGHT:
335                    this._autofill(event);
336                    break;
337
338                case keys.ENTER:
339                    this._enter(event);
340                    break;
341
342                default:                            
343                    break;
344
345
346            }
347
348        },
349        
350
351        _keyup: function(event){
352            
353            switch(event.which){
354                case keys.ESC:                                                  
355                    this.$container.trigger('comboselect:close')
356                    break;
357
358                case keys.ENTER:
359                case keys.UP:
360                case keys.DOWN:
361                case keys.LEFT:
362                case keys.RIGHT:
363                case keys.TAB:
364                case keys.SHIFT:                            
365                    break;
366                
367                default:                            
368                    this._filter(event.target.value)
369                    break;
370            }
371        },
372        
373        _enter: function(event){
374
375            var item = this._getHovered()
376
377            item.length && this._select(item);
378
379            /* Check if it enter key */
380            if(event && event.which == keys.ENTER){
381
382                if(!item.length) {
383                    
384                    /* Check if its illegal value */
385
386                    this._blur();
387
388                    return true;
389                }
390
391                event.preventDefault();
392            }
393            
394
395        },
396        _move: function(dir){
397
398            var items = this._getVisible(),
399                current = this._getHovered(),
400                index = current.prevAll('.option-item').filter(':visible').length,
401                total = items.length
402
403            
404            switch(dir){
405                case 'up':
406                    index--;
407                    (index < 0) && (index = (total - 1));
408                    break;
409
410                case 'down':                            
411                    index++;
412                    (index >= total) && (index = 0);                            
413                    break;
414            }
415
416            
417            items
418                .removeClass(this.settings.hoverClass)
419                .eq(index)
420                .addClass(this.settings.hoverClass)
421
422
423            if(!this.opened) this.$container.trigger('comboselect:open');
424
425            this._fixScroll()
426        },
427
428        _select: function(event){
429
430            var item = event.currentTarget? $(event.currentTarget) : $(event);
431
432            if(!item.length) return;
433
434            /**
435             * 1. get Index
436             */
437            
438            var index = item.data('index');
439
440            this._selectByIndex(index);
441
442            //this.$container.trigger('comboselect:close')                    
443
444            this.$input.focus();
445
446            this.$container.trigger('comboselect:close');                   
447
448        },
449
450        _selectByIndex: function(index){
451
452            /**
453             * Set selected index and trigger change
454             * @type {[type]}
455             */
456            var self = this;
457
458            if(typeof index == 'undefined'){
459
460                index = 0;
461                
462                this.$el.prop('selectedIndex', index);
463                
464            }
465            
466            if(this.$el.prop('selectedIndex') != index){
467
468                this.$el.prop('selectedIndex', index).trigger('change');
469            }
470
471        },
472
473        _autofill: function(){
474
475            var item = this._getHovered();
476
477            if(item.length){
478
479                var index = item.data('index')
480
481                this._selectByIndex(index)
482
483            }
484
485        },
486        
487
488        _filter: function(search){
489
490            var self = this,
491                items = this._getAll();
492                needle = $.trim(search).toLowerCase(),
493                reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g'),
494                pattern = '(' + search.replace(reEscape, '\\$1') + ')';
495
496
497            /**
498             * Unwrap all markers
499             */
500            
501            $('.'+self.settings.markerClass, items).contents().unwrap();
502
503            /* Search */
504            
505            if(needle){
506
507                /* Hide Disabled and optgroups */
508
509                this.$items.filter('.option-group, .option-disabled').hide();
510            
511                
512                items                           
513                    .hide()
514                    .filter(function(){
515
516                        var $this = $(this),
517                            text = $.trim($this.text()).toLowerCase();
518                        
519                        /* Found */
520                        if(text.toString().indexOf(needle) != -1){
521                                                                
522                            /**
523                             * Wrap the selection
524                             */                                 
525                            
526                            $this
527                                .html(function(index, oldhtml){
528                                return oldhtml.replace(new RegExp(pattern, 'gi'), '<span class="'+self.settings.markerClass+'">$1</span>')
529                            })                                  
530
531                            return true
532                        }
533
534                    })
535                    .show()
536            }else{
537
538                                
539                this.$items.show();
540            }
541
542            /* Open the comboselect */
543
544            this.$container.trigger('comboselect:open')
545            
546
547        },
548
549        _highlight: function(){
550
551            /* 
552            1. Check if there is a selected item 
553            2. Add hover class to it
554            3. If not add hover class to first item
555            */
556        
557            var visible = this._getVisible().removeClass(this.settings.hoverClass),
558                $selected = visible.filter('.'+this.settings.selectedClass)
559
560            if($selected.length){
561                
562                $selected.addClass(this.settings.hoverClass);
563
564            }else{
565
566                visible
567                    .removeClass(this.settings.hoverClass)
568                    .first()
569                    .addClass(this.settings.hoverClass)
570            }
571
572        },
573
574        _updateInput: function(){
575
576            var selected = this.$el.prop('selectedIndex')
577            
578            if(this.$el.val()){
579                
580                text = this.$el.find('option').eq(selected).text()
581
582                this.$input.val(text)
583
584            }else{
585                
586                this.$input.val('')
587
588            }
589
590            this.$container.toggleClass('combo-has-value', !!this.$input.val())
591
592            return this._getAll()
593                .removeClass(this.settings.selectedClass)
594                .filter(function(){
595
596                    return $(this).data('index') == selected
597                })
598                .addClass(this.settings.selectedClass)
599        
600        },
601        _blurSelect: function(){
602
603            this.$container.removeClass('combo-focus');
604
605        },
606        _focus: function(event){
607            
608            /* Toggle focus class */
609
610            this.$container.toggleClass('combo-focus', !this.opened);
611
612            /* If mobile: stop */
613            
614            if(isMobile) return;
615
616            /* Open combo */
617
618            if(!this.opened) this.$container.trigger('comboselect:open');
619            
620            /* Select the input */
621            
622            this.settings.focusInput && event && event.currentTarget && event.currentTarget.nodeName == 'INPUT' && event.currentTarget.select()
623        },
624
625        _blur: function(){
626
627            /**
628             * 1. Get hovered item
629             * 2. If not check if input value == select option
630             * 3. If none
631             */
632            
633            var val = $.trim(this.$input.val()).toLowerCase().replace(/\s\s+/gi, ' '),
634                isNumber = !isNaN(val);
635            
636            
637            var index = this.$options.filter(function(){
638                return this.nodeName == 'OPTION'
639            }).filter(function(){
640                
641                if(isNumber){       
642                    return parseInt($.trim(this.innerText).toLowerCase()) == val
643                }
644
645                var _val = $.trim(this.innerText).toLowerCase().replace(/\s\s+/gi, ' ');
646
647                return _val == val
648
649            }).prop('index')
650
651
652            if(!index && this.$el.prop('selectedIndex') != 'undefined'){
653                return this._updateInput();
654            }
655
656        
657            /* Select by Index */
658                        
659            this._selectByIndex(index)
660            
661        },
662
663        _change: function(){
664
665
666            this._updateInput();
667
668        },
669
670        _getAll: function(){
671
672            return this.$items.filter('.option-item')
673
674        },
675        _getVisible: function(){
676
677            return this.$items.filter('.option-item').filter(':visible')
678
679        },
680
681        _getHovered: function(){
682
683            return this._getVisible().filter('.' + this.settings.hoverClass);
684
685        },
686
687        _open: function(){
688
689            var self = this
690
691            this.$container.addClass('combo-open')          
692
693            this.opened = true
694            
695            /* Focus input field */         
696
697            this.settings.focusInput && setTimeout(function(){ !self.$input.is(':focus') && self.$input.focus(); });
698
699            /* Highligh the items */
700
701            this._highlight()
702
703            /* Fix scroll */
704
705            this._fixScroll()
706
707            /* Close all others */
708
709
710            $.each($.fn[ pluginName ].instances, function(i, plugin){
711
712                if(plugin != self && plugin.opened) plugin.$container.trigger('comboselect:close')
713            })
714
715        },
716
717        _toggle: function(){
718
719            this.opened? this._close.call(this) : this._open.call(this)
720        },
721
722        _close: function(){             
723
724            this.$container.removeClass('combo-open combo-focus')
725
726            this.$container.trigger('comboselect:closed')
727
728            this.opened = false
729
730            /* Show all items */
731
732            this.$items.show();
733
734        },
735        _fixScroll: function(){
736    
737            /**
738             * If dropdown is hidden
739             */
740            if(this.$dropdown.is(':hidden')) return;
741
742            
743            /**
744             * Else                  
745             */
746            var item = this._getHovered();
747
748            if(!item.length) return;                    
749
750            /**
751             * Scroll
752             */
753            
754            var offsetTop,
755                upperBound,
756                lowerBound,
757                heightDelta = item.outerHeight()
758
759            offsetTop = item[0].offsetTop;
760            
761            upperBound = this.$dropdown.scrollTop();
762
763            lowerBound = upperBound + this.settings.maxHeight - heightDelta;
764            
765            if (offsetTop < upperBound) {
766                    
767                this.$dropdown.scrollTop(offsetTop);
768
769            } else if (offsetTop > lowerBound) {
770                    
771                this.$dropdown.scrollTop(offsetTop - this.settings.maxHeight + heightDelta);
772            }
773
774        },
775        toggleDisplay: function(condition){
776
777            this.$el.toggle(condition)
778        },
779        /**
780         * Update API
781         */
782        
783        _update: function(){
784
785            this.$options = this.$el.find('option, optgroup')
786
787            this.$dropdown.empty(); 
788            this.$input.val('');        
789
790            this._build();
791        },
792        
793        /**
794         * Destroy API
795         */
796        
797        dispose: function(){
798
799            /* Remove combo arrow, input, dropdown */
800
801            this.$arrow.remove()
802
803            this.$input.remove()
804
805            this.$dropdown.remove()
806
807            /* Remove tabindex property */
808            this.$el
809                .removeAttr("tabindex")
810
811            /* Check if there is a tabindex set before */
812
813            if(!!this.$el.data('plugin_'+ dataKey + '_tabindex')){
814                this.$el.prop('tabindex', this.$el.data('plugin_'+ dataKey + '_tabindex'))
815            }
816
817            /* Unwrap */
818
819            this.$el.unwrap()
820
821            /* Remove data */
822
823            this.$el.removeData('plugin_'+dataKey)
824
825            /* Remove tabindex data */
826
827            this.$el.removeData('plugin_'+dataKey + '_tabindex')
828
829            /* Remove change event on select */
830
831            this.$el.off('change.select focus.select blur.select');
832
833        }   
834    });
835
836
837
838    // A really lightweight plugin wrapper around the constructor,
839    // preventing against multiple instantiations
840    $.fn[ pluginName ] = function ( options, args ) {
841
842        this.each(function() {
843
844            var $e = $(this),
845                instance = $e.data('plugin_'+dataKey)
846
847            if (typeof options === 'string') {
848                
849                if (instance && typeof instance[options] === 'function') {
850                        instance[options](args);
851                }
852
853            }else{
854
855                if (instance && instance.dispose) {
856                        instance.dispose();
857                }
858
859                $.data( this, "plugin_" + dataKey, new Plugin( this, options ) );
860
861            }
862
863        });
864
865        // chain jQuery functions
866        return this;
867    };
868
869    $.fn[ pluginName ].instances = [];
870
871}));

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.