ui.multiselect.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. /*
  2. * jQuery UI Multiselect
  3. *
  4. * Authors:
  5. * Michael Aufreiter (quasipartikel.at)
  6. * Yanick Rochon (yanick.rochon[at]gmail[dot]com)
  7. *
  8. * Dual licensed under the MIT (MIT-LICENSE.txt)
  9. * and GPL (GPL-LICENSE.txt) licenses.
  10. *
  11. * http://www.quasipartikel.at/multiselect/
  12. *
  13. *
  14. * Depends:
  15. * ui.core.js
  16. * ui.sortable.js
  17. *
  18. * Optional:
  19. * localization (http://plugins.jquery.com/project/localisation)
  20. * scrollTo (http://plugins.jquery.com/project/ScrollTo)
  21. *
  22. * Todo:
  23. * Make batch actions faster
  24. * Implement dynamic insertion through remote calls
  25. */
  26. (function($) {
  27. $.widget("ui.multiselect", {
  28. _init: function() {
  29. this.element.hide();
  30. this.id = this.element.attr("id");
  31. this.container = $('<div class="ui-multiselect ui-helper-clearfix ui-widget"></div>').insertAfter(this.element);
  32. this.count = 0; // number of currently selected options
  33. this.selectedContainer = $('<div class="selected"></div>').appendTo(this.container);
  34. this.availableContainer = $('<div class="available"></div>').appendTo(this.container);
  35. this.selectedActions = $('<div class="actions ui-widget-header ui-helper-clearfix"><span class="count">0 '+$.ui.multiselect.locale.itemsCount+'</span><a href="#" class="remove-all">'+$.ui.multiselect.locale.removeAll+'</a></div>').appendTo(this.selectedContainer);
  36. this.availableActions = $('<div class="actions ui-widget-header ui-helper-clearfix"><input type="text" class="search empty ui-widget-content ui-corner-all"/><a href="#" class="add-all">'+$.ui.multiselect.locale.addAll+'</a></div>').appendTo(this.availableContainer);
  37. this.selectedList = $('<ul class="selected connected-list"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.selectedContainer);
  38. this.availableList = $('<ul class="available connected-list"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.availableContainer);
  39. var that = this;
  40. // set dimensions
  41. this.container.width(this.element.width()+1);
  42. this.selectedContainer.width(Math.floor(this.element.width()*this.options.dividerLocation));
  43. this.availableContainer.width(Math.floor(this.element.width()*(1-this.options.dividerLocation)));
  44. // fix list height to match <option> depending on their individual header's heights
  45. this.selectedList.height(Math.max(this.element.height()-this.selectedActions.height(),1));
  46. this.availableList.height(Math.max(this.element.height()-this.availableActions.height(),1));
  47. if ( !this.options.animated ) {
  48. this.options.show = 'show';
  49. this.options.hide = 'hide';
  50. }
  51. // init lists
  52. this._populateLists(this.element.find('option'));
  53. // make selection sortable
  54. if (this.options.sortable) {
  55. $("ul.selected").sortable({
  56. placeholder: 'ui-state-highlight',
  57. axis: 'y',
  58. update: function(event, ui) {
  59. // apply the new sort order to the original selectbox
  60. that.selectedList.find('li').each(function() {
  61. if ($(this).data('optionLink'))
  62. $(this).data('optionLink').remove().appendTo(that.element);
  63. });
  64. },
  65. receive: function(event, ui) {
  66. ui.item.data('optionLink').attr('selected', true);
  67. // increment count
  68. that.count += 1;
  69. that._updateCount();
  70. // workaround, because there's no way to reference
  71. // the new element, see http://dev.jqueryui.com/ticket/4303
  72. that.selectedList.children('.ui-draggable').each(function() {
  73. $(this).removeClass('ui-draggable');
  74. $(this).data('optionLink', ui.item.data('optionLink'));
  75. $(this).data('idx', ui.item.data('idx'));
  76. that._applyItemState($(this), true);
  77. });
  78. // workaround according to http://dev.jqueryui.com/ticket/4088
  79. setTimeout(function() { ui.item.remove(); }, 1);
  80. }
  81. });
  82. }
  83. // set up livesearch
  84. if (this.options.searchable) {
  85. this._registerSearchEvents(this.availableContainer.find('input.search'));
  86. } else {
  87. $('.search').hide();
  88. }
  89. // batch actions
  90. $(".remove-all").click(function() {
  91. that._populateLists(that.element.find('option').removeAttr('selected'));
  92. return false;
  93. });
  94. $(".add-all").click(function() {
  95. that._populateLists(that.element.find('option').attr('selected', 'selected'));
  96. return false;
  97. });
  98. },
  99. destroy: function() {
  100. this.element.show();
  101. this.container.remove();
  102. $.widget.prototype.destroy.apply(this, arguments);
  103. },
  104. _populateLists: function(options) {
  105. this.selectedList.children('.ui-element').remove();
  106. this.availableList.children('.ui-element').remove();
  107. this.count = 0;
  108. var that = this;
  109. var items = $(options.map(function(i) {
  110. var item = that._getOptionNode(this).appendTo(this.selected ? that.selectedList : that.availableList).show();
  111. if (this.selected) that.count += 1;
  112. that._applyItemState(item, this.selected);
  113. item.data('idx', i);
  114. return item[0];
  115. }));
  116. // update count
  117. this._updateCount();
  118. },
  119. _updateCount: function() {
  120. this.selectedContainer.find('span.count').text(this.count+" "+$.ui.multiselect.locale.itemsCount);
  121. },
  122. _getOptionNode: function(option) {
  123. option = $(option);
  124. var node = $('<li class="ui-state-default ui-element" title="'+option.text()+'"><span class="ui-icon"/>'+option.text()+'<a href="#" class="action"><span class="ui-corner-all ui-icon"/></a></li>').hide();
  125. node.data('optionLink', option);
  126. return node;
  127. },
  128. // clones an item with associated data
  129. // didn't find a smarter away around this
  130. _cloneWithData: function(clonee) {
  131. var clone = clonee.clone();
  132. clone.data('optionLink', clonee.data('optionLink'));
  133. clone.data('idx', clonee.data('idx'));
  134. return clone;
  135. },
  136. _setSelected: function(item, selected) {
  137. item.data('optionLink').attr('selected', selected);
  138. if (selected) {
  139. var selectedItem = this._cloneWithData(item);
  140. item[this.options.hide](this.options.animated, function() { $(this).remove(); });
  141. selectedItem.appendTo(this.selectedList).hide()[this.options.show](this.options.animated);
  142. this._applyItemState(selectedItem, true);
  143. return selectedItem;
  144. } else {
  145. // look for successor based on initial option index
  146. var items = this.availableList.find('li'), comparator = this.options.nodeComparator;
  147. var succ = null, i = item.data('idx'), direction = comparator(item, $(items[i]));
  148. // TODO: test needed for dynamic list populating
  149. if ( direction ) {
  150. while (i>=0 && i<items.length) {
  151. direction > 0 ? i++ : i--;
  152. if ( direction != comparator(item, $(items[i])) ) {
  153. // going up, go back one item down, otherwise leave as is
  154. succ = items[direction > 0 ? i : i+1];
  155. break;
  156. }
  157. }
  158. } else {
  159. succ = items[i];
  160. }
  161. var availableItem = this._cloneWithData(item);
  162. succ ? availableItem.insertBefore($(succ)) : availableItem.appendTo(this.availableList);
  163. item[this.options.hide](this.options.animated, function() { $(this).remove(); });
  164. availableItem.hide()[this.options.show](this.options.animated);
  165. this._applyItemState(availableItem, false);
  166. return availableItem;
  167. }
  168. },
  169. _applyItemState: function(item, selected) {
  170. if (selected) {
  171. if (this.options.sortable)
  172. item.children('span').addClass('ui-icon-arrowthick-2-n-s').removeClass('ui-helper-hidden').addClass('ui-icon');
  173. else
  174. item.children('span').removeClass('ui-icon-arrowthick-2-n-s').addClass('ui-helper-hidden').removeClass('ui-icon');
  175. item.find('a.action span').addClass('ui-icon-minus').removeClass('ui-icon-plus');
  176. this._registerRemoveEvents(item.find('a.action'));
  177. } else {
  178. item.children('span').removeClass('ui-icon-arrowthick-2-n-s').addClass('ui-helper-hidden').removeClass('ui-icon');
  179. item.find('a.action span').addClass('ui-icon-plus').removeClass('ui-icon-minus');
  180. this._registerAddEvents(item.find('a.action'));
  181. }
  182. this._registerHoverEvents(item);
  183. },
  184. // taken from John Resig's liveUpdate script
  185. _filter: function(list) {
  186. var input = $(this);
  187. var rows = list.children('li'),
  188. cache = rows.map(function(){
  189. return $(this).text().toLowerCase();
  190. });
  191. var term = $.trim(input.val().toLowerCase()), scores = [];
  192. if (!term) {
  193. rows.show();
  194. } else {
  195. rows.hide();
  196. cache.each(function(i) {
  197. if (this.indexOf(term)>-1) { scores.push(i); }
  198. });
  199. $.each(scores, function() {
  200. $(rows[this]).show();
  201. });
  202. }
  203. },
  204. _registerHoverEvents: function(elements) {
  205. elements.removeClass('ui-state-hover');
  206. elements.mouseover(function() {
  207. $(this).addClass('ui-state-hover');
  208. });
  209. elements.mouseout(function() {
  210. $(this).removeClass('ui-state-hover');
  211. });
  212. },
  213. _registerAddEvents: function(elements) {
  214. var that = this;
  215. elements.click(function() {
  216. var item = that._setSelected($(this).parent(), true);
  217. that.count += 1;
  218. that._updateCount();
  219. return false;
  220. })
  221. // make draggable
  222. .each(function() {
  223. $(this).parent().draggable({
  224. connectToSortable: 'ul.selected',
  225. helper: function() {
  226. var selectedItem = that._cloneWithData($(this)).width($(this).width() - 50);
  227. selectedItem.width($(this).width());
  228. return selectedItem;
  229. },
  230. appendTo: '.ui-multiselect',
  231. containment: '.ui-multiselect',
  232. revert: 'invalid'
  233. });
  234. });
  235. },
  236. _registerRemoveEvents: function(elements) {
  237. var that = this;
  238. elements.click(function() {
  239. that._setSelected($(this).parent(), false);
  240. that.count -= 1;
  241. that._updateCount();
  242. return false;
  243. });
  244. },
  245. _registerSearchEvents: function(input) {
  246. var that = this;
  247. input.focus(function() {
  248. $(this).addClass('ui-state-active');
  249. })
  250. .blur(function() {
  251. $(this).removeClass('ui-state-active');
  252. })
  253. .keypress(function(e) {
  254. if (e.keyCode == 13)
  255. return false;
  256. })
  257. .keyup(function() {
  258. that._filter.apply(this, [that.availableList]);
  259. });
  260. }
  261. });
  262. $.extend($.ui.multiselect, {
  263. defaults: {
  264. sortable: true,
  265. searchable: true,
  266. animated: 'fast',
  267. show: 'slideDown',
  268. hide: 'slideUp',
  269. dividerLocation: 0.6,
  270. nodeComparator: function(node1,node2) {
  271. var text1 = node1.text(),
  272. text2 = node2.text();
  273. return text1 == text2 ? 0 : (text1 < text2 ? -1 : 1);
  274. }
  275. },
  276. locale: {
  277. addAll:'Add all',
  278. removeAll:'Remove all',
  279. itemsCount:'items selected'
  280. }
  281. });
  282. })(jQuery);