PageSourceSearch

https://iot.ru/bitrix/templates/.default/js/libs/jcf.select.js

js iot.ru collected 2026-10-02 07:11:57 UTC 29,420 bytes, 941 lines download raw bytes

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.