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