jquery.multiSelect.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. if (jQuery)(function($) {
  2. // render the html for a single option
  3. function renderOption(id, option) {
  4. var html = '<label><input type="checkbox" name="' + id + '[]" value="' + option.value + '"';
  5. if (option.selected) {
  6. html += ' checked="checked"';
  7. }
  8. html += ' />' + option.text + '</label>';
  9. return html;
  10. }
  11. // render the html for the options/optgroups
  12. function renderOptions(id, options, o) {
  13. var html = "";
  14. for (var i = 0; i < options.length; i++) {
  15. if (options[i].optgroup) {
  16. html += '<label class="optGroup">';
  17. if (o.optGroupSelectable) {
  18. html += '<input type="checkbox" class="optGroup" />' + options[i].optgroup;
  19. } else {
  20. html += options[i].optgroup;
  21. }
  22. html += '</label><div class="optGroupContainer">';
  23. html += renderOptions(id, options[i].options, o);
  24. html += '</div>';
  25. } else {
  26. html += renderOption(id, options[i]);
  27. }
  28. }
  29. return html;
  30. }
  31. // Building the actual options
  32. function buildOptions(options) {
  33. var multiSelect = $(this);
  34. var multiSelectOptions = multiSelect.next('.multiSelectOptions');
  35. var o = multiSelect.data("config");
  36. var callback = multiSelect.data("callback");
  37. // clear the existing options
  38. multiSelectOptions.html("");
  39. var html = "";
  40. // if we should have a select all option then add it
  41. if (o.selectAll) {
  42. html += '<label class="selectAll"><input type="checkbox" class="selectAll" />' + o.selectAllText + '</label>';
  43. }
  44. // generate the html for the new options
  45. html += renderOptions(multiSelect.attr('id'), options, o);
  46. multiSelectOptions.html(html);
  47. // variables needed to account for width changes due to a scrollbar
  48. var initialWidth = multiSelectOptions.width();
  49. var hasScrollbar = false;
  50. // set the height of the dropdown options
  51. if (multiSelectOptions.height() > o.listHeight) {
  52. multiSelectOptions.css("height", o.listHeight + 'px');
  53. hasScrollbar = true;
  54. } else {
  55. multiSelectOptions.css("height", '');
  56. }
  57. // 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
  58. var scrollbarWidth = hasScrollbar && (initialWidth == multiSelectOptions.width()) ? 17 : 0;
  59. // set the width of the dropdown options
  60. if ((multiSelectOptions.width() + scrollbarWidth) < multiSelect.outerWidth()) {
  61. multiSelectOptions.css("width", multiSelect.outerWidth() - 2
  62. /*border*/
  63. + 'px');
  64. } else {
  65. multiSelectOptions.css("width", (multiSelectOptions.width() + scrollbarWidth) + 'px');
  66. }
  67. // Apply bgiframe if available on IE6
  68. if ($.fn.bgiframe) multiSelect.next('.multiSelectOptions').bgiframe({
  69. width: multiSelectOptions.width(),
  70. height: multiSelectOptions.height()
  71. });
  72. // Handle selectAll oncheck
  73. if (o.selectAll) {
  74. multiSelectOptions.find('INPUT.selectAll').on('click',
  75. function() {
  76. // update all the child checkboxes
  77. $(this).prop('checked') ? multiSelectOptions.find('INPUT:checkbox').not('.selectAll, .optGroup').prop('checked', true) : multiSelectOptions.find('INPUT:checkbox').not('.selectAll, .optGroup').prop('checked', false);
  78. multiSelectOptions.find('INPUT:checkbox').parent("LABEL").toggleClass('checked', $(this).prop('checked'));
  79. });
  80. }
  81. // Handle OptGroup oncheck
  82. if (o.optGroupSelectable) {
  83. multiSelectOptions.addClass('optGroupHasCheckboxes');
  84. multiSelectOptions.find('INPUT.optGroup').on('click',
  85. function() {
  86. // update all the child checkboxes
  87. $(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);
  88. $(this).parent().next().find('INPUT:checkbox').parent("LABEL").toggleClass('checked', $(this).prop('checked'));
  89. });
  90. }
  91. // Handle all checkboxes
  92. multiSelectOptions.find('INPUT:checkbox').on('click',
  93. function() {
  94. // set the label checked class
  95. $(this).parent("LABEL").toggleClass('checked', $(this).prop('checked'));
  96. updateSelected.call(multiSelect);
  97. multiSelect.focus();
  98. if ($(this).parent().parent().hasClass('optGroupContainer')) {
  99. updateOptGroup.call(multiSelect, $(this).parent().parent().prev());
  100. }
  101. if (callback) {
  102. callback($(this));
  103. }
  104. });
  105. // Initial display
  106. multiSelectOptions.each(function() {
  107. $(this).find('INPUT:checked').parent().addClass('checked');
  108. });
  109. // Initialize selected and select all
  110. updateSelected.call(multiSelect);
  111. // Initialize optgroups
  112. if (o.optGroupSelectable) {
  113. multiSelectOptions.find('LABEL.optGroup').each(function() {
  114. updateOptGroup.call(multiSelect, $(this));
  115. });
  116. }
  117. // Handle hovers
  118. multiSelectOptions.find('LABEL:has(INPUT)').hover(function() {
  119. $(this).parent().find('LABEL').removeClass('hover');
  120. $(this).addClass('hover');
  121. },
  122. function() {
  123. $(this).parent().find('LABEL').removeClass('hover');
  124. });
  125. // Keyboard
  126. multiSelect.keydown(function(e) {
  127. var multiSelectOptions = $(this).next('.multiSelectOptions');
  128. // Is dropdown visible?
  129. if (multiSelectOptions.css('visibility') != 'hidden') {
  130. // Dropdown is visible
  131. // Tab
  132. if (e.keyCode == 9) {
  133. $(this).addClass('focus').trigger('click'); // esc, left, right - hide
  134. $(this).focus().next(':input').focus();
  135. return true;
  136. }
  137. // ESC, Left, Right
  138. if (e.keyCode == 27 || e.keyCode == 37 || e.keyCode == 39) {
  139. // Hide dropdown
  140. $(this).addClass('focus').trigger('click');
  141. }
  142. // Down || Up
  143. if (e.keyCode == 40 || e.keyCode == 38) {
  144. var allOptions = multiSelectOptions.find('LABEL');
  145. var oldHoverIndex = allOptions.index(allOptions.filter('.hover'));
  146. var newHoverIndex = -1;
  147. // if there is no current highlighted item then highlight the first item
  148. if (oldHoverIndex < 0) {
  149. // Default to first item
  150. multiSelectOptions.find('LABEL:first').addClass('hover');
  151. }
  152. // else if we are moving down and there is a next item then move
  153. else if (e.keyCode == 40 && oldHoverIndex < allOptions.length - 1) {
  154. newHoverIndex = oldHoverIndex + 1;
  155. }
  156. // else if we are moving up and there is a prev item then move
  157. else if (e.keyCode == 38 && oldHoverIndex > 0) {
  158. newHoverIndex = oldHoverIndex - 1;
  159. }
  160. if (newHoverIndex >= 0) {
  161. $(allOptions.get(oldHoverIndex)).removeClass('hover'); // remove the current highlight
  162. $(allOptions.get(newHoverIndex)).addClass('hover'); // add the new highlight
  163. // Adjust the viewport if necessary
  164. adjustViewPort(multiSelectOptions);
  165. }
  166. return false;
  167. }
  168. // Enter, Space
  169. if (e.keyCode == 13 || e.keyCode == 32) {
  170. var selectedCheckbox = multiSelectOptions.find('LABEL.hover INPUT:checkbox');
  171. // Set the checkbox (and label class)
  172. selectedCheckbox.prop('checked', !selectedCheckbox.attr('checked')).parent("LABEL").toggleClass('checked', selectedCheckbox.attr('checked'));
  173. // if the checkbox was the select all then set all the checkboxes
  174. if (selectedCheckbox.hasClass("selectAll")) {
  175. multiSelectOptions.find('INPUT:checkbox').prop('checked', selectedCheckbox.attr('checked')).parent("LABEL").addClass('checked').toggleClass('checked', selectedCheckbox.attr('checked'));
  176. }
  177. updateSelected.call(multiSelect);
  178. if (callback) callback($(this));
  179. return false;
  180. }
  181. // Any other standard keyboard character (try and match the first character of an option)
  182. if (e.keyCode >= 33 && e.keyCode <= 126) {
  183. // find the next matching item after the current hovered item
  184. var match = multiSelectOptions.find('LABEL:startsWith(' + String.fromCharCode(e.keyCode) + ')');
  185. var currentHoverIndex = match.index(match.filter('LABEL.hover'));
  186. // filter the set to any items after the current hovered item
  187. var afterHoverMatch = match.filter(function(index) {
  188. return index > currentHoverIndex;
  189. });
  190. // if there were no item after the current hovered item then try using the full search results (filtered to the first one)
  191. match = (afterHoverMatch.length >= 1 ? afterHoverMatch: match).filter("LABEL:first");
  192. if (match.length == 1) {
  193. // if we found a match then move the hover
  194. multiSelectOptions.find('LABEL.hover').removeClass('hover');
  195. match.addClass('hover');
  196. adjustViewPort(multiSelectOptions);
  197. }
  198. }
  199. } else {
  200. // Dropdown is not visible
  201. if (e.keyCode == 38 || e.keyCode == 40 || e.keyCode == 13 || e.keyCode == 32) { //up, down, enter, space - show
  202. // Show dropdown
  203. $(this).removeClass('focus').trigger('click');
  204. multiSelectOptions.find('LABEL:first').addClass('hover');
  205. return false;
  206. }
  207. // Tab key
  208. if (e.keyCode == 9) {
  209. // Shift focus to next INPUT element on page
  210. multiSelectOptions.next(':input').focus();
  211. return true;
  212. }
  213. }
  214. // Prevent enter key from submitting form
  215. if (e.keyCode == 13) return false;
  216. });
  217. }
  218. // Adjust the viewport if necessary
  219. function adjustViewPort(multiSelectOptions) {
  220. // check for and move down
  221. var selectionBottom = multiSelectOptions.find('LABEL.hover').position().top + multiSelectOptions.find('LABEL.hover').outerHeight();
  222. if (selectionBottom > multiSelectOptions.innerHeight()) {
  223. multiSelectOptions.scrollTop(multiSelectOptions.scrollTop() + selectionBottom - multiSelectOptions.innerHeight());
  224. }
  225. // check for and move up
  226. if (multiSelectOptions.find('LABEL.hover').position().top < 0) {
  227. multiSelectOptions.scrollTop(multiSelectOptions.scrollTop() + multiSelectOptions.find('LABEL.hover').position().top);
  228. }
  229. }
  230. // Update the optgroup checked status
  231. function updateOptGroup(optGroup) {
  232. var multiSelect = $(this);
  233. var o = multiSelect.data("config");
  234. // Determine if the optgroup should be checked
  235. if (o.optGroupSelectable) {
  236. var optGroupSelected = true;
  237. $(optGroup).next().find('INPUT:checkbox').each(function() {
  238. if (!$(this).prop('checked')) {
  239. optGroupSelected = false;
  240. return false;
  241. }
  242. });
  243. $(optGroup).find('INPUT.optGroup').prop('checked', optGroupSelected).parent("LABEL").toggleClass('checked', optGroupSelected);
  244. }
  245. }
  246. // Update the textbox with the total number of selected items, and determine select all
  247. function updateSelected() {
  248. var multiSelect = $(this);
  249. var multiSelectOptions = multiSelect.next('.multiSelectOptions');
  250. var o = multiSelect.data("config");
  251. var i = 0;
  252. var selectAll = true;
  253. var display = '';
  254. multiSelectOptions.find('INPUT:checkbox').not('.selectAll, .optGroup').each(function() {
  255. if ($(this).prop('checked')) {
  256. i++;
  257. display = display + $(this).parent().text() + ', ';
  258. } else selectAll = false;
  259. });
  260. // trim any end comma and surounding whitespace
  261. display = display.replace(/\s*\,\s*$/, '');
  262. if (i == 0) {
  263. multiSelect.find("span").html(o.noneSelected);
  264. } else {
  265. if (o.oneOrMoreSelected == '*') {
  266. multiSelect.find("span").html(display);
  267. multiSelect.attr("title", display);
  268. } else {
  269. multiSelect.find("span").html(o.oneOrMoreSelected.replace('%', i));
  270. }
  271. }
  272. // Determine if Select All should be checked
  273. if (o.selectAll) {
  274. multiSelectOptions.find('INPUT.selectAll').prop('checked', selectAll).parent("LABEL").toggleClass('checked', selectAll);
  275. }
  276. }
  277. $.extend($.fn, {
  278. multiSelect: function(o, callback) {
  279. // Default options
  280. if (!o) o = {};
  281. if (o.selectAll == undefined) o.selectAll = true;
  282. if (o.selectAllText == undefined) o.selectAllText = "Select All";
  283. if (o.noneSelected == undefined) o.noneSelected = 'Select options';
  284. if (o.oneOrMoreSelected == undefined) o.oneOrMoreSelected = '% selected';
  285. if (o.optGroupSelectable == undefined) o.optGroupSelectable = false;
  286. if (o.listHeight == undefined) o.listHeight = 150;
  287. // Initialize each multiSelect
  288. $(this).each(function() {
  289. var select = $(this);
  290. var html = '<a href="javascript:;" class="multiSelect"><span></span></a>';
  291. html += '<div class="multiSelectOptions" style="position: absolute; z-index: 99999; visibility: hidden;"></div>';
  292. $(select).after(html);
  293. var multiSelect = $(select).next('.multiSelect');
  294. var multiSelectOptions = multiSelect.next('.multiSelectOptions');
  295. // if the select object had a width defined then match the new multilsect to it
  296. multiSelect.find("span").css("width", $(select).width() + 'px');
  297. // Attach the config options to the multiselect
  298. multiSelect.data("config", o);
  299. // Attach the callback to the multiselect
  300. multiSelect.data("callback", callback);
  301. // Serialize the select options into json options
  302. var options = [];
  303. $(select).children().each(function() {
  304. if (this.tagName.toUpperCase() == 'OPTGROUP') {
  305. var suboptions = [];
  306. options.push({
  307. optgroup: $(this).attr('label'),
  308. options: suboptions
  309. });
  310. $(this).children('OPTION').each(function() {
  311. if ($(this).val() != '') {
  312. suboptions.push({
  313. text: $(this).html(),
  314. value: $(this).val(),
  315. selected: $(this).attr('selected')
  316. });
  317. }
  318. });
  319. } else if (this.tagName.toUpperCase() == 'OPTION') {
  320. if ($(this).val() != '') {
  321. options.push({
  322. text: $(this).html(),
  323. value: $(this).val(),
  324. selected: $(this).attr('selected')
  325. });
  326. }
  327. }
  328. });
  329. // Eliminate the original form element
  330. $(select).remove();
  331. // Add the id that was on the original select element to the new input
  332. multiSelect.attr("id", $(select).attr("id"));
  333. // Build the dropdown options
  334. buildOptions.call(multiSelect, options);
  335. // Events
  336. multiSelect.hover(function() {
  337. $(this).addClass('hover');
  338. },
  339. function() {
  340. $(this).removeClass('hover');
  341. }).click(function() {
  342. // Show/hide on click
  343. if ($(this).hasClass('active')) {
  344. $(this).multiSelectOptionsHide();
  345. } else {
  346. $(this).multiSelectOptionsShow();
  347. }
  348. return false;
  349. }).focus(function() {
  350. // So it can be styled with CSS
  351. $(this).addClass('focus');
  352. }).blur(function() {
  353. // So it can be styled with CSS
  354. $(this).removeClass('focus');
  355. });
  356. // Add an event listener to the window to close the multiselect if the user clicks off
  357. $(document).click(function(event) {
  358. // If somewhere outside of the multiselect was clicked then hide the multiselect
  359. if (! ($(event.target).parents().andSelf().is('.multiSelectOptions'))) {
  360. multiSelect.multiSelectOptionsHide();
  361. }
  362. });
  363. });
  364. },
  365. // Update the dropdown options
  366. multiSelectOptionsUpdate: function(options) {
  367. buildOptions.call($(this), options);
  368. },
  369. // Hide the dropdown
  370. multiSelectOptionsHide: function() {
  371. $(this).removeClass('active').removeClass('hover').next('.multiSelectOptions').css('visibility', 'hidden');
  372. },
  373. // Show the dropdown
  374. multiSelectOptionsShow: function() {
  375. var multiSelect = $(this);
  376. var multiSelectOptions = multiSelect.next('.multiSelectOptions');
  377. var o = multiSelect.data("config");
  378. // Hide any open option boxes
  379. $('.multiSelect').multiSelectOptionsHide();
  380. multiSelectOptions.find('LABEL').removeClass('hover');
  381. multiSelect.addClass('active').next('.multiSelectOptions').css('visibility', 'visible');
  382. multiSelect.focus();
  383. // reset the scroll to the top
  384. multiSelect.next('.multiSelectOptions').scrollTop(0);
  385. // Position it
  386. var offset = multiSelect.position();
  387. multiSelect.next('.multiSelectOptions').css({
  388. top: offset.top + $(this).outerHeight() + 'px'
  389. });
  390. multiSelect.next('.multiSelectOptions').css({
  391. left: offset.left + 'px'
  392. });
  393. },
  394. // get a coma-delimited list of selected values
  395. selectedValuesString: function() {
  396. var selectedValues = "";
  397. $(this).next('.multiSelectOptions').find('INPUT:checkbox:checked').not('.optGroup, .selectAll').each(function() {
  398. selectedValues += $(this).attr('value') + ",";
  399. });
  400. // trim any end comma and surounding whitespace
  401. return selectedValues.replace(/\s*\,\s*$/, '');
  402. }
  403. });
  404. // add a new ":startsWith" search filter
  405. $.expr[":"].startsWith = function(el, i, m) {
  406. var search = m[3];
  407. if (!search) return false;
  408. return eval("/^[/s]*" + search + "/i").test($(el).text());
  409. };
  410. })(jQuery);