if (jQuery)(function($) {
// render the html for a single option
function renderOption(id, option) {
var html = '';
return html;
}
// render the html for the options/optgroups
function renderOptions(id, options, o) {
var html = "";
for (var i = 0; i < options.length; i++) {
if (options[i].optgroup) {
html += '
';
html += renderOptions(id, options[i].options, o);
html += '
';
} else {
html += renderOption(id, options[i]);
}
}
return html;
}
// Building the actual options
function buildOptions(options) {
var multiSelect = $(this);
var multiSelectOptions = multiSelect.next('.multiSelectOptions');
var o = multiSelect.data("config");
var callback = multiSelect.data("callback");
// clear the existing options
multiSelectOptions.html("");
var html = "";
// if we should have a select all option then add it
if (o.selectAll) {
html += '';
}
// generate the html for the new options
html += renderOptions(multiSelect.attr('id'), options, o);
multiSelectOptions.html(html);
// variables needed to account for width changes due to a scrollbar
var initialWidth = multiSelectOptions.width();
var hasScrollbar = false;
// set the height of the dropdown options
if (multiSelectOptions.height() > o.listHeight) {
multiSelectOptions.css("height", o.listHeight + 'px');
hasScrollbar = true;
} else {
multiSelectOptions.css("height", '');
}
// if the there is a scrollbar and the browser did not already handle adjusting the width (i.e. Firefox) then we will need to manaually add the scrollbar width
var scrollbarWidth = hasScrollbar && (initialWidth == multiSelectOptions.width()) ? 17 : 0;
// set the width of the dropdown options
if ((multiSelectOptions.width() + scrollbarWidth) < multiSelect.outerWidth()) {
multiSelectOptions.css("width", multiSelect.outerWidth() - 2
/*border*/
+ 'px');
} else {
multiSelectOptions.css("width", (multiSelectOptions.width() + scrollbarWidth) + 'px');
}
// Apply bgiframe if available on IE6
if ($.fn.bgiframe) multiSelect.next('.multiSelectOptions').bgiframe({
width: multiSelectOptions.width(),
height: multiSelectOptions.height()
});
// Handle selectAll oncheck
if (o.selectAll) {
multiSelectOptions.find('INPUT.selectAll').on('click',
function() {
// update all the child checkboxes
$(this).prop('checked') ? multiSelectOptions.find('INPUT:checkbox').not('.selectAll, .optGroup').prop('checked', true) : multiSelectOptions.find('INPUT:checkbox').not('.selectAll, .optGroup').prop('checked', false);
multiSelectOptions.find('INPUT:checkbox').parent("LABEL").toggleClass('checked', $(this).prop('checked'));
});
}
// Handle OptGroup oncheck
if (o.optGroupSelectable) {
multiSelectOptions.addClass('optGroupHasCheckboxes');
multiSelectOptions.find('INPUT.optGroup').on('click',
function() {
// update all the child checkboxes
$(this).prop('checked') ? $(this).parent().next().find('INPUT:checkbox').not('.selectAll, .optGroup').prop('checked', true) : $(this).parent().next().find('INPUT:checkbox').not('.selectAll, .optGroup').prop('checked', false);
$(this).parent().next().find('INPUT:checkbox').parent("LABEL").toggleClass('checked', $(this).prop('checked'));
});
}
// Handle all checkboxes
multiSelectOptions.find('INPUT:checkbox').on('click',
function() {
// set the label checked class
$(this).parent("LABEL").toggleClass('checked', $(this).prop('checked'));
updateSelected.call(multiSelect);
multiSelect.focus();
if ($(this).parent().parent().hasClass('optGroupContainer')) {
updateOptGroup.call(multiSelect, $(this).parent().parent().prev());
}
if (callback) {
callback($(this));
}
});
// Initial display
multiSelectOptions.each(function() {
$(this).find('INPUT:checked').parent().addClass('checked');
});
// Initialize selected and select all
updateSelected.call(multiSelect);
// Initialize optgroups
if (o.optGroupSelectable) {
multiSelectOptions.find('LABEL.optGroup').each(function() {
updateOptGroup.call(multiSelect, $(this));
});
}
// Handle hovers
multiSelectOptions.find('LABEL:has(INPUT)').hover(function() {
$(this).parent().find('LABEL').removeClass('hover');
$(this).addClass('hover');
},
function() {
$(this).parent().find('LABEL').removeClass('hover');
});
// Keyboard
multiSelect.keydown(function(e) {
var multiSelectOptions = $(this).next('.multiSelectOptions');
// Is dropdown visible?
if (multiSelectOptions.css('visibility') != 'hidden') {
// Dropdown is visible
// Tab
if (e.keyCode == 9) {
$(this).addClass('focus').trigger('click'); // esc, left, right - hide
$(this).focus().next(':input').focus();
return true;
}
// ESC, Left, Right
if (e.keyCode == 27 || e.keyCode == 37 || e.keyCode == 39) {
// Hide dropdown
$(this).addClass('focus').trigger('click');
}
// Down || Up
if (e.keyCode == 40 || e.keyCode == 38) {
var allOptions = multiSelectOptions.find('LABEL');
var oldHoverIndex = allOptions.index(allOptions.filter('.hover'));
var newHoverIndex = -1;
// if there is no current highlighted item then highlight the first item
if (oldHoverIndex < 0) {
// Default to first item
multiSelectOptions.find('LABEL:first').addClass('hover');
}
// else if we are moving down and there is a next item then move
else if (e.keyCode == 40 && oldHoverIndex < allOptions.length - 1) {
newHoverIndex = oldHoverIndex + 1;
}
// else if we are moving up and there is a prev item then move
else if (e.keyCode == 38 && oldHoverIndex > 0) {
newHoverIndex = oldHoverIndex - 1;
}
if (newHoverIndex >= 0) {
$(allOptions.get(oldHoverIndex)).removeClass('hover'); // remove the current highlight
$(allOptions.get(newHoverIndex)).addClass('hover'); // add the new highlight
// Adjust the viewport if necessary
adjustViewPort(multiSelectOptions);
}
return false;
}
// Enter, Space
if (e.keyCode == 13 || e.keyCode == 32) {
var selectedCheckbox = multiSelectOptions.find('LABEL.hover INPUT:checkbox');
// Set the checkbox (and label class)
selectedCheckbox.prop('checked', !selectedCheckbox.attr('checked')).parent("LABEL").toggleClass('checked', selectedCheckbox.attr('checked'));
// if the checkbox was the select all then set all the checkboxes
if (selectedCheckbox.hasClass("selectAll")) {
multiSelectOptions.find('INPUT:checkbox').prop('checked', selectedCheckbox.attr('checked')).parent("LABEL").addClass('checked').toggleClass('checked', selectedCheckbox.attr('checked'));
}
updateSelected.call(multiSelect);
if (callback) callback($(this));
return false;
}
// Any other standard keyboard character (try and match the first character of an option)
if (e.keyCode >= 33 && e.keyCode <= 126) {
// find the next matching item after the current hovered item
var match = multiSelectOptions.find('LABEL:startsWith(' + String.fromCharCode(e.keyCode) + ')');
var currentHoverIndex = match.index(match.filter('LABEL.hover'));
// filter the set to any items after the current hovered item
var afterHoverMatch = match.filter(function(index) {
return index > currentHoverIndex;
});
// if there were no item after the current hovered item then try using the full search results (filtered to the first one)
match = (afterHoverMatch.length >= 1 ? afterHoverMatch: match).filter("LABEL:first");
if (match.length == 1) {
// if we found a match then move the hover
multiSelectOptions.find('LABEL.hover').removeClass('hover');
match.addClass('hover');
adjustViewPort(multiSelectOptions);
}
}
} else {
// Dropdown is not visible
if (e.keyCode == 38 || e.keyCode == 40 || e.keyCode == 13 || e.keyCode == 32) { //up, down, enter, space - show
// Show dropdown
$(this).removeClass('focus').trigger('click');
multiSelectOptions.find('LABEL:first').addClass('hover');
return false;
}
// Tab key
if (e.keyCode == 9) {
// Shift focus to next INPUT element on page
multiSelectOptions.next(':input').focus();
return true;
}
}
// Prevent enter key from submitting form
if (e.keyCode == 13) return false;
});
}
// Adjust the viewport if necessary
function adjustViewPort(multiSelectOptions) {
// check for and move down
var selectionBottom = multiSelectOptions.find('LABEL.hover').position().top + multiSelectOptions.find('LABEL.hover').outerHeight();
if (selectionBottom > multiSelectOptions.innerHeight()) {
multiSelectOptions.scrollTop(multiSelectOptions.scrollTop() + selectionBottom - multiSelectOptions.innerHeight());
}
// check for and move up
if (multiSelectOptions.find('LABEL.hover').position().top < 0) {
multiSelectOptions.scrollTop(multiSelectOptions.scrollTop() + multiSelectOptions.find('LABEL.hover').position().top);
}
}
// Update the optgroup checked status
function updateOptGroup(optGroup) {
var multiSelect = $(this);
var o = multiSelect.data("config");
// Determine if the optgroup should be checked
if (o.optGroupSelectable) {
var optGroupSelected = true;
$(optGroup).next().find('INPUT:checkbox').each(function() {
if (!$(this).prop('checked')) {
optGroupSelected = false;
return false;
}
});
$(optGroup).find('INPUT.optGroup').prop('checked', optGroupSelected).parent("LABEL").toggleClass('checked', optGroupSelected);
}
}
// Update the textbox with the total number of selected items, and determine select all
function updateSelected() {
var multiSelect = $(this);
var multiSelectOptions = multiSelect.next('.multiSelectOptions');
var o = multiSelect.data("config");
var i = 0;
var selectAll = true;
var display = '';
multiSelectOptions.find('INPUT:checkbox').not('.selectAll, .optGroup').each(function() {
if ($(this).prop('checked')) {
i++;
display = display + $(this).parent().text() + ', ';
} else selectAll = false;
});
// trim any end comma and surounding whitespace
display = display.replace(/\s*\,\s*$/, '');
if (i == 0) {
multiSelect.find("span").html(o.noneSelected);
} else {
if (o.oneOrMoreSelected == '*') {
multiSelect.find("span").html(display);
multiSelect.attr("title", display);
} else {
multiSelect.find("span").html(o.oneOrMoreSelected.replace('%', i));
}
}
// Determine if Select All should be checked
if (o.selectAll) {
multiSelectOptions.find('INPUT.selectAll').prop('checked', selectAll).parent("LABEL").toggleClass('checked', selectAll);
}
}
$.extend($.fn, {
multiSelect: function(o, callback) {
// Default options
if (!o) o = {};
if (o.selectAll == undefined) o.selectAll = true;
if (o.selectAllText == undefined) o.selectAllText = "Select All";
if (o.noneSelected == undefined) o.noneSelected = 'Select options';
if (o.oneOrMoreSelected == undefined) o.oneOrMoreSelected = '% selected';
if (o.optGroupSelectable == undefined) o.optGroupSelectable = false;
if (o.listHeight == undefined) o.listHeight = 150;
// Initialize each multiSelect
$(this).each(function() {
var select = $(this);
var html = '';
html += '';
$(select).after(html);
var multiSelect = $(select).next('.multiSelect');
var multiSelectOptions = multiSelect.next('.multiSelectOptions');
// if the select object had a width defined then match the new multilsect to it
multiSelect.find("span").css("width", $(select).width() + 'px');
// Attach the config options to the multiselect
multiSelect.data("config", o);
// Attach the callback to the multiselect
multiSelect.data("callback", callback);
// Serialize the select options into json options
var options = [];
$(select).children().each(function() {
if (this.tagName.toUpperCase() == 'OPTGROUP') {
var suboptions = [];
options.push({
optgroup: $(this).attr('label'),
options: suboptions
});
$(this).children('OPTION').each(function() {
if ($(this).val() != '') {
suboptions.push({
text: $(this).html(),
value: $(this).val(),
selected: $(this).attr('selected')
});
}
});
} else if (this.tagName.toUpperCase() == 'OPTION') {
if ($(this).val() != '') {
options.push({
text: $(this).html(),
value: $(this).val(),
selected: $(this).attr('selected')
});
}
}
});
// Eliminate the original form element
$(select).remove();
// Add the id that was on the original select element to the new input
multiSelect.attr("id", $(select).attr("id"));
// Build the dropdown options
buildOptions.call(multiSelect, options);
// Events
multiSelect.hover(function() {
$(this).addClass('hover');
},
function() {
$(this).removeClass('hover');
}).click(function() {
// Show/hide on click
if ($(this).hasClass('active')) {
$(this).multiSelectOptionsHide();
} else {
$(this).multiSelectOptionsShow();
}
return false;
}).focus(function() {
// So it can be styled with CSS
$(this).addClass('focus');
}).blur(function() {
// So it can be styled with CSS
$(this).removeClass('focus');
});
// Add an event listener to the window to close the multiselect if the user clicks off
$(document).click(function(event) {
// If somewhere outside of the multiselect was clicked then hide the multiselect
if (! ($(event.target).parents().andSelf().is('.multiSelectOptions'))) {
multiSelect.multiSelectOptionsHide();
}
});
});
},
// Update the dropdown options
multiSelectOptionsUpdate: function(options) {
buildOptions.call($(this), options);
},
// Hide the dropdown
multiSelectOptionsHide: function() {
$(this).removeClass('active').removeClass('hover').next('.multiSelectOptions').css('visibility', 'hidden');
},
// Show the dropdown
multiSelectOptionsShow: function() {
var multiSelect = $(this);
var multiSelectOptions = multiSelect.next('.multiSelectOptions');
var o = multiSelect.data("config");
// Hide any open option boxes
$('.multiSelect').multiSelectOptionsHide();
multiSelectOptions.find('LABEL').removeClass('hover');
multiSelect.addClass('active').next('.multiSelectOptions').css('visibility', 'visible');
multiSelect.focus();
// reset the scroll to the top
multiSelect.next('.multiSelectOptions').scrollTop(0);
// Position it
var offset = multiSelect.position();
multiSelect.next('.multiSelectOptions').css({
top: offset.top + $(this).outerHeight() + 'px'
});
multiSelect.next('.multiSelectOptions').css({
left: offset.left + 'px'
});
},
// get a coma-delimited list of selected values
selectedValuesString: function() {
var selectedValues = "";
$(this).next('.multiSelectOptions').find('INPUT:checkbox:checked').not('.optGroup, .selectAll').each(function() {
selectedValues += $(this).attr('value') + ",";
});
// trim any end comma and surounding whitespace
return selectedValues.replace(/\s*\,\s*$/, '');
}
});
// add a new ":startsWith" search filter
$.expr[":"].startsWith = function(el, i, m) {
var search = m[3];
if (!search) return false;
return eval("/^[/s]*" + search + "/i").test($(el).text());
};
})(jQuery);