jquery.multi-select.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. /*
  2. * MultiSelect v0.9.12
  3. * Copyright (c) 2012 Louis Cuny
  4. *
  5. * This program is free software. It comes without any warranty, to
  6. * the extent permitted by applicable law. You can redistribute it
  7. * and/or modify it under the terms of the Do What The Fuck You Want
  8. * To Public License, Version 2, as published by Sam Hocevar. See
  9. * http://sam.zoy.org/wtfpl/COPYING for more details.
  10. */
  11. !function ($) {
  12. "use strict";
  13. /* MULTISELECT CLASS DEFINITION
  14. * ====================== */
  15. var MultiSelect = function (element, options) {
  16. this.options = options;
  17. this.$element = $(element);
  18. this.$container = $('<div/>', { 'class': "ms-container" });
  19. this.$selectableContainer = $('<div/>', { 'class': 'ms-selectable' });
  20. this.$selectionContainer = $('<div/>', { 'class': 'ms-selection' });
  21. this.$selectableUl = $('<ul/>', { 'class': "ms-list", 'tabindex' : '-1' });
  22. this.$selectionUl = $('<ul/>', { 'class': "ms-list", 'tabindex' : '-1' });
  23. this.scrollTo = 0;
  24. this.elemsSelector = 'li:visible:not(.ms-optgroup-label,.ms-optgroup-container,.'+options.disabledClass+')';
  25. };
  26. MultiSelect.prototype = {
  27. constructor: MultiSelect,
  28. init: function(){
  29. var that = this,
  30. ms = this.$element;
  31. if (ms.next('.ms-container').length === 0){
  32. ms.css({ position: 'absolute', left: '-9999px' });
  33. ms.attr('id', ms.attr('id') ? ms.attr('id') : Math.ceil(Math.random()*1000)+'multiselect');
  34. this.$container.attr('id', 'ms-'+ms.attr('id'));
  35. this.$container.addClass(that.options.cssClass);
  36. ms.find('option').each(function(){
  37. that.generateLisFromOption(this);
  38. });
  39. this.$selectionUl.find('.ms-optgroup-label').hide();
  40. if (that.options.selectableHeader){
  41. that.$selectableContainer.append(that.options.selectableHeader);
  42. }
  43. that.$selectableContainer.append(that.$selectableUl);
  44. if (that.options.selectableFooter){
  45. that.$selectableContainer.append(that.options.selectableFooter);
  46. }
  47. if (that.options.selectionHeader){
  48. that.$selectionContainer.append(that.options.selectionHeader);
  49. }
  50. that.$selectionContainer.append(that.$selectionUl);
  51. if (that.options.selectionFooter){
  52. that.$selectionContainer.append(that.options.selectionFooter);
  53. }
  54. that.$container.append(that.$selectableContainer);
  55. that.$container.append(that.$selectionContainer);
  56. ms.after(that.$container);
  57. that.activeMouse(that.$selectableUl);
  58. that.activeKeyboard(that.$selectableUl);
  59. var action = that.options.dblClick ? 'dblclick' : 'click';
  60. that.$selectableUl.on(action, '.ms-elem-selectable', function(){
  61. that.select($(this).data('ms-value'));
  62. });
  63. that.$selectionUl.on(action, '.ms-elem-selection', function(){
  64. that.deselect($(this).data('ms-value'));
  65. });
  66. that.activeMouse(that.$selectionUl);
  67. that.activeKeyboard(that.$selectionUl);
  68. ms.on('focus', function(){
  69. that.$selectableUl.focus();
  70. });
  71. }
  72. var selectedValues = ms.find('option:selected').map(function(){ return $(this).val(); }).get();
  73. that.select(selectedValues, 'init');
  74. if (typeof that.options.afterInit === 'function') {
  75. that.options.afterInit.call(this, this.$container);
  76. }
  77. },
  78. 'generateLisFromOption' : function(option, index, $container){
  79. var that = this,
  80. ms = that.$element,
  81. attributes = "",
  82. $option = $(option);
  83. for (var cpt = 0; cpt < option.attributes.length; cpt++){
  84. var attr = option.attributes[cpt];
  85. if(attr.name !== 'value' && attr.name !== 'disabled'){
  86. attributes += attr.name+'="'+attr.value+'" ';
  87. }
  88. }
  89. var selectableLi = $('<li '+attributes+'><span>'+that.escapeHTML($option.text())+'</span></li>'),
  90. selectedLi = selectableLi.clone(),
  91. value = $option.val(),
  92. elementId = that.sanitize(value);
  93. selectableLi
  94. .data('ms-value', value)
  95. .addClass('ms-elem-selectable')
  96. .attr('id', elementId+'-selectable');
  97. selectedLi
  98. .data('ms-value', value)
  99. .addClass('ms-elem-selection')
  100. .attr('id', elementId+'-selection')
  101. .hide();
  102. if ($option.prop('disabled') || ms.prop('disabled')){
  103. selectedLi.addClass(that.options.disabledClass);
  104. selectableLi.addClass(that.options.disabledClass);
  105. }
  106. var $optgroup = $option.parent('optgroup');
  107. if ($optgroup.length > 0){
  108. var optgroupLabel = $optgroup.attr('label'),
  109. optgroupId = that.sanitize(optgroupLabel),
  110. $selectableOptgroup = that.$selectableUl.find('#optgroup-selectable-'+optgroupId),
  111. $selectionOptgroup = that.$selectionUl.find('#optgroup-selection-'+optgroupId);
  112. if ($selectableOptgroup.length === 0){
  113. var optgroupContainerTpl = '<li class="ms-optgroup-container"></li>',
  114. optgroupTpl = '<ul class="ms-optgroup"><li class="ms-optgroup-label"><span>'+optgroupLabel+'</span></li></ul>';
  115. $selectableOptgroup = $(optgroupContainerTpl);
  116. $selectionOptgroup = $(optgroupContainerTpl);
  117. $selectableOptgroup.attr('id', 'optgroup-selectable-'+optgroupId);
  118. $selectionOptgroup.attr('id', 'optgroup-selection-'+optgroupId);
  119. $selectableOptgroup.append($(optgroupTpl));
  120. $selectionOptgroup.append($(optgroupTpl));
  121. if (that.options.selectableOptgroup){
  122. $selectableOptgroup.find('.ms-optgroup-label').on('click', function(){
  123. var values = $optgroup.children(':not(:selected, :disabled)').map(function(){ return $(this).val();}).get();
  124. that.select(values);
  125. });
  126. $selectionOptgroup.find('.ms-optgroup-label').on('click', function(){
  127. var values = $optgroup.children(':selected:not(:disabled)').map(function(){ return $(this).val();}).get();
  128. that.deselect(values);
  129. });
  130. }
  131. that.$selectableUl.append($selectableOptgroup);
  132. that.$selectionUl.append($selectionOptgroup);
  133. }
  134. index = index === undefined ? $selectableOptgroup.find('ul').children().length : index + 1;
  135. selectableLi.insertAt(index, $selectableOptgroup.children());
  136. selectedLi.insertAt(index, $selectionOptgroup.children());
  137. } else {
  138. index = index === undefined ? that.$selectableUl.children().length : index;
  139. selectableLi.insertAt(index, that.$selectableUl);
  140. selectedLi.insertAt(index, that.$selectionUl);
  141. }
  142. },
  143. 'addOption' : function(options){
  144. var that = this;
  145. if (options.value !== undefined && options.value !== null){
  146. options = [options];
  147. }
  148. $.each(options, function(index, option){
  149. if (option.value !== undefined && option.value !== null &&
  150. that.$element.find("option[value='"+option.value+"']").length === 0){
  151. var $option = $('<option value="'+option.value+'">'+option.text+'</option>'),
  152. $container = option.nested === undefined ? that.$element : $("optgroup[label='"+option.nested+"']"),
  153. index = parseInt((typeof option.index === 'undefined' ? $container.children().length : option.index));
  154. if (option.optionClass) {
  155. $option.addClass(option.optionClass);
  156. }
  157. if (option.disabled) {
  158. $option.prop('disabled', true);
  159. }
  160. $option.insertAt(index, $container);
  161. that.generateLisFromOption($option.get(0), index, option.nested);
  162. }
  163. });
  164. },
  165. 'escapeHTML' : function(text){
  166. return $("<div>").text(text).html();
  167. },
  168. 'activeKeyboard' : function($list){
  169. var that = this;
  170. $list.on('focus', function(){
  171. $(this).addClass('ms-focus');
  172. })
  173. .on('blur', function(){
  174. $(this).removeClass('ms-focus');
  175. })
  176. .on('keydown', function(e){
  177. switch (e.which) {
  178. case 40:
  179. case 38:
  180. e.preventDefault();
  181. e.stopPropagation();
  182. that.moveHighlight($(this), (e.which === 38) ? -1 : 1);
  183. return;
  184. case 37:
  185. case 39:
  186. e.preventDefault();
  187. e.stopPropagation();
  188. that.switchList($list);
  189. return;
  190. case 9:
  191. if(that.$element.is('[tabindex]')){
  192. e.preventDefault();
  193. var tabindex = parseInt(that.$element.attr('tabindex'), 10);
  194. tabindex = (e.shiftKey) ? tabindex-1 : tabindex+1;
  195. $('[tabindex="'+(tabindex)+'"]').focus();
  196. return;
  197. }else{
  198. if(e.shiftKey){
  199. that.$element.trigger('focus');
  200. }
  201. }
  202. }
  203. if($.inArray(e.which, that.options.keySelect) > -1){
  204. e.preventDefault();
  205. e.stopPropagation();
  206. that.selectHighlighted($list);
  207. return;
  208. }
  209. });
  210. },
  211. 'moveHighlight': function($list, direction){
  212. var $elems = $list.find(this.elemsSelector),
  213. $currElem = $elems.filter('.ms-hover'),
  214. $nextElem = null,
  215. elemHeight = $elems.first().outerHeight(),
  216. containerHeight = $list.height(),
  217. containerSelector = '#'+this.$container.prop('id');
  218. $elems.removeClass('ms-hover');
  219. if (direction === 1){ // DOWN
  220. $nextElem = $currElem.nextAll(this.elemsSelector).first();
  221. if ($nextElem.length === 0){
  222. var $optgroupUl = $currElem.parent();
  223. if ($optgroupUl.hasClass('ms-optgroup')){
  224. var $optgroupLi = $optgroupUl.parent(),
  225. $nextOptgroupLi = $optgroupLi.next(':visible');
  226. if ($nextOptgroupLi.length > 0){
  227. $nextElem = $nextOptgroupLi.find(this.elemsSelector).first();
  228. } else {
  229. $nextElem = $elems.first();
  230. }
  231. } else {
  232. $nextElem = $elems.first();
  233. }
  234. }
  235. } else if (direction === -1){ // UP
  236. $nextElem = $currElem.prevAll(this.elemsSelector).first();
  237. if ($nextElem.length === 0){
  238. var $optgroupUl = $currElem.parent();
  239. if ($optgroupUl.hasClass('ms-optgroup')){
  240. var $optgroupLi = $optgroupUl.parent(),
  241. $prevOptgroupLi = $optgroupLi.prev(':visible');
  242. if ($prevOptgroupLi.length > 0){
  243. $nextElem = $prevOptgroupLi.find(this.elemsSelector).last();
  244. } else {
  245. $nextElem = $elems.last();
  246. }
  247. } else {
  248. $nextElem = $elems.last();
  249. }
  250. }
  251. }
  252. if ($nextElem.length > 0){
  253. $nextElem.addClass('ms-hover');
  254. var scrollTo = $list.scrollTop() + $nextElem.position().top -
  255. containerHeight / 2 + elemHeight / 2;
  256. $list.scrollTop(scrollTo);
  257. }
  258. },
  259. 'selectHighlighted' : function($list){
  260. var $elems = $list.find(this.elemsSelector),
  261. $highlightedElem = $elems.filter('.ms-hover').first();
  262. if ($highlightedElem.length > 0){
  263. if ($list.parent().hasClass('ms-selectable')){
  264. this.select($highlightedElem.data('ms-value'));
  265. } else {
  266. this.deselect($highlightedElem.data('ms-value'));
  267. }
  268. $elems.removeClass('ms-hover');
  269. }
  270. },
  271. 'switchList' : function($list){
  272. $list.blur();
  273. this.$container.find(this.elemsSelector).removeClass('ms-hover');
  274. if ($list.parent().hasClass('ms-selectable')){
  275. this.$selectionUl.focus();
  276. } else {
  277. this.$selectableUl.focus();
  278. }
  279. },
  280. 'activeMouse' : function($list){
  281. var that = this;
  282. this.$container.on('mouseenter', that.elemsSelector, function(){
  283. $(this).parents('.ms-container').find(that.elemsSelector).removeClass('ms-hover');
  284. $(this).addClass('ms-hover');
  285. });
  286. this.$container.on('mouseleave', that.elemsSelector, function () {
  287. $(this).parents('.ms-container').find(that.elemsSelector).removeClass('ms-hover');
  288. });
  289. },
  290. 'refresh' : function() {
  291. this.destroy();
  292. this.$element.multiSelect(this.options);
  293. },
  294. 'destroy' : function(){
  295. $("#ms-"+this.$element.attr("id")).remove();
  296. this.$element.off('focus');
  297. this.$element.css('position', '').css('left', '');
  298. this.$element.removeData('multiselect');
  299. },
  300. 'select' : function(value, method){
  301. if (typeof value === 'string'){ value = [value]; }
  302. var that = this,
  303. ms = this.$element,
  304. msIds = $.map(value, function(val){ return(that.sanitize(val)); }),
  305. selectables = this.$selectableUl.find('#' + msIds.join('-selectable, #')+'-selectable').filter(':not(.'+that.options.disabledClass+')'),
  306. selections = this.$selectionUl.find('#' + msIds.join('-selection, #') + '-selection').filter(':not(.'+that.options.disabledClass+')'),
  307. options = ms.find('option:not(:disabled)').filter(function(){ return($.inArray(this.value, value) > -1); });
  308. if (method === 'init'){
  309. selectables = this.$selectableUl.find('#' + msIds.join('-selectable, #')+'-selectable'),
  310. selections = this.$selectionUl.find('#' + msIds.join('-selection, #') + '-selection');
  311. }
  312. if (selectables.length > 0){
  313. selectables.addClass('ms-selected').hide();
  314. selections.addClass('ms-selected').show();
  315. options.prop('selected', true);
  316. that.$container.find(that.elemsSelector).removeClass('ms-hover');
  317. var selectableOptgroups = that.$selectableUl.children('.ms-optgroup-container');
  318. if (selectableOptgroups.length > 0){
  319. selectableOptgroups.each(function(){
  320. var selectablesLi = $(this).find('.ms-elem-selectable');
  321. if (selectablesLi.length === selectablesLi.filter('.ms-selected').length){
  322. $(this).find('.ms-optgroup-label').hide();
  323. }
  324. });
  325. var selectionOptgroups = that.$selectionUl.children('.ms-optgroup-container');
  326. selectionOptgroups.each(function(){
  327. var selectionsLi = $(this).find('.ms-elem-selection');
  328. if (selectionsLi.filter('.ms-selected').length > 0){
  329. $(this).find('.ms-optgroup-label').show();
  330. }
  331. });
  332. } else {
  333. if (that.options.keepOrder && method !== 'init'){
  334. var selectionLiLast = that.$selectionUl.find('.ms-selected');
  335. if((selectionLiLast.length > 1) && (selectionLiLast.last().get(0) != selections.get(0))) {
  336. selections.insertAfter(selectionLiLast.last());
  337. }
  338. }
  339. }
  340. if (method !== 'init'){
  341. ms.trigger('change');
  342. if (typeof that.options.afterSelect === 'function') {
  343. that.options.afterSelect.call(this, value);
  344. }
  345. }
  346. }
  347. },
  348. 'deselect' : function(value){
  349. if (typeof value === 'string'){ value = [value]; }
  350. var that = this,
  351. ms = this.$element,
  352. msIds = $.map(value, function(val){ return(that.sanitize(val)); }),
  353. selectables = this.$selectableUl.find('#' + msIds.join('-selectable, #')+'-selectable'),
  354. selections = this.$selectionUl.find('#' + msIds.join('-selection, #')+'-selection').filter('.ms-selected').filter(':not(.'+that.options.disabledClass+')'),
  355. options = ms.find('option').filter(function(){ return($.inArray(this.value, value) > -1); });
  356. if (selections.length > 0){
  357. selectables.removeClass('ms-selected').show();
  358. selections.removeClass('ms-selected').hide();
  359. options.prop('selected', false);
  360. that.$container.find(that.elemsSelector).removeClass('ms-hover');
  361. var selectableOptgroups = that.$selectableUl.children('.ms-optgroup-container');
  362. if (selectableOptgroups.length > 0){
  363. selectableOptgroups.each(function(){
  364. var selectablesLi = $(this).find('.ms-elem-selectable');
  365. if (selectablesLi.filter(':not(.ms-selected)').length > 0){
  366. $(this).find('.ms-optgroup-label').show();
  367. }
  368. });
  369. var selectionOptgroups = that.$selectionUl.children('.ms-optgroup-container');
  370. selectionOptgroups.each(function(){
  371. var selectionsLi = $(this).find('.ms-elem-selection');
  372. if (selectionsLi.filter('.ms-selected').length === 0){
  373. $(this).find('.ms-optgroup-label').hide();
  374. }
  375. });
  376. }
  377. ms.trigger('change');
  378. if (typeof that.options.afterDeselect === 'function') {
  379. that.options.afterDeselect.call(this, value);
  380. }
  381. }
  382. },
  383. 'select_all' : function(){
  384. var ms = this.$element,
  385. values = ms.val();
  386. ms.find('option:not(":disabled")').prop('selected', true);
  387. this.$selectableUl.find('.ms-elem-selectable').filter(':not(.'+this.options.disabledClass+')').addClass('ms-selected').hide();
  388. this.$selectionUl.find('.ms-optgroup-label').show();
  389. this.$selectableUl.find('.ms-optgroup-label').hide();
  390. this.$selectionUl.find('.ms-elem-selection').filter(':not(.'+this.options.disabledClass+')').addClass('ms-selected').show();
  391. this.$selectionUl.focus();
  392. ms.trigger('change');
  393. if (typeof this.options.afterSelect === 'function') {
  394. var selectedValues = $.grep(ms.val(), function(item){
  395. return $.inArray(item, values) < 0;
  396. });
  397. this.options.afterSelect.call(this, selectedValues);
  398. }
  399. },
  400. 'deselect_all' : function(){
  401. var ms = this.$element,
  402. values = ms.val();
  403. ms.find('option').prop('selected', false);
  404. this.$selectableUl.find('.ms-elem-selectable').removeClass('ms-selected').show();
  405. this.$selectionUl.find('.ms-optgroup-label').hide();
  406. this.$selectableUl.find('.ms-optgroup-label').show();
  407. this.$selectionUl.find('.ms-elem-selection').removeClass('ms-selected').hide();
  408. this.$selectableUl.focus();
  409. ms.trigger('change');
  410. if (typeof this.options.afterDeselect === 'function') {
  411. this.options.afterDeselect.call(this, values);
  412. }
  413. },
  414. sanitize: function(value){
  415. var hash = 0, i, character;
  416. if (value.length == 0) return hash;
  417. var ls = 0;
  418. for (i = 0, ls = value.length; i < ls; i++) {
  419. character = value.charCodeAt(i);
  420. hash = ((hash<<5)-hash)+character;
  421. hash |= 0; // Convert to 32bit integer
  422. }
  423. return hash;
  424. }
  425. };
  426. /* MULTISELECT PLUGIN DEFINITION
  427. * ======================= */
  428. $.fn.multiSelect = function () {
  429. var option = arguments[0],
  430. args = arguments;
  431. return this.each(function () {
  432. var $this = $(this),
  433. data = $this.data('multiselect'),
  434. options = $.extend({}, $.fn.multiSelect.defaults, $this.data(), typeof option === 'object' && option);
  435. if (!data){ $this.data('multiselect', (data = new MultiSelect(this, options))); }
  436. if (typeof option === 'string'){
  437. data[option](args[1]);
  438. } else {
  439. data.init();
  440. }
  441. });
  442. };
  443. $.fn.multiSelect.defaults = {
  444. keySelect: [32],
  445. selectableOptgroup: false,
  446. disabledClass : 'disabled',
  447. dblClick : false,
  448. keepOrder: false,
  449. cssClass: ''
  450. };
  451. $.fn.multiSelect.Constructor = MultiSelect;
  452. $.fn.insertAt = function(index, $parent) {
  453. return this.each(function() {
  454. if (index === 0) {
  455. $parent.prepend(this);
  456. } else {
  457. $parent.children().eq(index - 1).after(this);
  458. }
  459. });
  460. };
  461. }(window.jQuery);