1;(function($) { 2 'use strict'; 3 4 var pluginName = 'selectric', 5 classList = 'Input Items Open Disabled TempShow HideSelect Wrapper Hover Responsive Above Scroll Group GroupLabel', 6 bindSufix = '.sl', 7 defaults = { 8 onChange: function(elm) { $(elm).change(); }, 9 maxHeight: 300, 10 keySearchTimeout: 500, 11 arrowButtonMarkup: '<b class="button">▾</b>', 12 disableOnMobile: true, 13 openOnHover: false, 14 hoverIntentTimeout: 500, 15 expandToItemText: false, 16 responsive: false, 17 preventWindowScroll: true, 18 inheritOriginalWidth: false, 19 allowWrap: true, 20 customClass: { 21 prefix: pluginName, 22 camelCase: false, 23 overwrite: true 24 }, 25 optionsItemBuilder: '{text}', // function(itemData, element, index) 26 labelBuilder: '{text}' // function(currItem) 27 }, 28 hooks = { 29 add: function(callbackName, hookName, fn) { 30 if ( !this[callbackName] ) 31 this[callbackName] = {}; 32 33 this[callbackName][hookName] = fn; 34 }, 35 remove: function(callbackName, hookName) { 36 delete this[callbackName][hookName]; 37 } 38 }, 39 _utils = { 40 // Replace diacritics 41 replaceDiacritics: function(s) { 42 // /[\340-\346]/g, // a 43 // /[\350-\353]/g, // e 44 // /[\354-\357]/g, // i 45 // /[\362-\370]/g, // o 46 // /[\371-\374]/g, // u 47 // /[\361]/g, // n 48 // /[\347]/g, // c 49 // /[\377]/g // y 50 var d = '40-46 50-53 54-57 62-70 71-74 61 47 77'.replace(/\d+/g, '\\3$&').split(' '), 51 k = d.length; 52 53 while (k--) 54 s = s.toLowerCase().replace(RegExp('[' + d[k] + ']', 'g'), 'aeiouncy'.charAt(k)); 55 56 return s; 57 }, 58 // https://gist.github.com/atesgoral/984375 59 format: function(f) {var a=arguments;return(""+f).replace(/{(\d+|(\w+))}/g,function(s,i,p) {return p&&a[1]?a[1][p]:a[i]})}, 60 nextEnabledItem: function(selectItems, selected) { 61 while ( selectItems[ selected = (selected + 1) % selectItems.length ].disabled ) {} 62 return selected; 63 }, 64 previousEnabledItem: function(selectItems, selected) { 65 while ( selectItems[ selected = (selected > 0 ? selected : selectItems.length) - 1 ].disabled ) {} 66 return selected; 67 }, 68 toDash: function(str) { 69 return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 70 }, 71 triggerCallback: function(fn, scope) { 72 var elm = scope.element, 73 func = scope.options['on' + fn]; 74 75 if ( $.isFunction(func) ) 76 func.call(elm, elm, scope); 77 78 if ( hooks[fn] ) { 79 $.each(hooks[fn], function() { 80 this.call(elm, elm, scope); 81 }); 82 } 83 84 $(elm).trigger(pluginName + '-' + _utils.toDash(fn), scope); 85 } 86 }, 87 $doc = $(document), 88 $win = $(window), 89 Selectric = function(element, opts) { 90 var _this = this, 91 $original = $(element), 92 $input, $items, $itemsScroll, $wrapper, $label, $outerWrapper, $li, 93 isOpen = false, 94 isEnabled = false, 95 selected, 96 currValue, 97 itemsHeight, 98 itemsInnerHeight, 99 finalWidth, 100 optionsLength, 101 eventTriggers, 102 isMobile = /android|ip(hone|od|ad)/i.test(navigator.userAgent), 103 tabindex = $original.prop('tabindex'), 104 labelBuilder; 105 106 function _init(opts) { 107 _this.options = $.extend(true, {}, defaults, _this.options, opts); 108 _this.classes = {}; 109 _this.element = element; 110 111 _utils.triggerCallback('BeforeInit', _this); 112 113 // Disable on mobile browsers 114 if ( _this.options.disableOnMobile && isMobile ) { 115 _this.disableOnMobile = true; 116 return; 117 } 118 119 // Preserve data 120 _destroy(true); 121 122 // Generate classNames for elements 123 var customClass = _this.options.customClass, 124 postfixes = classList.split(' '), 125 originalWidth = $original.width(); 126 127 $.each(postfixes, function(i, currClass) { 128 var c = customClass.prefix + '-' + currClass; 129 _this.classes[currClass.toLowerCase()] = customClass.camelCase ? c : _utils.toDash(c); 130 }); 131 132 $input = $('<input/>', { 'class': _this.classes.input, 'readonly': isMobile }); 133 $items = $('<div/>', { 'class': _this.classes.items, 'tabindex': -1 }); 134 $itemsScroll = $('<div/>', { 'class': _this.classes.scroll }); 135 $wrapper = $('<div/>', { 'class': customClass.prefix, 'html': _this.options.arrowButtonMarkup }); 136 $label = $('<p class="label"/>'); 137 $outerWrapper = $original.wrap('<div>').parent().append($wrapper.prepend($label), $items, $input); 138 139 eventTriggers = { 140 open : _open, 141 close : _close, 142 destroy : _destroy, 143 refresh : _refresh, 144 init : _init 145 }; 146 147 $original.on(eventTriggers).wrap('<div class="' + _this.classes.hideselect + '">'); 148 $.extend(_this, eventTriggers); 149 150 labelBuilder = _this.options.labelBuilder; 151 152 if ( _this.options.inheritOriginalWidth && originalWidth > 0 ) 153 $outerWrapper.width(originalWidth); 154 155 _populate(); 156 } 157 158 // Generate options markup and event binds 159 function _populate() { 160 _this.items = []; 161 162 var $options = $original.children(), 163 _$li = '<ul>', 164 $justOptions = $original.find('option'), 165 selectedIndex = $justOptions.index($justOptions.filter(':selected')), 166 currIndex = 0; 167 168 currValue = (selected = ~selectedIndex ? selectedIndex : 0); 169 170 if ( optionsLength = $options.length ) { 171 // Build options markup 172 $options.each(function() { 173 var $elm = $(this); 174 175 if ( $elm.is('optgroup') ) { 176 var groupDisabled = $elm.prop('disabled'), 177 $children = $elm.children(); 178 179 _$li += _utils.format('<ul class="{1}"><li class="{2}">{3}</li>', 180 $.trim([_this.classes.group, groupDisabled ? 'disabled' : '', $elm.prop('class')].join(' ')), 181 _this.classes.grouplabel, 182 $elm.prop('label') 183 ); 184 185 if ( groupDisabled ) { 186 $children.prop('disabled', true); 187 } 188 189 $children.each(buildOption); 190 191 _$li += '</ul>'; 192 } else { 193 buildOption.call($elm); 194 } 195 196 function buildOption() { 197 var $elm = $(this), 198 optionText = $elm.html(), 199 selectDisabled = $elm.prop('disabled'), 200 itemBuilder = _this.options.optionsItemBuilder; 201 202 _this.items[currIndex] = { 203 element : $elm, 204 value : $elm.val(), 205 text : optionText, 206 slug : _utils.replaceDiacritics(optionText), 207 disabled : selectDisabled 208 }; 209 210 _$li += _utils.format('<li data-index="{1}" class="{2}">{3}</li>', 211 currIndex, 212 $.trim([currIndex == currValue ? 'selected' : '', currIndex == optionsLength - 1 ? 'last' : '', selectDisabled ? 'disabled' : ''].join(' ')), 213 $.isFunction(itemBuilder) ? itemBuilder(_this.items[currIndex], $elm, currIndex) : _utils.format(itemBuilder, _this.items[currIndex]) 214 ); 215 216 currIndex++; 217 } 218 }); 219 220 $items.append( $itemsScroll.html(_$li + '</ul>') ); 221 222 $label.html( 223 $.isFunction(labelBuilder) ? labelBuilder(_this.items[currValue]) : _utils.format(labelBuilder, _this.items[currValue]) 224 ) 225 } 226 227 $wrapper.add($original).add($outerWrapper).add($input).off(bindSufix); 228 229 $outerWrapper.prop('class', [ 230 _this.classes.wrapper, 231 _this.options.customClass.overwrite ? 232 $original.prop('class').replace(/\S+/g, _this.options.customClass.prefix + '-$&') : 233 $original.prop('class'), 234 _this.options.responsive ? _this.classes.responsive : '' 235 ].join(' ')); 236 237 if ( !$original.prop('disabled') ) { 238 isEnabled = true; 239 240 // Not disabled, so... Removing disabled class and bind hover 241 $outerWrapper.removeClass(_this.classes.disabled).on('mouseenter' + bindSufix + ' mouseleave' + bindSufix, function(e) { 242 $(this).toggleClass(_this.classes.hover); 243 244 // Delay close effect when openOnHover is true 245 if ( _this.options.openOnHover ) { 246 clearTimeout(_this.closeTimer); 247 e.type == 'mouseleave' ? _this.closeTimer = setTimeout(_close, _this.options.hoverIntentTimeout) : _open(); 248 } 249 }); 250 251 // Toggle open/close
252 $wrapper.on('click' + bindSufix, function(e) { 253 isOpen ? _close() : _open(e); 254 }); 255 256 $input 257 .prop({ 258 tabindex: tabindex, 259 disabled: false 260 }) 261 .on('keypress' + bindSufix, _handleSystemKeys) 262 .on('keydown' + bindSufix, function(e) { 263 _handleSystemKeys(e); 264 265 // Clear search 266 clearTimeout(_this.resetStr); 267 _this.resetStr = setTimeout(function() { 268 $input.val(''); 269 }, _this.options.keySearchTimeout); 270 271 var key = e.keyCode || e.which; 272 273 // If it's a directional key 274 // 37 => Left 275 // 38 => Up 276 // 39 => Right 277 // 40 => Down 278 if ( key > 36 && key < 41 ) { 279 if ( !_this.options.allowWrap ) { 280 if ( (key < 39 && selected == 0) || (key > 38 && (selected + 1) == _this.items.length) ) { 281 return; 282 } 283 } 284 285 _select(_utils[(key < 39 ? 'previous' : 'next') + 'EnabledItem'](_this.items, selected)); 286 } 287 }) 288 .on('focusin' + bindSufix, function(e) { 289 // Stupid, but necessary... Prevent the flicker when 290 // focusing out and back again in the browser window 291 $input.one('blur', function() { 292 $input.blur(); 293 }); 294 295 isOpen || _open(e); 296 }) 297 .on('oninput' in $input[0] ? 'input' : 'keyup', function() { 298 if ( $input.val().length ) { 299 // Search in select options 300 $.each(_this.items, function(i, elm) { 301 if ( RegExp('^' + $input.val(), 'i').test(elm.slug) && !elm.disabled ) { 302 _select(i); 303 return false; 304 } 305 }); 306 } 307 }); 308 309 $original.prop('tabindex', false); 310 311 // Remove styles from items box 312 // Fix incorrect height when refreshed is triggered with fewer options 313 $li = $('li', $items.removeAttr('style')).on({ 314 // Prevent <input> blur on Chrome 315 mousedown: function(e) { 316 e.preventDefault(); 317 e.stopPropagation(); 318 }, 319 click: function() { 320 // The second parameter is to close the box after click 321 _select($(this).data('index'), true); 322 323 // Chrome doesn't close options box if select is wrapped with a label 324 // We need to 'return false' to avoid that 325 return false; 326 } 327 }).filter('[data-index]'); 328 } else { 329 $outerWrapper.addClass(_this.classes.disabled); 330 $input.prop('disabled', true); 331 } 332 333 _utils.triggerCallback('Init', _this); 334 } 335 336 function _refresh() { 337 _utils.triggerCallback('Refresh', _this); 338 _populate(); 339 } 340 341 // Behavior when system keys is pressed 342 function _handleSystemKeys(e) { 343 var key = e.keyCode || e.which; 344 345 if ( key == 13 ) { 346 e.preventDefault(); 347 } 348 349 // Tab / Enter / ESC 350 if ( /^(9|13|27)$/.test(key) ) { 351 e.stopPropagation(); 352 _select(selected, true); 353 } 354 } 355 356 // Set options box width/height 357 function _calculateOptionsDimensions() { 358 // Calculate options box height 359 // Set a temporary class on the hidden parent of the element 360 var hiddenChildren = $items.closest(':visible').children(':hidden').addClass(_this.classes.tempshow), 361 maxHeight = _this.options.maxHeight, 362 itemsWidth = $items.outerWidth(), 363 wrapperWidth = $wrapper.outerWidth() - (itemsWidth - $items.width()); 364 365 // Set the dimensions, minimum is wrapper width, expand for long items if option is true
366 if ( !_this.options.expandToItemText || wrapperWidth > itemsWidth ) 367 finalWidth = wrapperWidth; 368 else { 369 // Make sure the scrollbar width is included 370 $items.css('overflow', 'scroll'); 371 372 // Set a really long width for $outerWrapper 373 $outerWrapper.width(9e4); 374 finalWidth = $items.width(); 375 // Set scroll bar to auto 376 $items.css('overflow', ''); 377 $outerWrapper.width(''); 378 } 379 380 $items.width(finalWidth).height() > maxHeight && $items.height(maxHeight); 381 382 // Remove the temporary class 383 hiddenChildren.removeClass(_this.classes.tempshow); 384 } 385 386 // Open the select options box 387 function _open(e) { 388 _utils.triggerCallback('BeforeOpen', _this); 389 390 if ( e ) { 391 e.preventDefault(); 392 e.stopPropagation(); 393 } 394 395 if ( isEnabled ) { 396 _calculateOptionsDimensions(); 397 398 // Find any other opened instances of select and close it 399 $('.' + _this.classes.hideselect, '.' + _this.classes.open).children()[pluginName]('close'); 400 401 isOpen = true; 402 itemsHeight = $items.outerHeight(); 403 itemsInnerHeight = $items.height(); 404 405 // Toggle options box visibility 406 $outerWrapper.addClass(_this.classes.open); 407 408 // Give dummy input focus 409 $input.val('').is(':focus') || $input.focus(); 410 411 $doc.on('click' + bindSufix, _close).on('scroll' + bindSufix, _isInViewport); 412 _isInViewport(); 413 414 // Prevent window scroll when using mouse wheel inside items box 415 if ( _this.options.preventWindowScroll ) { 416 $doc.on('mousewheel' + bindSufix + ' DOMMouseScroll' + bindSufix, '.' + _this.classes.scroll, function(e) { 417 var orgEvent = e.originalEvent, 418 scrollTop = $(this).scrollTop(), 419 deltaY = 0; 420 421 if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; } 422 if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; } 423 if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; } 424 if ( 'deltaY' in orgEvent ) { deltaY = orgEvent.deltaY * -1; } 425 426 if ( scrollTop == (this.scrollHeight - itemsInnerHeight) && deltaY < 0 || scrollTop == 0 && deltaY > 0 ) { 427 e.preventDefault(); 428 } 429 }); 430 } 431 432 _detectItemVisibility(selected); 433 434 _utils.triggerCallback('Open', _this); 435 } 436 } 437 438 // Detect is the options box is inside the window 439 function _isInViewport() { 440 $outerWrapper.toggleClass(_this.classes.above, $outerWrapper.offset().top + $outerWrapper.outerHeight() + itemsHeight > $win.scrollTop() + $win.height()); 441 } 442 443 // Close the select options box 444 function _close() { 445 _utils.triggerCallback('BeforeClose', _this); 446 447 if ( currValue != selected ) { 448 _utils.triggerCallback('BeforeChange', _this); 449 450 var text = _this.items[selected].text; 451 452 // Apply changed value to original select 453 $original 454 .prop('selectedIndex', currValue = selected) 455 .data('value', text); 456 457 // Change label text 458 $label.html( 459 $.isFunction(labelBuilder) ? labelBuilder(_this.items[selected]) : _utils.format(labelBuilder, _this.items[selected]) 460 ) 461 462 _utils.triggerCallback('Change', _this); 463 } 464 465 // Remove custom events on document 466 $doc.off(bindSufix); 467 468 // Remove visible class to hide options box 469 $outerWrapper.removeClass(_this.classes.open); 470 471 isOpen = false; 472 473 _utils.triggerCallback('Close', _this); 474 } 475 476 // Select option 477 function _select(index, close) { 478 // Parameter index is required 479 if ( index == undefined ) { 480 return; 481 } 482 483 // If element is disabled, can't select it 484 if ( !_this.items[index].disabled ) {
485 // If 'close' is false (default), the options box won't close after 486 // each selected item, this is necessary for keyboard navigation 487 $li 488 .removeClass('selected') 489 .eq(selected = index) 490 .addClass('selected'); 491 492 _detectItemVisibility(index); 493 close && _close(); 494 } 495 } 496 497 // Detect if currently selected option is visible and scroll the options box to show it 498 function _detectItemVisibility(index) { 499 var liHeight = $li.eq(index).outerHeight(), 500 liTop = $li[index].offsetTop, 501 itemsScrollTop = $itemsScroll.scrollTop(), 502 scrollT = liTop + liHeight * 2; 503 504 $itemsScroll.scrollTop( 505 scrollT > itemsScrollTop + itemsHeight ? scrollT - itemsHeight : 506 liTop - liHeight < itemsScrollTop ? liTop - liHeight : 507 itemsScrollTop 508 ); 509 } 510 511 // Unbind and remove 512 function _destroy(preserveData) { 513 if ( isEnabled ) { 514 $items.add($wrapper).add($input).remove(); 515 !preserveData && $original.removeData(pluginName).removeData('value'); 516 $original.prop('tabindex', tabindex).off(bindSufix).off(eventTriggers).unwrap().unwrap(); 517 isEnabled = false; 518 } 519 } 520 521 _init(opts); 522 }; 523 524 // A really lightweight plugin wrapper around the constructor, 525 // preventing against multiple instantiations 526 $.fn[pluginName] = function(args) { 527 return this.each(function() { 528 var data = $.data(this, pluginName); 529 530 if ( data && !data.disableOnMobile ) 531 (''+args === args && data[args]) ? data[args]() : data.init(args); 532 else 533 $.data(this, pluginName, new Selectric(this, args)); 534 }); 535 }; 536 537 $.fn[pluginName].hooks = hooks; 538}(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.