jquery.multiselect.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. /*
  2. * jQuery MultiSelect UI Widget 1.11
  3. * Copyright (c) 2011 Eric Hynds
  4. *
  5. * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/
  6. *
  7. * Depends:
  8. * - jQuery 1.4.2+
  9. * - jQuery UI 1.8 widget factory
  10. *
  11. * Optional:
  12. * - jQuery UI effects
  13. * - jQuery UI position utility
  14. *
  15. * Dual licensed under the MIT and GPL licenses:
  16. * http://www.opensource.org/licenses/mit-license.php
  17. * http://www.gnu.org/licenses/gpl.html
  18. *
  19. */
  20. (function($, undefined){
  21. var multiselectID = 0;
  22. $.widget("ech.multiselect", {
  23. // default options
  24. options: {
  25. header: true,
  26. height: 175,
  27. minWidth: 225,
  28. classes: '',
  29. checkAllText: 'Check all',
  30. uncheckAllText: 'Uncheck all',
  31. noneSelectedText: 'Select options',
  32. selectedText: '# selected',
  33. selectedList: 0,
  34. show: '',
  35. hide: '',
  36. autoOpen: false,
  37. multiple: true,
  38. position: {}
  39. },
  40. _create: function(){
  41. var el = this.element.hide(),
  42. o = this.options;
  43. this.speed = $.fx.speeds._default; // default speed for effects
  44. this._isOpen = false; // assume no
  45. var
  46. button = (this.button = $('<button type="button"><span class="ui-icon ui-icon-triangle-2-n-s"></span></button>'))
  47. .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all')
  48. .addClass( o.classes )
  49. .attr({ 'title':el.attr('title'), 'aria-haspopup':true, 'tabIndex':el.attr('tabIndex') })
  50. .insertAfter( el ),
  51. buttonlabel = (this.buttonlabel = $('<span />'))
  52. .html( o.noneSelectedText )
  53. .appendTo( button ),
  54. menu = (this.menu = $('<div />'))
  55. .addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all')
  56. .addClass( o.classes )
  57. .insertAfter( button ),
  58. header = (this.header = $('<div />'))
  59. .addClass('ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix')
  60. .appendTo( menu ),
  61. headerLinkContainer = (this.headerLinkContainer = $('<ul />'))
  62. .addClass('ui-helper-reset')
  63. .html(function(){
  64. if( o.header === true ){
  65. return '<li><a class="ui-multiselect-all" href="#"><span class="ui-icon ui-icon-check"></span><span>' + o.checkAllText + '</span></a></li><li><a class="ui-multiselect-none" href="#"><span class="ui-icon ui-icon-closethick"></span><span>' + o.uncheckAllText + '</span></a></li>';
  66. } else if(typeof o.header === "string"){
  67. return '<li>' + o.header + '</li>';
  68. } else {
  69. return '';
  70. }
  71. })
  72. .append('<li class="ui-multiselect-close"><a href="#" class="ui-multiselect-close"><span class="ui-icon ui-icon-circle-close"></span></a></li>')
  73. .appendTo( header ),
  74. checkboxContainer = (this.checkboxContainer = $('<ul />'))
  75. .addClass('ui-multiselect-checkboxes ui-helper-reset')
  76. .appendTo( menu );
  77. // perform event bindings
  78. this._bindEvents();
  79. // build menu
  80. this.refresh( true );
  81. // some addl. logic for single selects
  82. if( !o.multiple ){
  83. menu.addClass('ui-multiselect-single');
  84. }
  85. },
  86. _init: function(){
  87. if( this.options.header === false ){
  88. this.header.hide();
  89. }
  90. if( !this.options.multiple ){
  91. this.headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none').hide();
  92. }
  93. if( this.options.autoOpen ){
  94. this.open();
  95. }
  96. if( this.element.is(':disabled') ){
  97. this.disable();
  98. }
  99. },
  100. refresh: function( init ){
  101. var el = this.element,
  102. o = this.options,
  103. menu = this.menu,
  104. checkboxContainer = this.checkboxContainer,
  105. optgroups = [],
  106. html = [],
  107. id = el.attr('id') || multiselectID++; // unique ID for the label & option tags
  108. // build items
  109. this.element.find('option').each(function( i ){
  110. var $this = $(this),
  111. parent = this.parentNode,
  112. title = this.innerHTML,
  113. description = this.title,
  114. value = this.value,
  115. inputID = this.id || 'ui-multiselect-'+id+'-option-'+i,
  116. isDisabled = this.disabled,
  117. isSelected = this.selected,
  118. labelClasses = ['ui-corner-all'],
  119. optLabel;
  120. // is this an optgroup?
  121. if( parent.tagName.toLowerCase() === 'optgroup' ){
  122. optLabel = parent.getAttribute('label');
  123. // has this optgroup been added already?
  124. if( $.inArray(optLabel, optgroups) === -1 ){
  125. html.push('<li class="ui-multiselect-optgroup-label"><a href="#">' + optLabel + '</a></li>');
  126. optgroups.push( optLabel );
  127. }
  128. }
  129. if( isDisabled ){
  130. labelClasses.push('ui-state-disabled');
  131. }
  132. // browsers automatically select the first option
  133. // by default with single selects
  134. if( isSelected && !o.multiple ){
  135. labelClasses.push('ui-state-active');
  136. }
  137. html.push('<li class="' + (isDisabled ? 'ui-multiselect-disabled' : '') + '">');
  138. // create the label
  139. html.push('<label for="'+inputID+'" title="'+description+'" class="'+labelClasses.join(' ')+ '">');
  140. html.push('<input id="'+inputID+'" name="multiselect_'+id+'" type="'+(o.multiple ? "checkbox" : "radio")+'" value="'+value+'" title="'+title+'"');
  141. // pre-selected?
  142. if( isSelected ){
  143. html.push(' checked="checked"');
  144. html.push(' aria-selected="true"');
  145. }
  146. // disabled?
  147. if( isDisabled ){
  148. html.push(' disabled="disabled"');
  149. html.push(' aria-disabled="true"');
  150. }
  151. // add the title and close everything off
  152. html.push(' /><span>' + title + '</span></label></li>');
  153. });
  154. // insert into the DOM
  155. checkboxContainer.html( html.join('') );
  156. // cache some moar useful elements
  157. this.labels = menu.find('label');
  158. // set widths
  159. this._setButtonWidth();
  160. this._setMenuWidth();
  161. // remember default value
  162. this.button[0].defaultValue = this.update();
  163. // broadcast refresh event; useful for widgets
  164. if( !init ){
  165. this._trigger('refresh');
  166. }
  167. },
  168. // updates the button text. call refresh() to rebuild
  169. update: function(){
  170. var o = this.options,
  171. $inputs = this.labels.find('input'),
  172. $checked = $inputs.filter(':checked'),
  173. numChecked = $checked.length,
  174. value;
  175. if( numChecked === 0 ){
  176. value = o.noneSelectedText;
  177. } else {
  178. if($.isFunction( o.selectedText )){
  179. value = o.selectedText.call(this, numChecked, $inputs.length, $checked.get());
  180. } else if( /\d/.test(o.selectedList) && o.selectedList > 0 && numChecked <= o.selectedList){
  181. value = $checked.map(function(){ return this.title; }).get().join(', ');
  182. } else {
  183. value = o.selectedText.replace('#', numChecked).replace('#', $inputs.length);
  184. }
  185. }
  186. this.buttonlabel.html( value );
  187. return value;
  188. },
  189. // binds events
  190. _bindEvents: function(){
  191. var self = this, button = this.button;
  192. function clickHandler(){
  193. self[ self._isOpen ? 'close' : 'open' ]();
  194. return false;
  195. }
  196. // webkit doesn't like it when you click on the span :(
  197. button
  198. .find('span')
  199. .bind('click.multiselect', clickHandler);
  200. // button events
  201. button.bind({
  202. click: clickHandler,
  203. keypress: function( e ){
  204. switch(e.which){
  205. case 27: // esc
  206. case 38: // up
  207. case 37: // left
  208. self.close();
  209. break;
  210. case 39: // right
  211. case 40: // down
  212. self.open();
  213. break;
  214. }
  215. },
  216. mouseenter: function(){
  217. if( !button.hasClass('ui-state-disabled') ){
  218. $(this).addClass('ui-state-hover');
  219. }
  220. },
  221. mouseleave: function(){
  222. $(this).removeClass('ui-state-hover');
  223. },
  224. focus: function(){
  225. if( !button.hasClass('ui-state-disabled') ){
  226. $(this).addClass('ui-state-focus');
  227. }
  228. },
  229. blur: function(){
  230. $(this).removeClass('ui-state-focus');
  231. }
  232. });
  233. // header links
  234. this.header
  235. .delegate('a', 'click.multiselect', function( e ){
  236. // close link
  237. if( $(this).hasClass('ui-multiselect-close') ){
  238. self.close();
  239. // check all / uncheck all
  240. } else {
  241. self[ $(this).hasClass('ui-multiselect-all') ? 'checkAll' : 'uncheckAll' ]();
  242. }
  243. e.preventDefault();
  244. });
  245. // optgroup label toggle support
  246. this.menu
  247. .delegate('li.ui-multiselect-optgroup-label a', 'click.multiselect', function( e ){
  248. e.preventDefault();
  249. var $this = $(this),
  250. $inputs = $this.parent().nextUntil('li.ui-multiselect-optgroup-label').find('input:visible:not(:disabled)'),
  251. nodes = $inputs.get(),
  252. label = $this.parent().text();
  253. // trigger event and bail if the return is false
  254. if( self._trigger('beforeoptgrouptoggle', e, { inputs:nodes, label:label }) === false ){
  255. return;
  256. }
  257. // toggle inputs
  258. self._toggleChecked(
  259. $inputs.filter(':checked').length !== $inputs.length,
  260. $inputs
  261. );
  262. self._trigger('optgrouptoggle', e, {
  263. inputs: nodes,
  264. label: label,
  265. checked: nodes[0].checked
  266. });
  267. })
  268. .delegate('label', 'mouseenter.multiselect', function(){
  269. if( !$(this).hasClass('ui-state-disabled') ){
  270. self.labels.removeClass('ui-state-hover');
  271. $(this).addClass('ui-state-hover').find('input').focus();
  272. }
  273. })
  274. .delegate('label', 'keydown.multiselect', function( e ){
  275. e.preventDefault();
  276. switch(e.which){
  277. case 9: // tab
  278. case 27: // esc
  279. self.close();
  280. break;
  281. case 38: // up
  282. case 40: // down
  283. case 37: // left
  284. case 39: // right
  285. self._traverse(e.which, this);
  286. break;
  287. case 13: // enter
  288. $(this).find('input')[0].click();
  289. break;
  290. }
  291. })
  292. .delegate('input[type="checkbox"], input[type="radio"]', 'click.multiselect', function( e ){
  293. var $this = $(this),
  294. val = this.value,
  295. checked = this.checked,
  296. tags = self.element.find('option');
  297. // bail if this input is disabled or the event is cancelled
  298. if( this.disabled || self._trigger('click', e, { value:val, text:this.title, checked:checked }) === false ){
  299. e.preventDefault();
  300. return;
  301. }
  302. // toggle aria state
  303. $this.attr('aria-selected', checked);
  304. // change state on the original option tags
  305. tags.each(function(){
  306. if( this.value === val ){
  307. this.selected = checked;
  308. } else if( !self.options.multiple ){
  309. this.selected = false;
  310. }
  311. });
  312. // some additional single select-specific logic
  313. if( !self.options.multiple ){
  314. self.labels.removeClass('ui-state-active');
  315. $this.closest('label').toggleClass('ui-state-active', checked );
  316. // close menu
  317. self.close();
  318. }
  319. // fire change on the select box
  320. self.element.trigger("change");
  321. // setTimeout is to fix multiselect issue #14 and #47. caused by jQuery issue #3827
  322. // http://bugs.jquery.com/ticket/3827
  323. setTimeout($.proxy(self.update, self), 10);
  324. });
  325. // close each widget when clicking on any other element/anywhere else on the page
  326. $(document).bind('mousedown.multiselect', function( e ){
  327. if(self._isOpen && !$.contains(self.menu[0], e.target) && !$.contains(self.button[0], e.target) && e.target !== self.button[0]){
  328. self.close();
  329. }
  330. });
  331. // deal with form resets. the problem here is that buttons aren't
  332. // restored to their defaultValue prop on form reset, and the reset
  333. // handler fires before the form is actually reset. delaying it a bit
  334. // gives the form inputs time to clear.
  335. $(this.element[0].form).bind('reset.multiselect', function(){
  336. setTimeout(function(){ self.update(); }, 10);
  337. });
  338. },
  339. // set button width
  340. _setButtonWidth: function(){
  341. var width = this.element.outerWidth(),
  342. o = this.options;
  343. if( /\d/.test(o.minWidth) && width < o.minWidth){
  344. width = o.minWidth;
  345. }
  346. // set widths
  347. this.button.width( width );
  348. },
  349. // set menu width
  350. _setMenuWidth: function(){
  351. var m = this.menu,
  352. width = this.button.outerWidth()-
  353. parseInt(m.css('padding-left'),10)-
  354. parseInt(m.css('padding-right'),10)-
  355. parseInt(m.css('border-right-width'),10)-
  356. parseInt(m.css('border-left-width'),10);
  357. m.width( width || this.button.outerWidth() );
  358. },
  359. // move up or down within the menu
  360. _traverse: function( which, start ){
  361. var $start = $(start),
  362. moveToLast = which === 38 || which === 37,
  363. // select the first li that isn't an optgroup label / disabled
  364. $next = $start.parent()[moveToLast ? 'prevAll' : 'nextAll']('li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup-label)')[ moveToLast ? 'last' : 'first']();
  365. // if at the first/last element
  366. if( !$next.length ){
  367. var $container = this.menu.find('ul:last');
  368. // move to the first/last
  369. this.menu.find('label')[ moveToLast ? 'last' : 'first' ]().trigger('mouseover');
  370. // set scroll position
  371. $container.scrollTop( moveToLast ? $container.height() : 0 );
  372. } else {
  373. $next.find('label').trigger('mouseover');
  374. }
  375. },
  376. // This is an internal function to toggle the checked property and
  377. // other related attributes of a checkbox.
  378. //
  379. // The context of this function should be a checkbox; do not proxy it.
  380. _toggleCheckbox: function( prop, flag ){
  381. return function(){
  382. !this.disabled && (this[ prop ] = flag);
  383. if( flag ){
  384. this.setAttribute('aria-selected', true);
  385. } else {
  386. this.removeAttribute('aria-selected');
  387. }
  388. }
  389. },
  390. _toggleChecked: function( flag, group ){
  391. var $inputs = (group && group.length) ?
  392. group :
  393. this.labels.find('input'),
  394. self = this;
  395. // toggle state on inputs
  396. $inputs.each(this._toggleCheckbox('checked', flag));
  397. // update button text
  398. this.update();
  399. // gather an array of the values that actually changed
  400. var values = $inputs.map(function(){
  401. return this.value;
  402. }).get();
  403. // toggle state on original option tags
  404. this.element
  405. .find('option')
  406. .each(function(){
  407. if( !this.disabled && $.inArray(this.value, values) > -1 ){
  408. self._toggleCheckbox('selected', flag).call( this );
  409. }
  410. });
  411. // trigger the change event on the select
  412. if( $inputs.length ) {
  413. this.element.trigger("change");
  414. }
  415. },
  416. _toggleDisabled: function( flag ){
  417. this.button
  418. .attr({ 'disabled':flag, 'aria-disabled':flag })[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled');
  419. this.menu
  420. .find('input')
  421. .attr({ 'disabled':flag, 'aria-disabled':flag })
  422. .parent()[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled');
  423. this.element
  424. .attr({ 'disabled':flag, 'aria-disabled':flag });
  425. },
  426. // open the menu
  427. open: function( e ){
  428. var self = this,
  429. button = this.button,
  430. menu = this.menu,
  431. speed = this.speed,
  432. o = this.options;
  433. // bail if the multiselectopen event returns false, this widget is disabled, or is already open
  434. if( this._trigger('beforeopen') === false || button.hasClass('ui-state-disabled') || this._isOpen ){
  435. return;
  436. }
  437. var $container = menu.find('ul:last'),
  438. effect = o.show,
  439. pos = button.position();
  440. // figure out opening effects/speeds
  441. if( $.isArray(o.show) ){
  442. effect = o.show[0];
  443. speed = o.show[1] || self.speed;
  444. }
  445. // set the scroll of the checkbox container
  446. $container.scrollTop(0).height(o.height);
  447. // position and show menu
  448. if( $.ui.position && !$.isEmptyObject(o.position) ){
  449. o.position.of = o.position.of || button;
  450. menu
  451. .show()
  452. .position( o.position )
  453. .hide()
  454. .show( effect, speed );
  455. // if position utility is not available...
  456. } else {
  457. menu.css({
  458. top: pos.top+button.outerHeight(),
  459. left: pos.left
  460. }).show( effect, speed );
  461. }
  462. // select the first option
  463. // triggering both mouseover and mouseover because 1.4.2+ has a bug where triggering mouseover
  464. // will actually trigger mouseenter. the mouseenter trigger is there for when it's eventually fixed
  465. this.labels.eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus');
  466. button.addClass('ui-state-active');
  467. this._isOpen = true;
  468. this._trigger('open');
  469. },
  470. // close the menu
  471. close: function(){
  472. if(this._trigger('beforeclose') === false){
  473. return;
  474. }
  475. var o = this.options, effect = o.hide, speed = this.speed;
  476. // figure out opening effects/speeds
  477. if( $.isArray(o.hide) ){
  478. effect = o.hide[0];
  479. speed = o.hide[1] || this.speed;
  480. }
  481. this.menu.hide(effect, speed);
  482. this.button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave');
  483. this._isOpen = false;
  484. this._trigger('close');
  485. },
  486. enable: function(){
  487. this._toggleDisabled(false);
  488. },
  489. disable: function(){
  490. this._toggleDisabled(true);
  491. },
  492. checkAll: function( e ){
  493. this._toggleChecked(true);
  494. this._trigger('checkAll');
  495. },
  496. uncheckAll: function(){
  497. this._toggleChecked(false);
  498. this._trigger('uncheckAll');
  499. },
  500. getChecked: function(){
  501. return this.menu.find('input').filter(':checked');
  502. },
  503. destroy: function(){
  504. // remove classes + data
  505. $.Widget.prototype.destroy.call( this );
  506. this.button.remove();
  507. this.menu.remove();
  508. this.element.show();
  509. return this;
  510. },
  511. isOpen: function(){
  512. return this._isOpen;
  513. },
  514. widget: function(){
  515. return this.menu;
  516. },
  517. // react to option changes after initialization
  518. _setOption: function( key, value ){
  519. var menu = this.menu;
  520. switch(key){
  521. case 'header':
  522. menu.find('div.ui-multiselect-header')[ value ? 'show' : 'hide' ]();
  523. break;
  524. case 'checkAllText':
  525. menu.find('a.ui-multiselect-all span').eq(-1).text(value);
  526. break;
  527. case 'uncheckAllText':
  528. menu.find('a.ui-multiselect-none span').eq(-1).text(value);
  529. break;
  530. case 'height':
  531. menu.find('ul:last').height( parseInt(value,10) );
  532. break;
  533. case 'minWidth':
  534. this.options[ key ] = parseInt(value,10);
  535. this._setButtonWidth();
  536. this._setMenuWidth();
  537. break;
  538. case 'selectedText':
  539. case 'selectedList':
  540. case 'noneSelectedText':
  541. this.options[key] = value; // these all needs to update immediately for the update() call
  542. this.update();
  543. break;
  544. case 'classes':
  545. menu.add(this.button).removeClass(this.options.classes).addClass(value);
  546. break;
  547. }
  548. $.Widget.prototype._setOption.apply( this, arguments );
  549. }
  550. });
  551. })(jQuery);