961 lines
29 KiB
JavaScript
961 lines
29 KiB
JavaScript
/*!
|
|
* JavaScript Custom Forms : Select Module
|
|
*
|
|
* Copyright 2014-2015 PSD2HTML - http://psd2html.com/jcf
|
|
* Released under the MIT license (LICENSE.txt)
|
|
*
|
|
* Version: 1.2.1
|
|
*/
|
|
|
|
(function(jcf) {
|
|
|
|
jcf.addModule(function($, window) {
|
|
'use strict';
|
|
|
|
var module = {
|
|
name: 'Select',
|
|
selector: 'select',
|
|
options: {
|
|
element: null,
|
|
multipleCompactStyle: false
|
|
},
|
|
plugins: {
|
|
ListBox: ListBox,
|
|
ComboBox: ComboBox,
|
|
SelectList: SelectList
|
|
},
|
|
matchElement: function(element) {
|
|
return element.is('select');
|
|
},
|
|
init: function() {
|
|
this.element = $(this.options.element);
|
|
this.createInstance();
|
|
},
|
|
isListBox: function() {
|
|
return this.element.is('[size]:not([jcf-size]), [multiple]');
|
|
},
|
|
createInstance: function() {
|
|
if (this.instance) {
|
|
this.instance.destroy();
|
|
}
|
|
if (this.isListBox() && !this.options.multipleCompactStyle) {
|
|
this.instance = new ListBox(this.options);
|
|
} else {
|
|
this.instance = new ComboBox(this.options);
|
|
}
|
|
},
|
|
refresh: function() {
|
|
var typeMismatch = (this.isListBox() && this.instance instanceof ComboBox) ||
|
|
(!this.isListBox() && this.instance instanceof ListBox);
|
|
|
|
if (typeMismatch) {
|
|
this.createInstance();
|
|
} else {
|
|
this.instance.refresh();
|
|
}
|
|
},
|
|
destroy: function() {
|
|
this.instance.destroy();
|
|
}
|
|
};
|
|
|
|
// combobox module
|
|
function ComboBox(options) {
|
|
this.options = $.extend({
|
|
wrapNative: true,
|
|
wrapNativeOnMobile: true,
|
|
fakeDropInBody: true,
|
|
useCustomScroll: true,
|
|
flipDropToFit: true,
|
|
maxVisibleItems: 10,
|
|
fakeAreaStructure: '<span class="jcf-select"><span class="jcf-select-text"></span><span class="jcf-select-opener"></span></span>',
|
|
fakeDropStructure: '<div class="jcf-select-drop"><div class="jcf-select-drop-content"></div></div>',
|
|
optionClassPrefix: 'jcf-option-',
|
|
selectClassPrefix: 'jcf-select-',
|
|
dropContentSelector: '.jcf-select-drop-content',
|
|
selectTextSelector: '.jcf-select-text',
|
|
dropActiveClass: 'jcf-drop-active',
|
|
flipDropClass: 'jcf-drop-flipped'
|
|
}, options);
|
|
this.init();
|
|
}
|
|
$.extend(ComboBox.prototype, {
|
|
init: function() {
|
|
this.initStructure();
|
|
this.bindHandlers();
|
|
this.attachEvents();
|
|
this.refresh();
|
|
},
|
|
initStructure: function() {
|
|
// prepare structure
|
|
this.win = $(window);
|
|
this.doc = $(document);
|
|
this.realElement = $(this.options.element);
|
|
this.fakeElement = $(this.options.fakeAreaStructure).insertAfter(this.realElement);
|
|
this.selectTextContainer = this.fakeElement.find(this.options.selectTextSelector);
|
|
this.selectText = $('<span></span>').appendTo(this.selectTextContainer);
|
|
makeUnselectable(this.fakeElement);
|
|
|
|
// copy classes from original select
|
|
this.fakeElement.addClass(getPrefixedClasses(this.realElement.prop('className'), this.options.selectClassPrefix));
|
|
|
|
// handle compact multiple style
|
|
if (this.realElement.prop('multiple')) {
|
|
this.fakeElement.addClass('jcf-compact-multiple');
|
|
}
|
|
|
|
// detect device type and dropdown behavior
|
|
if (this.options.isMobileDevice && this.options.wrapNativeOnMobile && !this.options.wrapNative) {
|
|
this.options.wrapNative = true;
|
|
}
|
|
|
|
if (this.options.wrapNative) {
|
|
// wrap native select inside fake block
|
|
this.realElement.prependTo(this.fakeElement).css({
|
|
position: 'absolute',
|
|
height: '100%',
|
|
width: '100%'
|
|
}).addClass(this.options.resetAppearanceClass);
|
|
} else {
|
|
// just hide native select
|
|
this.realElement.addClass(this.options.hiddenClass);
|
|
this.fakeElement.attr('title', this.realElement.attr('title'));
|
|
this.fakeDropTarget = this.options.fakeDropInBody ? $('body') : this.fakeElement;
|
|
}
|
|
},
|
|
attachEvents: function() {
|
|
// delayed refresh handler
|
|
var self = this;
|
|
this.delayedRefresh = function() {
|
|
setTimeout(function() {
|
|
self.refresh();
|
|
if (self.list) {
|
|
self.list.refresh();
|
|
self.list.scrollToActiveOption();
|
|
}
|
|
}, 1);
|
|
};
|
|
|
|
// native dropdown event handlers
|
|
if (this.options.wrapNative) {
|
|
this.realElement.on({
|
|
focus: this.onFocus,
|
|
change: this.onChange,
|
|
click: this.onChange,
|
|
keydown: this.delayedRefresh
|
|
});
|
|
} else {
|
|
// custom dropdown event handlers
|
|
this.realElement.on({
|
|
focus: this.onFocus,
|
|
change: this.onChange,
|
|
keydown: this.onKeyDown
|
|
});
|
|
this.fakeElement.on({
|
|
'jcf-pointerdown': this.onSelectAreaPress
|
|
});
|
|
}
|
|
},
|
|
onKeyDown: function(e) {
|
|
if (e.which === 13) {
|
|
this.toggleDropdown();
|
|
} else if (this.dropActive) {
|
|
this.delayedRefresh();
|
|
}
|
|
},
|
|
onChange: function() {
|
|
this.refresh();
|
|
},
|
|
onFocus: function() {
|
|
if (!this.pressedFlag || !this.focusedFlag) {
|
|
this.fakeElement.addClass(this.options.focusClass);
|
|
this.realElement.on('blur', this.onBlur);
|
|
this.toggleListMode(true);
|
|
this.focusedFlag = true;
|
|
}
|
|
},
|
|
onBlur: function() {
|
|
if (!this.pressedFlag) {
|
|
this.fakeElement.removeClass(this.options.focusClass);
|
|
this.realElement.off('blur', this.onBlur);
|
|
this.toggleListMode(false);
|
|
this.focusedFlag = false;
|
|
}
|
|
},
|
|
onResize: function() {
|
|
if (this.dropActive) {
|
|
this.hideDropdown();
|
|
}
|
|
},
|
|
onSelectDropPress: function() {
|
|
this.pressedFlag = true;
|
|
},
|
|
onSelectDropRelease: function(e, pointerEvent) {
|
|
this.pressedFlag = false;
|
|
if (pointerEvent.pointerType === 'mouse') {
|
|
this.realElement.focus();
|
|
}
|
|
},
|
|
onSelectAreaPress: function(e) {
|
|
// skip click if drop inside fake element or real select is disabled
|
|
var dropClickedInsideFakeElement = !this.options.fakeDropInBody && $(e.target).closest(this.dropdown).length;
|
|
if (dropClickedInsideFakeElement || e.button > 1 || this.realElement.is(':disabled')) {
|
|
return;
|
|
}
|
|
|
|
// toggle dropdown visibility
|
|
this.selectOpenedByEvent = e.pointerType;
|
|
this.toggleDropdown();
|
|
|
|
// misc handlers
|
|
if (!this.focusedFlag) {
|
|
if (e.pointerType === 'mouse') {
|
|
this.realElement.focus();
|
|
} else {
|
|
this.onFocus(e);
|
|
}
|
|
}
|
|
this.pressedFlag = true;
|
|
this.fakeElement.addClass(this.options.pressedClass);
|
|
this.doc.on('jcf-pointerup', this.onSelectAreaRelease);
|
|
},
|
|
onSelectAreaRelease: function(e) {
|
|
if (this.focusedFlag && e.pointerType === 'mouse') {
|
|
this.realElement.focus();
|
|
}
|
|
this.pressedFlag = false;
|
|
this.fakeElement.removeClass(this.options.pressedClass);
|
|
this.doc.off('jcf-pointerup', this.onSelectAreaRelease);
|
|
},
|
|
onOutsideClick: function(e) {
|
|
var target = $(e.target),
|
|
clickedInsideSelect = target.closest(this.fakeElement).length || target.closest(this.dropdown).length;
|
|
|
|
if (!clickedInsideSelect) {
|
|
this.hideDropdown();
|
|
}
|
|
},
|
|
onSelect: function() {
|
|
this.refresh();
|
|
|
|
if (this.realElement.prop('multiple')) {
|
|
this.repositionDropdown();
|
|
} else {
|
|
this.hideDropdown();
|
|
}
|
|
|
|
this.fireNativeEvent(this.realElement, 'change');
|
|
},
|
|
toggleListMode: function(state) {
|
|
if (!this.options.wrapNative) {
|
|
if (state) {
|
|
// temporary change select to list to avoid appearing of native dropdown
|
|
this.realElement.attr({
|
|
size: 4,
|
|
'jcf-size': ''
|
|
});
|
|
} else {
|
|
// restore select from list mode to dropdown select
|
|
if (!this.options.wrapNative) {
|
|
this.realElement.removeAttr('size jcf-size');
|
|
}
|
|
}
|
|
}
|
|
},
|
|
createDropdown: function() {
|
|
// destroy previous dropdown if needed
|
|
if (this.dropdown) {
|
|
this.list.destroy();
|
|
this.dropdown.remove();
|
|
}
|
|
|
|
// create new drop container
|
|
this.dropdown = $(this.options.fakeDropStructure).appendTo(this.fakeDropTarget);
|
|
this.dropdown.addClass(getPrefixedClasses(this.realElement.prop('className'), this.options.selectClassPrefix));
|
|
makeUnselectable(this.dropdown);
|
|
|
|
// handle compact multiple style
|
|
if (this.realElement.prop('multiple')) {
|
|
this.dropdown.addClass('jcf-compact-multiple');
|
|
}
|
|
|
|
// set initial styles for dropdown in body
|
|
if (this.options.fakeDropInBody) {
|
|
this.dropdown.css({
|
|
position: 'absolute',
|
|
top: -9999
|
|
});
|
|
}
|
|
|
|
// create new select list instance
|
|
this.list = new SelectList({
|
|
useHoverClass: true,
|
|
handleResize: false,
|
|
alwaysPreventMouseWheel: true,
|
|
maxVisibleItems: this.options.maxVisibleItems,
|
|
useCustomScroll: this.options.useCustomScroll,
|
|
holder: this.dropdown.find(this.options.dropContentSelector),
|
|
multipleSelectWithoutKey: this.realElement.prop('multiple'),
|
|
element: this.realElement
|
|
});
|
|
$(this.list).on({
|
|
select: this.onSelect,
|
|
press: this.onSelectDropPress,
|
|
release: this.onSelectDropRelease
|
|
});
|
|
},
|
|
repositionDropdown: function() {
|
|
var selectOffset = this.fakeElement.offset(),
|
|
selectWidth = this.fakeElement.outerWidth(),
|
|
selectHeight = this.fakeElement.outerHeight(),
|
|
dropHeight = this.dropdown.css('width', selectWidth).outerHeight(),
|
|
winScrollTop = this.win.scrollTop(),
|
|
winHeight = this.win.height(),
|
|
calcTop, calcLeft, bodyOffset, needFlipDrop = false;
|
|
|
|
// check flip drop position
|
|
if (selectOffset.top + selectHeight + dropHeight > winScrollTop + winHeight && selectOffset.top - dropHeight > winScrollTop) {
|
|
needFlipDrop = true;
|
|
}
|
|
|
|
if (this.options.fakeDropInBody) {
|
|
bodyOffset = this.fakeDropTarget.css('position') !== 'static' ? this.fakeDropTarget.offset().top : 0;
|
|
if (this.options.flipDropToFit && needFlipDrop) {
|
|
// calculate flipped dropdown position
|
|
calcLeft = selectOffset.left;
|
|
calcTop = selectOffset.top - dropHeight - bodyOffset;
|
|
} else {
|
|
// calculate default drop position
|
|
calcLeft = selectOffset.left;
|
|
calcTop = selectOffset.top + selectHeight - bodyOffset;
|
|
}
|
|
|
|
// update drop styles
|
|
this.dropdown.css({
|
|
width: selectWidth,
|
|
left: calcLeft,
|
|
top: calcTop
|
|
});
|
|
}
|
|
|
|
// refresh flipped class
|
|
this.dropdown.add(this.fakeElement).toggleClass(this.options.flipDropClass, this.options.flipDropToFit && needFlipDrop);
|
|
},
|
|
showDropdown: function() {
|
|
// do not show empty custom dropdown
|
|
if (!this.realElement.prop('options').length) {
|
|
return;
|
|
}
|
|
|
|
// create options list if not created
|
|
if (!this.dropdown) {
|
|
this.createDropdown();
|
|
}
|
|
|
|
// show dropdown
|
|
this.dropActive = true;
|
|
this.dropdown.appendTo(this.fakeDropTarget);
|
|
this.fakeElement.addClass(this.options.dropActiveClass);
|
|
this.refreshSelectedText();
|
|
this.repositionDropdown();
|
|
this.list.setScrollTop(this.savedScrollTop);
|
|
this.list.refresh();
|
|
|
|
// add temporary event handlers
|
|
this.win.on('resize', this.onResize);
|
|
this.doc.on('jcf-pointerdown', this.onOutsideClick);
|
|
},
|
|
hideDropdown: function() {
|
|
if (this.dropdown) {
|
|
this.savedScrollTop = this.list.getScrollTop();
|
|
this.fakeElement.removeClass(this.options.dropActiveClass + ' ' + this.options.flipDropClass);
|
|
this.dropdown.removeClass(this.options.flipDropClass).detach();
|
|
this.doc.off('jcf-pointerdown', this.onOutsideClick);
|
|
this.win.off('resize', this.onResize);
|
|
this.dropActive = false;
|
|
if (this.selectOpenedByEvent === 'touch') {
|
|
this.onBlur();
|
|
}
|
|
}
|
|
},
|
|
toggleDropdown: function() {
|
|
if (this.dropActive) {
|
|
this.hideDropdown();
|
|
} else {
|
|
this.showDropdown();
|
|
}
|
|
},
|
|
refreshSelectedText: function() {
|
|
// redraw selected area
|
|
var selectedIndex = this.realElement.prop('selectedIndex'),
|
|
selectedOption = this.realElement.prop('options')[selectedIndex],
|
|
selectedOptionImage = selectedOption ? selectedOption.getAttribute('data-image') : null,
|
|
selectedOptionText = '',
|
|
selectedOptionClasses,
|
|
self = this;
|
|
|
|
if (this.realElement.prop('multiple')) {
|
|
$.each(this.realElement.prop('options'), function(index, option) {
|
|
if (option.selected) {
|
|
selectedOptionText += (selectedOptionText ? ', ' : '') + option.innerHTML;
|
|
}
|
|
});
|
|
if (!selectedOptionText) {
|
|
selectedOptionText = self.realElement.attr('placeholder') || '';
|
|
}
|
|
this.selectText.removeAttr('class').html(selectedOptionText);
|
|
} else if (!selectedOption) {
|
|
if (this.selectImage) {
|
|
this.selectImage.hide();
|
|
}
|
|
this.selectText.removeAttr('class').empty();
|
|
} else if (this.currentSelectedText !== selectedOption.innerHTML || this.currentSelectedImage !== selectedOptionImage) {
|
|
selectedOptionClasses = getPrefixedClasses(selectedOption.className, this.options.optionClassPrefix);
|
|
this.selectText.attr('class', selectedOptionClasses).html(selectedOption.innerHTML);
|
|
|
|
if (selectedOptionImage) {
|
|
if (!this.selectImage) {
|
|
this.selectImage = $('<img>').prependTo(this.selectTextContainer).hide();
|
|
}
|
|
this.selectImage.attr('src', selectedOptionImage).show();
|
|
} else if (this.selectImage) {
|
|
this.selectImage.hide();
|
|
}
|
|
|
|
this.currentSelectedText = selectedOption.innerHTML;
|
|
this.currentSelectedImage = selectedOptionImage;
|
|
}
|
|
},
|
|
refresh: function() {
|
|
// refresh fake select visibility
|
|
if (this.realElement.prop('style').display === 'none') {
|
|
this.fakeElement.hide();
|
|
} else {
|
|
this.fakeElement.show();
|
|
}
|
|
|
|
// refresh selected text
|
|
this.refreshSelectedText();
|
|
|
|
// handle disabled state
|
|
this.fakeElement.toggleClass(this.options.disabledClass, this.realElement.is(':disabled'));
|
|
},
|
|
destroy: function() {
|
|
// restore structure
|
|
if (this.options.wrapNative) {
|
|
this.realElement.insertBefore(this.fakeElement).css({
|
|
position: '',
|
|
height: '',
|
|
width: ''
|
|
}).removeClass(this.options.resetAppearanceClass);
|
|
} else {
|
|
this.realElement.removeClass(this.options.hiddenClass);
|
|
if (this.realElement.is('[jcf-size]')) {
|
|
this.realElement.removeAttr('size jcf-size');
|
|
}
|
|
}
|
|
|
|
// removing element will also remove its event handlers
|
|
this.fakeElement.remove();
|
|
|
|
// remove other event handlers
|
|
this.doc.off('jcf-pointerup', this.onSelectAreaRelease);
|
|
this.realElement.off({
|
|
focus: this.onFocus
|
|
});
|
|
}
|
|
});
|
|
|
|
// listbox module
|
|
function ListBox(options) {
|
|
this.options = $.extend({
|
|
wrapNative: true,
|
|
useCustomScroll: true,
|
|
fakeStructure: '<span class="jcf-list-box"><span class="jcf-list-wrapper"></span></span>',
|
|
selectClassPrefix: 'jcf-select-',
|
|
listHolder: '.jcf-list-wrapper'
|
|
}, options);
|
|
this.init();
|
|
}
|
|
$.extend(ListBox.prototype, {
|
|
init: function() {
|
|
this.bindHandlers();
|
|
this.initStructure();
|
|
this.attachEvents();
|
|
},
|
|
initStructure: function() {
|
|
this.realElement = $(this.options.element);
|
|
this.fakeElement = $(this.options.fakeStructure).insertAfter(this.realElement);
|
|
this.listHolder = this.fakeElement.find(this.options.listHolder);
|
|
makeUnselectable(this.fakeElement);
|
|
|
|
// copy classes from original select
|
|
this.fakeElement.addClass(getPrefixedClasses(this.realElement.prop('className'), this.options.selectClassPrefix));
|
|
this.realElement.addClass(this.options.hiddenClass);
|
|
|
|
this.list = new SelectList({
|
|
useCustomScroll: this.options.useCustomScroll,
|
|
holder: this.listHolder,
|
|
selectOnClick: false,
|
|
element: this.realElement
|
|
});
|
|
},
|
|
attachEvents: function() {
|
|
// delayed refresh handler
|
|
var self = this;
|
|
this.delayedRefresh = function(e) {
|
|
if (e && (e.which === 16 || e.ctrlKey || e.metaKey || e.altKey)) {
|
|
// ignore modifier keys
|
|
return;
|
|
} else {
|
|
clearTimeout(self.refreshTimer);
|
|
self.refreshTimer = setTimeout(function() {
|
|
self.refresh();
|
|
self.list.scrollToActiveOption();
|
|
}, 1);
|
|
}
|
|
};
|
|
|
|
// other event handlers
|
|
this.realElement.on({
|
|
focus: this.onFocus,
|
|
click: this.delayedRefresh,
|
|
keydown: this.delayedRefresh
|
|
});
|
|
|
|
// select list event handlers
|
|
$(this.list).on({
|
|
select: this.onSelect,
|
|
press: this.onFakeOptionsPress,
|
|
release: this.onFakeOptionsRelease
|
|
});
|
|
},
|
|
onFakeOptionsPress: function(e, pointerEvent) {
|
|
this.pressedFlag = true;
|
|
if (pointerEvent.pointerType === 'mouse') {
|
|
this.realElement.focus();
|
|
}
|
|
},
|
|
onFakeOptionsRelease: function(e, pointerEvent) {
|
|
this.pressedFlag = false;
|
|
if (pointerEvent.pointerType === 'mouse') {
|
|
this.realElement.focus();
|
|
}
|
|
},
|
|
onSelect: function() {
|
|
this.fireNativeEvent(this.realElement, 'change');
|
|
this.fireNativeEvent(this.realElement, 'click');
|
|
},
|
|
onFocus: function() {
|
|
if (!this.pressedFlag || !this.focusedFlag) {
|
|
this.fakeElement.addClass(this.options.focusClass);
|
|
this.realElement.on('blur', this.onBlur);
|
|
this.focusedFlag = true;
|
|
}
|
|
},
|
|
onBlur: function() {
|
|
if (!this.pressedFlag) {
|
|
this.fakeElement.removeClass(this.options.focusClass);
|
|
this.realElement.off('blur', this.onBlur);
|
|
this.focusedFlag = false;
|
|
}
|
|
},
|
|
refresh: function() {
|
|
this.fakeElement.toggleClass(this.options.disabledClass, this.realElement.is(':disabled'));
|
|
this.list.refresh();
|
|
},
|
|
destroy: function() {
|
|
this.list.destroy();
|
|
this.realElement.insertBefore(this.fakeElement).removeClass(this.options.hiddenClass);
|
|
this.fakeElement.remove();
|
|
}
|
|
});
|
|
|
|
// options list module
|
|
function SelectList(options) {
|
|
this.options = $.extend({
|
|
holder: null,
|
|
maxVisibleItems: 10,
|
|
selectOnClick: true,
|
|
useHoverClass: false,
|
|
useCustomScroll: false,
|
|
handleResize: true,
|
|
multipleSelectWithoutKey: false,
|
|
alwaysPreventMouseWheel: false,
|
|
indexAttribute: 'data-index',
|
|
cloneClassPrefix: 'jcf-option-',
|
|
containerStructure: '<span class="jcf-list"><span class="jcf-list-content"></span></span>',
|
|
containerSelector: '.jcf-list-content',
|
|
captionClass: 'jcf-optgroup-caption',
|
|
disabledClass: 'jcf-disabled',
|
|
optionClass: 'jcf-option',
|
|
groupClass: 'jcf-optgroup',
|
|
hoverClass: 'jcf-hover',
|
|
selectedClass: 'jcf-selected',
|
|
scrollClass: 'jcf-scroll-active'
|
|
}, options);
|
|
this.init();
|
|
}
|
|
$.extend(SelectList.prototype, {
|
|
init: function() {
|
|
this.initStructure();
|
|
this.refreshSelectedClass();
|
|
this.attachEvents();
|
|
},
|
|
initStructure: function() {
|
|
this.element = $(this.options.element);
|
|
this.indexSelector = '[' + this.options.indexAttribute + ']';
|
|
this.container = $(this.options.containerStructure).appendTo(this.options.holder);
|
|
this.listHolder = this.container.find(this.options.containerSelector);
|
|
this.lastClickedIndex = this.element.prop('selectedIndex');
|
|
this.rebuildList();
|
|
|
|
// save current selection in multiple select
|
|
if (this.element.prop('multiple')) {
|
|
this.previousSelection = this.getSelectedOptionsIndexes();
|
|
}
|
|
},
|
|
attachEvents: function() {
|
|
this.bindHandlers();
|
|
this.listHolder.on('jcf-pointerdown', this.indexSelector, this.onItemPress);
|
|
this.listHolder.on('jcf-pointerdown', this.onPress);
|
|
|
|
if (this.options.useHoverClass) {
|
|
this.listHolder.on('jcf-pointerover', this.indexSelector, this.onHoverItem);
|
|
}
|
|
},
|
|
onPress: function(e) {
|
|
$(this).trigger('press', e);
|
|
this.listHolder.on('jcf-pointerup', this.onRelease);
|
|
},
|
|
onRelease: function(e) {
|
|
$(this).trigger('release', e);
|
|
this.listHolder.off('jcf-pointerup', this.onRelease);
|
|
},
|
|
onHoverItem: function(e) {
|
|
var hoverIndex = parseFloat(e.currentTarget.getAttribute(this.options.indexAttribute));
|
|
this.fakeOptions.removeClass(this.options.hoverClass).eq(hoverIndex).addClass(this.options.hoverClass);
|
|
},
|
|
onItemPress: function(e) {
|
|
if (e.pointerType === 'touch' || this.options.selectOnClick) {
|
|
// select option after "click"
|
|
this.tmpListOffsetTop = this.list.offset().top;
|
|
this.listHolder.on('jcf-pointerup', this.indexSelector, this.onItemRelease);
|
|
} else {
|
|
// select option immediately
|
|
this.onSelectItem(e);
|
|
}
|
|
},
|
|
onItemRelease: function(e) {
|
|
// remove event handlers and temporary data
|
|
this.listHolder.off('jcf-pointerup', this.indexSelector, this.onItemRelease);
|
|
|
|
// simulate item selection
|
|
if (this.tmpListOffsetTop === this.list.offset().top) {
|
|
this.listHolder.on('click', this.indexSelector, { savedPointerType: e.pointerType }, this.onSelectItem);
|
|
}
|
|
delete this.tmpListOffsetTop;
|
|
},
|
|
onSelectItem: function(e) {
|
|
var clickedIndex = parseFloat(e.currentTarget.getAttribute(this.options.indexAttribute)),
|
|
pointerType = e.data && e.data.savedPointerType || e.pointerType || 'mouse',
|
|
range;
|
|
|
|
// remove click event handler
|
|
this.listHolder.off('click', this.indexSelector, this.onSelectItem);
|
|
|
|
// ignore clicks on disabled options
|
|
if (e.button > 1 || this.realOptions[clickedIndex].disabled) {
|
|
return;
|
|
}
|
|
|
|
if (this.element.prop('multiple')) {
|
|
if (e.metaKey || e.ctrlKey || pointerType === 'touch' || this.options.multipleSelectWithoutKey) {
|
|
// if CTRL/CMD pressed or touch devices - toggle selected option
|
|
this.realOptions[clickedIndex].selected = !this.realOptions[clickedIndex].selected;
|
|
} else if (e.shiftKey) {
|
|
// if SHIFT pressed - update selection
|
|
range = [this.lastClickedIndex, clickedIndex].sort(function(a, b) {
|
|
return a - b;
|
|
});
|
|
this.realOptions.each(function(index, option) {
|
|
option.selected = (index >= range[0] && index <= range[1]);
|
|
});
|
|
} else {
|
|
// set single selected index
|
|
this.element.prop('selectedIndex', clickedIndex);
|
|
}
|
|
} else {
|
|
this.element.prop('selectedIndex', clickedIndex);
|
|
}
|
|
|
|
// save last clicked option
|
|
if (!e.shiftKey) {
|
|
this.lastClickedIndex = clickedIndex;
|
|
}
|
|
|
|
// refresh classes
|
|
this.refreshSelectedClass();
|
|
|
|
// scroll to active item in desktop browsers
|
|
if (pointerType === 'mouse') {
|
|
this.scrollToActiveOption();
|
|
}
|
|
|
|
// make callback when item selected
|
|
$(this).trigger('select');
|
|
},
|
|
rebuildList: function() {
|
|
// rebuild options
|
|
var self = this,
|
|
rootElement = this.element[0];
|
|
|
|
// recursively create fake options
|
|
this.storedSelectHTML = rootElement.innerHTML;
|
|
this.optionIndex = 0;
|
|
this.list = $(this.createOptionsList(rootElement));
|
|
this.listHolder.empty().append(this.list);
|
|
this.realOptions = this.element.find('option');
|
|
this.fakeOptions = this.list.find(this.indexSelector);
|
|
this.fakeListItems = this.list.find('.' + this.options.captionClass + ',' + this.indexSelector);
|
|
delete this.optionIndex;
|
|
|
|
// detect max visible items
|
|
var maxCount = this.options.maxVisibleItems,
|
|
sizeValue = this.element.prop('size');
|
|
if (sizeValue > 1 && !this.element.is('[jcf-size]')) {
|
|
maxCount = sizeValue;
|
|
}
|
|
|
|
// handle scrollbar
|
|
var needScrollBar = this.fakeOptions.length > maxCount;
|
|
this.container.toggleClass(this.options.scrollClass, needScrollBar);
|
|
if (needScrollBar) {
|
|
// change max-height
|
|
this.listHolder.css({
|
|
maxHeight: this.getOverflowHeight(maxCount),
|
|
overflow: 'auto'
|
|
});
|
|
|
|
if (this.options.useCustomScroll && jcf.modules.Scrollable) {
|
|
// add custom scrollbar if specified in options
|
|
jcf.replace(this.listHolder, 'Scrollable', {
|
|
handleResize: this.options.handleResize,
|
|
alwaysPreventMouseWheel: this.options.alwaysPreventMouseWheel
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
// disable edge wheel scrolling
|
|
if (this.options.alwaysPreventMouseWheel) {
|
|
this.preventWheelHandler = function(e) {
|
|
var currentScrollTop = self.listHolder.scrollTop(),
|
|
maxScrollTop = self.listHolder.prop('scrollHeight') - self.listHolder.innerHeight();
|
|
|
|
// check edge cases
|
|
if ((currentScrollTop <= 0 && e.deltaY < 0) || (currentScrollTop >= maxScrollTop && e.deltaY > 0)) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
this.listHolder.on('jcf-mousewheel', this.preventWheelHandler);
|
|
}
|
|
},
|
|
refreshSelectedClass: function() {
|
|
var self = this,
|
|
selectedItem,
|
|
isMultiple = this.element.prop('multiple'),
|
|
selectedIndex = this.element.prop('selectedIndex');
|
|
|
|
if (isMultiple) {
|
|
this.realOptions.each(function(index, option) {
|
|
self.fakeOptions.eq(index).toggleClass(self.options.selectedClass, !!option.selected);
|
|
});
|
|
} else {
|
|
this.fakeOptions.removeClass(this.options.selectedClass + ' ' + this.options.hoverClass);
|
|
selectedItem = this.fakeOptions.eq(selectedIndex).addClass(this.options.selectedClass);
|
|
if (this.options.useHoverClass) {
|
|
selectedItem.addClass(this.options.hoverClass);
|
|
}
|
|
}
|
|
},
|
|
scrollToActiveOption: function() {
|
|
// scroll to target option
|
|
var targetOffset = this.getActiveOptionOffset();
|
|
if (typeof targetOffset === 'number') {
|
|
this.listHolder.prop('scrollTop', targetOffset);
|
|
}
|
|
},
|
|
getSelectedOptionsIndexes: function() {
|
|
var selection = [];
|
|
this.realOptions.each(function(index, option) {
|
|
if (option.selected) {
|
|
selection.push(index);
|
|
}
|
|
});
|
|
return selection;
|
|
},
|
|
getChangedSelectedIndex: function() {
|
|
var selectedIndex = this.element.prop('selectedIndex'),
|
|
self = this,
|
|
found = false,
|
|
targetIndex = null;
|
|
|
|
if (this.element.prop('multiple')) {
|
|
// multiple selects handling
|
|
this.currentSelection = this.getSelectedOptionsIndexes();
|
|
$.each(this.currentSelection, function(index, optionIndex) {
|
|
if (!found && self.previousSelection.indexOf(optionIndex) < 0) {
|
|
if (index === 0) {
|
|
found = true;
|
|
}
|
|
targetIndex = optionIndex;
|
|
}
|
|
});
|
|
this.previousSelection = this.currentSelection;
|
|
return targetIndex;
|
|
} else {
|
|
// single choice selects handling
|
|
return selectedIndex;
|
|
}
|
|
},
|
|
getActiveOptionOffset: function() {
|
|
// calc values
|
|
var currentIndex = this.getChangedSelectedIndex();
|
|
|
|
// selection was not changed
|
|
if (currentIndex === null) {
|
|
return;
|
|
}
|
|
|
|
// find option and scroll to it if needed
|
|
var dropHeight = this.listHolder.height(),
|
|
dropScrollTop = this.listHolder.prop('scrollTop'),
|
|
fakeOption = this.fakeOptions.eq(currentIndex),
|
|
fakeOptionOffset = fakeOption.offset().top - this.list.offset().top,
|
|
fakeOptionHeight = fakeOption.innerHeight();
|
|
|
|
// scroll list
|
|
if (fakeOptionOffset + fakeOptionHeight >= dropScrollTop + dropHeight) {
|
|
// scroll down (always scroll to option)
|
|
return fakeOptionOffset - dropHeight + fakeOptionHeight;
|
|
} else if (fakeOptionOffset < dropScrollTop) {
|
|
// scroll up to option
|
|
return fakeOptionOffset;
|
|
}
|
|
},
|
|
getOverflowHeight: function(sizeValue) {
|
|
var item = this.fakeListItems.eq(sizeValue - 1),
|
|
listOffset = this.list.offset().top,
|
|
itemOffset = item.offset().top,
|
|
itemHeight = item.innerHeight();
|
|
|
|
return itemOffset + itemHeight - listOffset;
|
|
},
|
|
getScrollTop: function() {
|
|
return this.listHolder.scrollTop();
|
|
},
|
|
setScrollTop: function(value) {
|
|
this.listHolder.scrollTop(value);
|
|
},
|
|
createOption: function(option) {
|
|
var newOption = document.createElement('span');
|
|
newOption.className = this.options.optionClass;
|
|
newOption.innerHTML = option.innerHTML;
|
|
newOption.setAttribute(this.options.indexAttribute, this.optionIndex++);
|
|
|
|
var optionImage, optionImageSrc = option.getAttribute('data-image');
|
|
if (optionImageSrc) {
|
|
optionImage = document.createElement('img');
|
|
optionImage.src = optionImageSrc;
|
|
newOption.insertBefore(optionImage, newOption.childNodes[0]);
|
|
}
|
|
if (option.disabled) {
|
|
newOption.className += ' ' + this.options.disabledClass;
|
|
}
|
|
if (option.className) {
|
|
newOption.className += ' ' + getPrefixedClasses(option.className, this.options.cloneClassPrefix);
|
|
}
|
|
return newOption;
|
|
},
|
|
createOptGroup: function(optgroup) {
|
|
var optGroupContainer = document.createElement('span'),
|
|
optGroupName = optgroup.getAttribute('label'),
|
|
optGroupCaption, optGroupList;
|
|
|
|
// create caption
|
|
optGroupCaption = document.createElement('span');
|
|
optGroupCaption.className = this.options.captionClass;
|
|
optGroupCaption.innerHTML = optGroupName;
|
|
optGroupContainer.appendChild(optGroupCaption);
|
|
|
|
// create list of options
|
|
if (optgroup.children.length) {
|
|
optGroupList = this.createOptionsList(optgroup);
|
|
optGroupContainer.appendChild(optGroupList);
|
|
}
|
|
|
|
optGroupContainer.className = this.options.groupClass;
|
|
return optGroupContainer;
|
|
},
|
|
createOptionContainer: function() {
|
|
var optionContainer = document.createElement('li');
|
|
return optionContainer;
|
|
},
|
|
createOptionsList: function(container) {
|
|
var self = this,
|
|
list = document.createElement('ul');
|
|
|
|
$.each(container.children, function(index, currentNode) {
|
|
var item = self.createOptionContainer(currentNode),
|
|
newNode;
|
|
|
|
switch (currentNode.tagName.toLowerCase()) {
|
|
case 'option': newNode = self.createOption(currentNode); break;
|
|
case 'optgroup': newNode = self.createOptGroup(currentNode); break;
|
|
}
|
|
list.appendChild(item).appendChild(newNode);
|
|
});
|
|
return list;
|
|
},
|
|
refresh: function() {
|
|
// check for select innerHTML changes
|
|
if (this.storedSelectHTML !== this.element.prop('innerHTML')) {
|
|
this.rebuildList();
|
|
}
|
|
|
|
// refresh custom scrollbar
|
|
var scrollInstance = jcf.getInstance(this.listHolder);
|
|
if (scrollInstance) {
|
|
scrollInstance.refresh();
|
|
}
|
|
|
|
// refresh selectes classes
|
|
this.refreshSelectedClass();
|
|
},
|
|
destroy: function() {
|
|
this.listHolder.off('jcf-mousewheel', this.preventWheelHandler);
|
|
this.listHolder.off('jcf-pointerdown', this.indexSelector, this.onSelectItem);
|
|
this.listHolder.off('jcf-pointerover', this.indexSelector, this.onHoverItem);
|
|
this.listHolder.off('jcf-pointerdown', this.onPress);
|
|
}
|
|
});
|
|
|
|
// helper functions
|
|
var getPrefixedClasses = function(className, prefixToAdd) {
|
|
return className ? className.replace(/[\s]*([\S]+)+[\s]*/gi, prefixToAdd + '$1 ') : '';
|
|
};
|
|
var makeUnselectable = (function() {
|
|
var unselectableClass = jcf.getOptions().unselectableClass;
|
|
function preventHandler(e) {
|
|
e.preventDefault();
|
|
}
|
|
return function(node) {
|
|
node.addClass(unselectableClass).on('selectstart', preventHandler);
|
|
};
|
|
}());
|
|
|
|
return module;
|
|
});
|
|
|
|
}(jcf));
|