1/*! 2 * JavaScript Custom Forms : Select Module 3 * 4 * Copyright 2014-2015 PSD2HTML - http://psd2html.com/jcf 5 * Released under the MIT license (LICENSE.txt) 6 * 7 * Version: 1.2.0 8 */ 9jcf.addModule(function($, window) { 10 'use strict'; 11 12 var module = { 13 name: 'Select', 14 selector: 'select', 15 options: { 16 element: null, 17 multipleCompactStyle: false 18 }, 19 plugins: { 20 ListBox: ListBox, 21 ComboBox: ComboBox, 22 SelectList: SelectList 23 }, 24 matchElement: function(element) { 25 return element.is('select'); 26 }, 27 init: function() { 28 this.element = $(this.options.element); 29 this.createInstance(); 30 }, 31 isListBox: function() { 32 return this.element.is('[size]:not([jcf-size]), [multiple]'); 33 }, 34 createInstance: function() { 35 if (this.instance) { 36 this.instance.destroy(); 37 } 38 if (this.isListBox() && !this.options.multipleCompactStyle) { 39 this.instance = new ListBox(this.options); 40 } else { 41 this.instance = new ComboBox(this.options); 42 } 43 }, 44 refresh: function() { 45 var typeMismatch = (this.isListBox() && this.instance instanceof ComboBox) || 46 (!this.isListBox() && this.instance instanceof ListBox); 47 48 if (typeMismatch) { 49 this.createInstance(); 50 } else { 51 this.instance.refresh(); 52 } 53 }, 54 destroy: function() { 55 this.instance.destroy(); 56 } 57 }; 58 59 // combobox module 60 function ComboBox(options) { 61 this.options = $.extend({ 62 wrapNative: true, 63 wrapNativeOnMobile: true, 64 fakeDropInBody: true, 65 useCustomScroll: true, 66 flipDropToFit: true, 67 maxVisibleItems: 10, 68 fakeAreaStructure: '<span class="jcf-select"><span class="jcf-select-text"></span><span class="jcf-select-opener"></span></span>', 69 fakeDropStructure: '<div class="jcf-select-drop"><div class="jcf-select-drop-content"></div></div>', 70 optionClassPrefix: 'jcf-option-', 71 selectClassPrefix: 'jcf-select-', 72 dropContentSelector: '.jcf-select-drop-content', 73 selectTextSelector: '.jcf-select-text', 74 dropActiveClass: 'jcf-drop-active', 75 flipDropClass: 'jcf-drop-flipped' 76 }, options); 77 this.init(); 78 } 79 $.extend(ComboBox.prototype, { 80 init: function() { 81 this.initStructure(); 82 this.bindHandlers(); 83 this.attachEvents(); 84 this.refresh(); 85 }, 86 initStructure: function() { 87 // prepare structure 88 this.win = $(window); 89 this.doc = $(document); 90 this.realElement = $(this.options.element); 91 this.fakeElement = $(this.options.fakeAreaStructure).insertAfter(this.realElement); 92 this.selectTextContainer = this.fakeElement.find(this.options.selectTextSelector); 93 this.selectText = $('<span></span>').appendTo(this.selectTextContainer); 94 makeUnselectable(this.fakeElement); 95 96 // copy classes from original select 97 this.fakeElement.addClass(getPrefixedClasses(this.realElement.prop('className'), this.options.selectClassPrefix)); 98 99 // handle compact multiple style 100 if (this.realElement.prop('multiple')) { 101 this.fakeElement.addClass('jcf-compact-multiple'); 102 } 103 104 // detect device type and dropdown behavior 105 if (this.options.isMobileDevice && this.options.wrapNativeOnMobile && !this.options.wrapNative) { 106 this.options.wrapNative = true; 107 } 108 109 if (this.options.wrapNative) { 110 // wrap native select inside fake block 111 this.realElement.prependTo(this.fakeElement).css({ 112 position: 'absolute', 113 height: '100%', 114 width: '100%' 115 }).addClass(this.options.resetAppearanceClass); 116 } else { 117 // just hide native select 118 this.realElement.addClass(this.options.hiddenClass); 119 this.fakeElement.attr('title', this.realElement.attr('title')); 120 this.fakeDropTarget = this.options.fakeDropInBody ? $('body') : this.fakeElement; 121 } 122 }, 123 attachEvents: function() { 124 // delayed refresh handler 125 var self = this; 126 this.delayedRefresh = function() { 127 setTimeout(function() { 128 self.refresh(); 129 if (self.list) { 130 self.list.refresh(); 131 self.list.scrollToActiveOption(); 132 } 133 }, 1); 134 }; 135 136 // native dropdown event handlers 137 if (this.options.wrapNative) { 138 this.realElement.on({ 139 focus: this.onFocus, 140 change: this.onChange, 141 click: this.onChange, 142 keydown: this.onChange 143 }); 144 } else { 145 // custom dropdown event handlers 146 this.realElement.on({ 147 focus: this.onFocus, 148 change: this.onChange, 149 keydown: this.onKeyDown 150 }); 151 this.fakeElement.on({ 152 'jcf-pointerdown': this.onSelectAreaPress 153 }); 154 } 155 }, 156 onKeyDown: function(e) { 157 if (e.which === 13) { 158 this.toggleDropdown(); 159 } else if (this.dropActive) { 160 this.delayedRefresh(); 161 } 162 }, 163 onChange: function() { 164 this.refresh(); 165 }, 166 onFocus: function() { 167 if (!this.pressedFlag || !this.focusedFlag) { 168 this.fakeElement.addClass(this.options.focusClass); 169 this.realElement.on('blur', this.onBlur); 170 this.toggleListMode(true); 171 this.focusedFlag = true; 172 } 173 }, 174 onBlur: function() { 175 if (!this.pressedFlag) { 176 this.fakeElement.removeClass(this.options.focusClass); 177 this.realElement.off('blur', this.onBlur); 178 this.toggleListMode(false); 179 this.focusedFlag = false;
180 } 181 }, 182 onResize: function() { 183 if (this.dropActive) { 184 this.hideDropdown(); 185 } 186 }, 187 onSelectDropPress: function() { 188 this.pressedFlag = true; 189 }, 190 onSelectDropRelease: function(e, pointerEvent) { 191 this.pressedFlag = false; 192 if (pointerEvent.pointerType === 'mouse') { 193 this.realElement.focus(); 194 } 195 }, 196 onSelectAreaPress: function(e) { 197 // skip click if drop inside fake element or real select is disabled 198 var dropClickedInsideFakeElement = !this.options.fakeDropInBody && $(e.target).closest(this.dropdown).length; 199 if (dropClickedInsideFakeElement || e.button > 1 || this.realElement.is(':disabled')) { 200 return; 201 } 202 203 // toggle dropdown visibility 204 this.selectOpenedByEvent = e.pointerType; 205 this.toggleDropdown(); 206 207 // misc handlers 208 if (!this.focusedFlag) { 209 if (e.pointerType === 'mouse') { 210 this.realElement.focus(); 211 } else { 212 this.onFocus(e); 213 } 214 } 215 this.pressedFlag = true; 216 this.fakeElement.addClass(this.options.pressedClass); 217 this.doc.on('jcf-pointerup', this.onSelectAreaRelease); 218 }, 219 onSelectAreaRelease: function(e) { 220 if (this.focusedFlag && e.pointerType === 'mouse') { 221 this.realElement.focus(); 222 } 223 this.pressedFlag = false; 224 this.fakeElement.removeClass(this.options.pressedClass); 225 this.doc.off('jcf-pointerup', this.onSelectAreaRelease); 226 }, 227 onOutsideClick: function(e) { 228 var target = $(e.target), 229 clickedInsideSelect = target.closest(this.fakeElement).length || target.closest(this.dropdown).length; 230 231 if (!clickedInsideSelect) { 232 this.hideDropdown(); 233 } 234 }, 235 onSelect: function() { 236 this.refresh(); 237 238 if (this.realElement.prop('multiple')) { 239 this.repositionDropdown(); 240 } else { 241 this.hideDropdown(); 242 } 243 244 this.fireNativeEvent(this.realElement, 'change'); 245 }, 246 toggleListMode: function(state) { 247 if (!this.options.wrapNative) { 248 if (state) { 249 // temporary change select to list to avoid appearing of native dropdown 250 this.realElement.attr({ 251 size: 4, 252 'jcf-size': '' 253 }); 254 } else { 255 // restore select from list mode to dropdown select 256 if (!this.options.wrapNative) { 257 this.realElement.removeAttr('size jcf-size'); 258 } 259 } 260 } 261 }, 262 createDropdown: function() { 263 // destroy previous dropdown if needed 264 if (this.dropdown) { 265 this.list.destroy(); 266 this.dropdown.remove(); 267 } 268 269 // create new drop container 270 this.dropdown = $(this.options.fakeDropStructure).appendTo(this.fakeDropTarget); 271 this.dropdown.addClass(getPrefixedClasses(this.realElement.prop('className'), this.options.selectClassPrefix)); 272 makeUnselectable(this.dropdown); 273 274 // handle compact multiple style 275 if (this.realElement.prop('multiple')) { 276 this.dropdown.addClass('jcf-compact-multiple'); 277 } 278 279 // set initial styles for dropdown in body 280 if (this.options.fakeDropInBody) { 281 this.dropdown.css({ 282 position: 'absolute', 283 top: -9999 284 }); 285 } 286 287 // create new select list instance 288 this.list = new SelectList({ 289 useHoverClass: true, 290 handleResize: false, 291 alwaysPreventMouseWheel: true, 292 maxVisibleItems: this.options.maxVisibleItems, 293 useCustomScroll: this.options.useCustomScroll, 294 holder: this.dropdown.find(this.options.dropContentSelector), 295 multipleSelectWithoutKey: this.realElement.prop('multiple'), 296 element: this.realElement 297 }); 298 $(this.list).on({ 299 select: this.onSelect, 300 press: this.onSelectDropPress, 301 release: this.onSelectDropRelease 302 }); 303 }, 304 repositionDropdown: function() { 305 var selectOffset = this.fakeElement.offset(), 306 selectWidth = this.fakeElement.outerWidth(), 307 selectHeight = this.fakeElement.outerHeight(), 308 dropHeight = this.dropdown.css('width', selectWidth).outerHeight(), 309 winScrollTop = this.win.scrollTop(), 310 winHeight = this.win.height(), 311 calcTop, calcLeft, bodyOffset, needFlipDrop = false; 312 313 // check flip drop position 314 if (selectOffset.top + selectHeight + dropHeight > winScrollTop + winHeight && selectOffset.top - dropHeight > winScrollTop) { 315 needFlipDrop = true; 316 } 317 318 if (this.options.fakeDropInBody) { 319 bodyOffset = this.fakeDropTarget.css('position') !== 'static' ? this.fakeDropTarget.offset().top : 0; 320 if (this.options.flipDropToFit && needFlipDrop) { 321 // calculate flipped dropdown position 322 calcLeft = selectOffset.left; 323 calcTop = selectOffset.top - dropHeight - bodyOffset; 324 } else { 325 // calculate default drop position 326 calcLeft = selectOffset.left; 327 calcTop = selectOffset.top + selectHeight - bodyOffset; 328 } 329 330 // update drop styles 331 this.dropdown.css({ 332 width: selectWidth, 333 left: calcLeft, 334 top: calcTop 335 }); 336 } 337 338 // refresh flipped class 339 this.dropdown.add(this.fakeElement).toggleClass(this.options.flipDropClass, this.options.flipDropToFit && needFlipDrop); 340 }, 341 showDropdown: function() { 342 // do not show empty custom dropdown 343 if (!this.realElement.prop('options').length) { 344 return; 345 } 346 347 // create options list if not created 348 if (!this.dropdown) { 349 this.createDropdown(); 350 } 351 352 // show dropdown 353 this.dropActive = true; 354 this.dropdown.appendTo(this.fakeDropTarget); 355 this.fakeElement.addClass(this.options.dropActiveClass); 356 this.refreshSelectedText(); 357 this.repositionDropdown(); 358 this.list.setScrollTop(this.savedScrollTop); 359 this.list.refresh(); 360 361 // add temporary event handlers 362 this.win.on('resize', this.onResize); 363 this.doc.on('jcf-pointerdown', this.onOutsideClick); 364 }, 365 hideDropdown: function() { 366 if (this.dropdown) { 367 this.savedScrollTop = this.list.getScrollTop(); 368 this.fakeElement.removeClass(this.options.dropActiveClass + ' ' + this.options.flipDropClass); 369 this.dropdown.removeClass(this.options.flipDropClass).detach(); 370 this.doc.off('jcf-pointerdown', this.onOutsideClick); 371 this.win.off('resize', this.onResize); 372 this.dropActive = false;
373 if (this.selectOpenedByEvent === 'touch') { 374 this.onBlur(); 375 } 376 } 377 }, 378 toggleDropdown: function() { 379 if (this.dropActive) { 380 this.hideDropdown(); 381 } else { 382 this.showDropdown(); 383 } 384 }, 385 refreshSelectedText: function() { 386 // redraw selected area 387 var selectedIndex = this.realElement.prop('selectedIndex'), 388 selectedOption = this.realElement.prop('options')[selectedIndex], 389 selectedOptionImage = selectedOption ? selectedOption.getAttribute('data-image') : null, 390 selectedOptionText = '', 391 selectedOptionClasses, 392 self = this; 393 394 if (this.realElement.prop('multiple')) { 395 $.each(this.realElement.prop('options'), function(index, option) { 396 if (option.selected) { 397 selectedOptionText += (selectedOptionText ? ', ' : '') + option.innerHTML; 398 } 399 }); 400 if (!selectedOptionText) { 401 selectedOptionText = self.realElement.attr('placeholder') || ''; 402 } 403 this.selectText.removeAttr('class').html(selectedOptionText); 404 } else if (!selectedOption) { 405 if (this.selectImage) { 406 this.selectImage.hide(); 407 } 408 this.selectText.removeAttr('class').empty(); 409 } else if (this.currentSelectedText !== selectedOption.innerHTML || this.currentSelectedImage !== selectedOptionImage) { 410 selectedOptionClasses = getPrefixedClasses(selectedOption.className, this.options.optionClassPrefix); 411 this.selectText.attr('class', selectedOptionClasses).html(selectedOption.innerHTML); 412 413 if (selectedOptionImage) { 414 if (!this.selectImage) { 415 this.selectImage = $('<img>').prependTo(this.selectTextContainer).hide(); 416 } 417 this.selectImage.attr('src', selectedOptionImage).show(); 418 } else if (this.selectImage) { 419 this.selectImage.hide(); 420 } 421 422 this.currentSelectedText = selectedOption.innerHTML; 423 this.currentSelectedImage = selectedOptionImage; 424 } 425 }, 426 refresh: function() { 427 // refresh fake select visibility 428 if (this.realElement.prop('style').display === 'none') { 429 this.fakeElement.hide(); 430 } else { 431 this.fakeElement.show(); 432 } 433 434 // refresh selected text 435 this.refreshSelectedText(); 436 437 // handle disabled state 438 this.fakeElement.toggleClass(this.options.disabledClass, this.realElement.is(':disabled')); 439 }, 440 destroy: function() { 441 // restore structure 442 if (this.options.wrapNative) { 443 this.realElement.insertBefore(this.fakeElement).css({ 444 position: '', 445 height: '', 446 width: '' 447 }).removeClass(this.options.resetAppearanceClass); 448 } else { 449 this.realElement.removeClass(this.options.hiddenClass); 450 if (this.realElement.is('[jcf-size]')) { 451 this.realElement.removeAttr('size jcf-size'); 452 } 453 } 454 455 // removing element will also remove its event handlers 456 this.fakeElement.remove(); 457 458 // remove other event handlers 459 this.doc.off('jcf-pointerup', this.onSelectAreaRelease); 460 this.realElement.off({ 461 focus: this.onFocus 462 }); 463 } 464 }); 465 466 // listbox module 467 function ListBox(options) { 468 this.options = $.extend({ 469 wrapNative: true, 470 useCustomScroll: true, 471 fakeStructure: '<span class="jcf-list-box"><span class="jcf-list-wrapper"></span></span>', 472 selectClassPrefix: 'jcf-select-', 473 listHolder: '.jcf-list-wrapper' 474 }, options); 475 this.init(); 476 } 477 $.extend(ListBox.prototype, { 478 init: function() { 479 this.bindHandlers(); 480 this.initStructure(); 481 this.attachEvents(); 482 }, 483 initStructure: function() { 484 this.realElement = $(this.options.element); 485 this.fakeElement = $(this.options.fakeStructure).insertAfter(this.realElement); 486 this.listHolder = this.fakeElement.find(this.options.listHolder); 487 makeUnselectable(this.fakeElement); 488 489 // copy classes from original select 490 this.fakeElement.addClass(getPrefixedClasses(this.realElement.prop('className'), this.options.selectClassPrefix)); 491 this.realElement.addClass(this.options.hiddenClass); 492 493 this.list = new SelectList({ 494 useCustomScroll: this.options.useCustomScroll, 495 holder: this.listHolder, 496 selectOnClick: false, 497 element: this.realElement 498 }); 499 }, 500 attachEvents: function() { 501 // delayed refresh handler 502 var self = this; 503 this.delayedRefresh = function(e) { 504 if (e && e.which === 16) { 505 // ignore SHIFT key 506 return; 507 } else { 508 clearTimeout(self.refreshTimer); 509 self.refreshTimer = setTimeout(function() { 510 self.refresh(); 511 self.list.scrollToActiveOption(); 512 }, 1); 513 } 514 }; 515 516 // other event handlers 517 this.realElement.on({ 518 focus: this.onFocus, 519 click: this.delayedRefresh, 520 keydown: this.delayedRefresh 521 }); 522 523 // select list event handlers 524 $(this.list).on({ 525 select: this.onSelect, 526 press: this.onFakeOptionsPress, 527 release: this.onFakeOptionsRelease 528 }); 529 }, 530 onFakeOptionsPress: function(e, pointerEvent) { 531 this.pressedFlag = true; 532 if (pointerEvent.pointerType === 'mouse') { 533 this.realElement.focus(); 534 } 535 }, 536 onFakeOptionsRelease: function(e, pointerEvent) { 537 this.pressedFlag = false;
538 if (pointerEvent.pointerType === 'mouse') { 539 this.realElement.focus(); 540 } 541 }, 542 onSelect: function() { 543 this.fireNativeEvent(this.realElement, 'change'); 544 this.fireNativeEvent(this.realElement, 'click'); 545 }, 546 onFocus: function() { 547 if (!this.pressedFlag || !this.focusedFlag) { 548 this.fakeElement.addClass(this.options.focusClass); 549 this.realElement.on('blur', this.onBlur); 550 this.focusedFlag = true; 551 } 552 }, 553 onBlur: function() { 554 if (!this.pressedFlag) { 555 this.fakeElement.removeClass(this.options.focusClass); 556 this.realElement.off('blur', this.onBlur); 557 this.focusedFlag = false; 558 } 559 }, 560 refresh: function() { 561 this.fakeElement.toggleClass(this.options.disabledClass, this.realElement.is(':disabled')); 562 this.list.refresh(); 563 }, 564 destroy: function() { 565 this.list.destroy(); 566 this.realElement.insertBefore(this.fakeElement).removeClass(this.options.hiddenClass); 567 this.fakeElement.remove(); 568 } 569 }); 570 571 // options list module 572 function SelectList(options) { 573 this.options = $.extend({ 574 holder: null, 575 maxVisibleItems: 10, 576 selectOnClick: true, 577 useHoverClass: false, 578 useCustomScroll: false, 579 handleResize: true, 580 multipleSelectWithoutKey: false, 581 alwaysPreventMouseWheel: false, 582 indexAttribute: 'data-index', 583 cloneClassPrefix: 'jcf-option-', 584 containerStructure: '<span class="jcf-list"><span class="jcf-list-content"></span></span>', 585 containerSelector: '.jcf-list-content', 586 captionClass: 'jcf-optgroup-caption', 587 disabledClass: 'jcf-disabled', 588 optionClass: 'jcf-option', 589 groupClass: 'jcf-optgroup', 590 hoverClass: 'jcf-hover', 591 selectedClass: 'jcf-selected', 592 scrollClass: 'jcf-scroll-active' 593 }, options); 594 this.init(); 595 } 596 $.extend(SelectList.prototype, { 597 init: function() { 598 this.initStructure(); 599 this.refreshSelectedClass(); 600 this.attachEvents(); 601 }, 602 initStructure: function() { 603 this.element = $(this.options.element); 604 this.indexSelector = '[' + this.options.indexAttribute + ']'; 605 this.container = $(this.options.containerStructure).appendTo(this.options.holder); 606 this.listHolder = this.container.find(this.options.containerSelector); 607 this.lastClickedIndex = this.element.prop('selectedIndex'); 608 this.rebuildList(); 609 }, 610 attachEvents: function() { 611 this.bindHandlers(); 612 this.listHolder.on('jcf-pointerdown', this.indexSelector, this.onItemPress); 613 this.listHolder.on('jcf-pointerdown', this.onPress); 614 615 if (this.options.useHoverClass) { 616 this.listHolder.on('jcf-pointerover', this.indexSelector, this.onHoverItem); 617 } 618 }, 619 onPress: function(e) { 620 $(this).trigger('press', e); 621 this.listHolder.on('jcf-pointerup', this.onRelease); 622 }, 623 onRelease: function(e) { 624 $(this).trigger('release', e); 625 this.listHolder.off('jcf-pointerup', this.onRelease); 626 }, 627 onHoverItem: function(e) { 628 var hoverIndex = parseFloat(e.currentTarget.getAttribute(this.options.indexAttribute)); 629 this.fakeOptions.removeClass(this.options.hoverClass).eq(hoverIndex).addClass(this.options.hoverClass); 630 }, 631 onItemPress: function(e) { 632 if (e.pointerType === 'touch' || this.options.selectOnClick) { 633 // select option after "click" 634 this.tmpListOffsetTop = this.list.offset().top; 635 this.listHolder.on('jcf-pointerup', this.indexSelector, this.onItemRelease); 636 } else { 637 // select option immediately 638 this.onSelectItem(e); 639 } 640 }, 641 onItemRelease: function(e) { 642 // remove event handlers and temporary data 643 this.listHolder.off('jcf-pointerup', this.indexSelector, this.onItemRelease); 644 645 // simulate item selection 646 if (this.tmpListOffsetTop === this.list.offset().top) { 647 this.listHolder.on('click', this.indexSelector, { savedPointerType: e.pointerType }, this.onSelectItem); 648 } 649 delete this.tmpListOffsetTop; 650 }, 651 onSelectItem: function(e) { 652 var clickedIndex = parseFloat(e.currentTarget.getAttribute(this.options.indexAttribute)), 653 pointerType = e.data && e.data.savedPointerType || e.pointerType || 'mouse', 654 range; 655 656 // remove click event handler 657 this.listHolder.off('click', this.indexSelector, this.onSelectItem); 658 659 // ignore clicks on disabled options 660 if (e.button > 1 || this.realOptions[clickedIndex].disabled) { 661 return; 662 } 663 664 if (this.element.prop('multiple')) { 665 if (e.metaKey || e.ctrlKey || pointerType === 'touch' || this.options.multipleSelectWithoutKey) { 666 // if CTRL/CMD pressed or touch devices - toggle selected option 667 this.realOptions[clickedIndex].selected = !this.realOptions[clickedIndex].selected; 668 } else if (e.shiftKey) { 669 // if SHIFT pressed - update selection 670 range = [this.lastClickedIndex, clickedIndex].sort(function(a, b) { 671 return a - b; 672 }); 673 this.realOptions.each(function(index, option) { 674 option.selected = (index >= range[0] && index <= range[1]); 675 }); 676 } else { 677 // set single selected index 678 this.element.prop('selectedIndex', clickedIndex); 679 } 680 } else { 681 this.element.prop('selectedIndex', clickedIndex); 682 } 683 684 // save last clicked option 685 if (!e.shiftKey) { 686 this.lastClickedIndex = clickedIndex; 687 } 688 689 // refresh classes 690 this.refreshSelectedClass(); 691 692 // scroll to active item in desktop browsers 693 if (pointerType === 'mouse') { 694 this.scrollToActiveOption(); 695 } 696 697 // make callback when item selected 698 $(this).trigger('select'); 699 }, 700 rebuildList: function() { 701 // rebuild options 702 var self = this, 703 rootElement = this.element[0]; 704 705 // recursively create fake options 706 this.storedSelectHTML = rootElement.innerHTML; 707 this.optionIndex = 0; 708 this.list = $(this.createOptionsList(rootElement)); 709 this.listHolder.empty().append(this.list); 710 this.realOptions = this.element.find('option'); 711 this.fakeOptions = this.list.find(this.indexSelector); 712 this.fakeListItems = this.list.find('.' + this.options.captionClass + ',' + this.indexSelector); 713 delete this.optionIndex; 714 715 // detect max visible items 716 var maxCount = this.options.maxVisibleItems, 717 sizeValue = this.element.prop('size'); 718 if (sizeValue > 1 && !this.element.is('[jcf-size]')) { 719 maxCount = sizeValue; 720 } 721 722 // handle scrollbar 723 var needScrollBar = this.fakeOptions.length > maxCount; 724 this.container.toggleClass(this.options.scrollClass, needScrollBar); 725 if (needScrollBar) { 726 // change max-height 727 this.listHolder.css({ 728 maxHeight: this.getOverflowHeight(maxCount), 729 overflow: 'auto' 730 }); 731 732 if (this.options.useCustomScroll && jcf.modules.Scrollable) { 733 // add custom scrollbar if specified in options 734 jcf.replace(this.listHolder, 'Scrollable', { 735 handleResize: this.options.handleResize, 736 alwaysPreventMouseWheel: this.options.alwaysPreventMouseWheel 737 }); 738 return; 739 } 740 } 741 742 // disable edge wheel scrolling 743 if (this.options.alwaysPreventMouseWheel) { 744 this.preventWheelHandler = function(e) { 745 var currentScrollTop = self.listHolder.scrollTop(), 746 maxScrollTop = self.listHolder.prop('scrollHeight') - self.listHolder.innerHe
746ight(); 747 748 // check edge cases 749 if ((currentScrollTop <= 0 && e.deltaY < 0) || (currentScrollTop >= maxScrollTop && e.deltaY > 0)) { 750 e.preventDefault(); 751 } 752 }; 753 this.listHolder.on('jcf-mousewheel', this.preventWheelHandler); 754 } 755 }, 756 refreshSelectedClass: function() { 757 var self = this, 758 selectedItem, 759 isMultiple = this.element.prop('multiple'), 760 selectedIndex = this.element.prop('selectedIndex'); 761 762 if (isMultiple) { 763 this.realOptions.each(function(index, option) { 764 self.fakeOptions.eq(index).toggleClass(self.options.selectedClass, !!option.selected); 765 }); 766 } else { 767 this.fakeOptions.removeClass(this.options.selectedClass + ' ' + this.options.hoverClass); 768 selectedItem = this.fakeOptions.eq(selectedIndex).addClass(this.options.selectedClass); 769 if (this.options.useHoverClass) { 770 selectedItem.addClass(this.options.hoverClass); 771 } 772 } 773 }, 774 scrollToActiveOption: function() { 775 // scroll to target option 776 var targetOffset = this.getActiveOptionOffset(); 777 if (typeof targetOffset === 'number') { 778 this.listHolder.prop('scrollTop', targetOffset); 779 } 780 }, 781 getSelectedIndexRange: function() { 782 var firstSelected = -1, lastSelected = -1; 783 this.realOptions.each(function(index, option) { 784 if (option.selected) { 785 if (firstSelected < 0) { 786 firstSelected = index; 787 } 788 lastSelected = index; 789 } 790 }); 791 return [firstSelected, lastSelected]; 792 }, 793 getChangedSelectedIndex: function() { 794 var selectedIndex = this.element.prop('selectedIndex'), 795 targetIndex; 796 797 if (this.element.prop('multiple')) { 798 // multiple selects handling 799 if (!this.previousRange) { 800 this.previousRange = [selectedIndex, selectedIndex]; 801 } 802 this.currentRange = this.getSelectedIndexRange(); 803 targetIndex = this.currentRange[this.currentRange[0] !== this.previousRange[0] ? 0 : 1]; 804 this.previousRange = this.currentRange; 805 return targetIndex; 806 } else { 807 // single choice selects handling 808 return selectedIndex; 809 } 810 }, 811 getActiveOptionOffset: function() { 812 // calc values 813 var dropHeight = this.listHolder.height(), 814 dropScrollTop = this.listHolder.prop('scrollTop'), 815 currentIndex = this.getChangedSelectedIndex(), 816 fakeOption = this.fakeOptions.eq(currentIndex), 817 fakeOptionOffset = fakeOption.offset().top - this.list.offset().top, 818 fakeOptionHeight = fakeOption.innerHeight(); 819 820 // scroll list 821 if (fakeOptionOffset + fakeOptionHeight >= dropScrollTop + dropHeight) { 822 // scroll down (always scroll to option) 823 return fakeOptionOffset - dropHeight + fakeOptionHeight; 824 } else if (fakeOptionOffset < dropScrollTop) { 825 // scroll up to option 826 return fakeOptionOffset; 827 } 828 }, 829 getOverflowHeight: function(sizeValue) { 830 var item = this.fakeListItems.eq(sizeValue - 1), 831 listOffset = this.list.offset().top, 832 itemOffset = item.offset().top, 833 itemHeight = item.innerHeight(); 834 835 return itemOffset + itemHeight - listOffset; 836 }, 837 getScrollTop: function() { 838 return this.listHolder.scrollTop(); 839 }, 840 setScrollTop: function(value) { 841 this.listHolder.scrollTop(value); 842 }, 843 createOption: function(option) { 844 var newOption = document.createElement('span'); 845 newOption.className = this.options.optionClass; 846 newOption.innerHTML = option.innerHTML; 847 newOption.setAttribute(this.options.indexAttribute, this.optionIndex++); 848 849 var optionImage, optionImageSrc = option.getAttribute('data-image'); 850 if (optionImageSrc) { 851 optionImage = document.createElement('img'); 852 optionImage.src = optionImageSrc; 853 newOption.insertBefore(optionImage, newOption.childNodes[0]); 854 } 855 if (option.disabled) { 856 newOption.className += ' ' + this.options.disabledClass; 857 } 858 if (option.className) { 859 newOption.className += ' ' + getPrefixedClasses(option.className, this.options.cloneClassPrefix); 860 } 861 return newOption; 862 }, 863 createOptGroup: function(optgroup) { 864 var optGroupContainer = document.createElement('span'), 865 optGroupName = optgroup.getAttribute('label'), 866 optGroupCaption, optGroupList; 867 868 // create caption 869 optGroupCaption = document.createElement('span'); 870 optGroupCaption.className = this.options.captionClass; 871 optGroupCaption.innerHTML = optGroupName; 872 optGroupContainer.appendChild(optGroupCaption); 873 874 // create list of options 875 if (optgroup.children.length) { 876 optGroupList = this.createOptionsList(optgroup); 877 optGroupContainer.appendChild(optGroupList); 878 } 879 880 optGroupContainer.className = this.options.groupClass; 881 return optGroupContainer; 882 }, 883 createOptionContainer: function() { 884 var optionContainer = document.createElement('li'); 885 return optionContainer; 886 }, 887 createOptionsList: function(container) { 888 var self = this, 889 list = document.createElement('ul'); 890 891 $.each(container.children, function(index, currentNode) { 892 var item = self.createOptionContainer(currentNode), 893 newNode; 894 895 switch (currentNode.tagName.toLowerCase()) { 896 case 'option': newNode = self.createOption(currentNode); break; 897 case 'optgroup': newNode = self.createOptGroup(currentNode); break; 898 } 899 list.appendChild(item).appendChild(newNode); 900 }); 901 return list; 902 }, 903 refresh: function() { 904 // check for select innerHTML changes 905 if (this.storedSelectHTML !== this.element.prop('innerHTML')) { 906 this.rebuildList(); 907 } 908 909 // refresh custom scrollbar 910 var scrollInstance = jcf.getInstance(this.listHolder); 911 if (scrollInstance) { 912 scrollInstance.refresh(); 913 } 914 915 // refresh selectes classes 916 this.refreshSelectedClass(); 917 }, 918 destroy: function() { 919 this.listHolder.off('jcf-mousewheel', this.preventWheelHandler); 920 this.listHolder.off('jcf-pointerdown', this.indexSelector, this.onSelectItem); 921 this.listHolder.off('jcf-pointerover', this.indexSelector, this.onHoverItem); 922 this.listHolder.off('jcf-pointerdown', this.onPress); 923 } 924 }); 925 926 // helper functions
927 var getPrefixedClasses = function(className, prefixToAdd) { 928 return className ? className.replace(/[\s]*([\S]+)+[\s]*/gi, prefixToAdd + '$1 ') : ''; 929 }; 930 var makeUnselectable = (function() { 931 var unselectableClass = jcf.getOptions().unselectableClass; 932 function preventHandler(e) { 933 e.preventDefault(); 934 } 935 return function(node) { 936 node.addClass(unselectableClass).on('selectstart', preventHandler); 937 }; 938 }()); 939 940 return module; 941});
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.