PageSourceSearch

https://www.sumitomo-rd.co.jp/common/scripts/jcf.select.js

js sumitomo-rd.co.jp collected 2026-10-02 10:36:01 UTC 30,868 bytes, 962 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.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.