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