1/* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, boss:true, undef:true, curly:true, browser:true, jquery:true */ 2/* 3 * jQuery MultiSelect UI Widget Filtering Plugin 3.0.0 4 * Copyright (c) 2012 Eric Hynds 5 * 6 * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/ 7 * 8 * Depends: 9 * - jQuery UI MultiSelect widget 10 * 11 * Dual licensed under the MIT and GPL licenses: 12 * http://www.opensource.org/licenses/mit-license.php 13 * http://www.gnu.org/licenses/gpl.html 14 * 15 */ 16(function($) { 17 var rEscape = /[\-\[\]{}()*+?.,\\\^$|#\s]/g; 18 19 // "{{term}}" is a placeholder below for where the search term 20 // would be inserted in the resulting regular expression. 21 var filterRules = { 22 contains: '{{term}}', 23 beginsWith: '^{{term}}', 24 endsWith: '{{term}}$', 25 exactMatch: '^{{term}}$', 26 containsNumber: '\d', 27 isNumeric: '^\d+$', 28 isNonNumeric: '^\D+$' 29 }; 30 31 var headerSelector = '.ui-multiselect-header'; 32 var hasFilterClass = 'ui-multiselect-hasfilter'; 33 var filterClass = 'ui-multiselect-filter'; 34 var optgroupClass = 'ui-multiselect-optgroup'; 35 var groupLabelClass = 'ui-multiselect-grouplabel'; 36 var hiddenClass = 'ui-multiselect-excluded'; 37 38 //Courtesy of underscore.js 39 function debounce(func, wait, immediate) { 40 var timeout; 41 return function() { 42 var context = this, args = arguments; 43 var later = function() { 44 timeout = null; 45 if (!immediate) { 46 func.apply(context, args); 47 } 48 }; 49 var callNow = immediate && !timeout; 50 clearTimeout(timeout); 51 timeout = setTimeout(later, wait); 52 if (callNow) { 53 func.apply(context, args); 54 } 55 }; 56 } 57 58 $.widget('ech.multiselectfilter', { 59 60 options: { 61 label: 'Filter:', // (string) The label to show with the input 62 placeholder: 'Enter keywords', // (string) The placeholder text to show in the input 63 filterRule: 'contains', // (string) Either a named filter rule from above or a regular expression containing {{term}} as a placeholder 64 searchGroups: false, // (true | false) If true, search option group labels and show an entire group on a match. 65 autoReset: false, // (true | false) If true, clear the filter each time the widget menu is closed. 66 width: null, // (number) Override default width set in css file (px). null will inherit 67 debounceMS: 250 // (number) Number of milleseconds to wait between running the search handler. 68 }, 69 70 /** 71 * Performs widget creation 72 * Widget API has already set this.element and this.options for us 73 * - Find the multiselect widget. 74 * - Create the filter input 75 * - Set up event handlers 76 * - Insert in header 77 * - Create text cache 78 * - Override toggleState 79 */ 80 _create: function() { 81 var opts = this.options; 82 var $element = this.element; 83 84 // get the multiselect instance 85 this.instance = $element.data('ech-multiselect'); 86 87 // store header; add filter class so the close/check all/uncheck all links can be positioned correctly 88 this.$header = this.instance.$menu.find(headerSelector).addClass(hasFilterClass); 89 90 // wrapper $element 91 this.$input = $(document.createElement('input')) 92 .attr({ 93 placeholder: opts.placeholder, 94 type: "search" 95 }) 96 .css({ width: (typeof opts.width === 'string') 97 ? this.instance._parse2px(opts.width, this.$header).px + 'px' 98 : (/\d/.test(opts.width) ? opts.width + 'px' : null) 99 }); 100 this._bindInputEvents(); 101 // automatically reset the widget on close? 102 if (this.options.autoReset) { 103 $element.on('multiselectbeforeclose', $.proxy(this._reset, this)); 104 } 105 106 var $label = $(document.createElement('label')).text(opts.label).append(this.$input).addClass('ui-multiselect-filter-label'); 107 this.$wrapper = $(document.createElement('div')) 108 .addClass(filterClass) 109 .append($label) 110 .prependTo(this.$header); 111 112 // If menu already opened, have to reset menu height since 113 // addition of the filter input changes the header height calc. 114 if (!!this.instance._isOpen) { 115 this.instance._setMenuHeight(true); 116 } 117 118 // cache input values for searching 119 this.updateCache(); 120 121 // Change the normal _toggleChecked fxn behavior so that when checkAll/uncheckAll 122 // is fired, only the currently displayed filtered inputs are checked if filter entered. 123 var instance = this.instance; 124 var filter = this.$input[0]; 125 instance._oldToggleChecked = instance._toggleChecked; 126 instance._toggleChecked = function(flag, group) { 127 instance._oldToggleChecked(flag, group, !!filter.value); 128 }; 129 }, 130 131 /** 132 * Binds keyboard events to the input 133 * This is where special behavior like ALT-R for reset is bound 134 */ 135 _bindInputEvents: function() { 136 this.$input.on({ 137 keydown: function(e) { 138 // prevent the enter key from submitting the form / closing the widget 139 if(e.which === 13) 140 e.preventDefault(); 141 else if(e.which === 27) { 142 $element.multiselect('close'); 143 e.preventDefault(); 144 } 145 else if(e.which === 9 && e.shiftKey) { 146 $element.multiselect('close'); 147 e.preventDefault(); 148 } 149 else if(e.altKey) { 150 switch(e.which) { 151 case 82: 152 e.preventDefault(); 153 $(this).val('').trigger('input', ''); 154 break; 155 case 65: 156 $element.multiselect('checkAll'); 157 break; 158 case 85: 159 $element.multiselect('uncheckAll'); 160 break; 161 case 70: 162 $element.multiselect('flipAll'); 163 break; 164 case 76: 165 $element.multiselect('instance').$labels.first().trigger("mouseenter"); 166 break; 167 } 168 } 169 }, 170 input: $.proxy(debounce(this._handler, this.options.debounceMS), this), 171 search: $.proxy(this._handler, this) 172 }); 173 }, 174 175 /** 176 * Handles searches as text is entered in the filter box. 177 * Uses a text cache to speed up searching. 178 * Debouncing is done to limit how often this is ran. 179 * Alternate filter rules can be used. 180 * Option group labels may be searched, also. 181 * @param (object) event object from original event. 182 */ 183 _handler: function(e) { 184 var term = this.$input[0].value.toLowerCase().replace(/^\s+|\s+$/g,''); 185 var filterRule = this.options.filterRule || 'contains'; 186 var regex = new RegExp( ( filterRules[filterRule] || filterRule ).replace('{{term}}', term.replace(rEscape, "\\$&")), 'i'); 187 var searchGroups = !!this.options.searchGroups; 188 var $checkboxes = this.instance.$checkboxes; 189 var cache = this.cache; // Cached text() object 190 191 this.$rows.toggleClass(hiddenClass, !!term); 192 var filteredInputs = $checkboxes.children().map(function(x) { 193 var elem = this; 194 var $groupItems = $(elem); 195 var groupShown = false;
196 197 // Account for optgroups 198 // If we are searching in option group labels and we match an optgroup label, 199 // then show all its children and return all its inputs also. 200 if (elem.classList.contains(optgroupClass)) { 201 var $groupItems = $groupItems.find('li'); 202 if (searchGroups && regex.test( cache[x] ) ) { 203 elem.classList.remove(hiddenClass); 204 $groupItems.removeClass(hiddenClass); 205 return $groupItems.find('input').get(); 206 } 207 } 208 209 return $groupItems.map(function(y) { 210 if ( regex.test( cache[x + '.' + y] ) ) { 211 // Show the opt group heading if needed 212 if (!groupShown) { 213 elem.classList.remove(hiddenClass); 214 groupShown = true; 215 } 216 this.classList.remove(hiddenClass); 217 return this.getElementsByTagName('input')[0]; 218 } 219 return null; 220 }); 221 222 }); 223 if (term) { 224 this._trigger('filter', e, filteredInputs); 225 } 226 if (!this.instance.options.listbox && this.instance._isOpen) { 227 this.instance._setMenuHeight(true); 228 this.instance.position(); 229 } 230 return; 231 }, 232 233 _reset: function () { 234 this.$input.val('') 235 var event = document.createEvent('Event'); 236 event.initEvent('reset', true, true); 237 this.$input.get(0).dispatchEvent(event) 238 this._handler(event) 239 }, 240 241 /** 242 * Creates a text cache object from the widget options' text. 243 * @param (boolean) alsoRefresh causes the displayed search results to refresh. 244 */ 245 updateCache: function(alsoRefresh) { 246 var cache = {}; // keys are like 0, 0.1, 1, 1.0, 1.1 etc. 247 this.instance.$checkboxes.children().each(function(x) { 248 var $element = $(this); 249 // Account for optgroups 250 if (this.classList.contains(optgroupClass)) { 251 // Single number keys are the option labels 252 cache[x] = this.getElementsByClassName(groupLabelClass)[0].textContent; 253 $element = $element.find('li'); 254 } 255 $element.each(function(y) { 256 cache[x + '.' + y] = this.textContent; 257 }); 258 }); 259 this.cache = cache; 260 this.$rows = this.instance.$checkboxes.find('li'); 261 if (!!alsoRefresh) { 262 this._handler(); 263 } 264 }, 265 266 /** 267 * Returns the input wrapper div 268 */ 269 widget: function() { 270 return this.$wrapper; 271 }, 272 273 /** 274 * Destroys this widget 275 */ 276 destroy: function() { 277 $.Widget.prototype.destroy.call(this); 278 this.$input.val('').trigger("keyup").off('keydown input search'); 279 this.instance.$menu.find(headerSelector).removeClass(hasFilterClass); 280 this.$wrapper.remove(); 281 } 282 }); 283 284})(jQuery);
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.