jquery.multiselect.js 18 KB

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