ui.multiselect.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. /**
  2. * @license 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. * UPDATED by Oleg Kiriljuk (oleg.kiriljuk@ok-soft-gmbh.com) to support jQuery 1.6 and hight
  14. * (the usage of jQuery.attr and jQuery.removeAttr is replaced to the usage of jQuery.prop
  15. * in case of working with selected options of select)
  16. *
  17. * Depends:
  18. * ui.core.js
  19. * ui.sortable.js
  20. *
  21. * Optional:
  22. * localization (http://plugins.jquery.com/project/localisation)
  23. * scrollTo (http://plugins.jquery.com/project/ScrollTo)
  24. *
  25. * Todo:
  26. * Make batch actions faster
  27. * Implement dynamic insertion through remote calls
  28. */
  29. /*global jQuery, define, module, require */
  30. (function (factory) {
  31. "use strict";
  32. if (typeof define === "function" && define.amd) {
  33. // AMD. Register as an anonymous module.
  34. define([
  35. "jquery",
  36. "jquery-ui/sortable"
  37. ], factory);
  38. } else if (typeof module === "object" && module.exports) {
  39. // Node/CommonJS
  40. module.exports = function (root, $) {
  41. if ($ === undefined) {
  42. // require("jquery") returns a factory that requires window to
  43. // build a jQuery instance, we normalize how we use modules
  44. // that require this pattern but the window provided is a noop
  45. // if it's defined (how jquery works)
  46. $ = typeof window !== "undefined" ?
  47. require("jquery") :
  48. require("jquery")(root || window);
  49. }
  50. require("jquery-ui/sortable");
  51. factory($);
  52. return $;
  53. };
  54. } else {
  55. // Browser globals
  56. factory(jQuery);
  57. }
  58. }(function ($) {
  59. $.widget("ui.multiselect", {
  60. options: {
  61. sortable: true,
  62. searchable: true,
  63. doubleClickable: true,
  64. animated: 'fast',
  65. show: 'slideDown',
  66. hide: 'slideUp',
  67. dividerLocation: 0.6,
  68. availableFirst: false,
  69. nodeComparator: function(node1,node2) {
  70. var text1 = node1.text(),
  71. text2 = node2.text();
  72. return text1 == text2 ? 0 : (text1 < text2 ? -1 : 1);
  73. }
  74. },
  75. _create: function() {
  76. this.element.hide();
  77. this.id = this.element.attr("id");
  78. this.container = $('<div class="ui-multiselect ui-helper-clearfix ui-widget"></div>').insertAfter(this.element);
  79. this.count = 0; // number of currently selected options
  80. this.selectedContainer = $('<div class="selected"></div>').appendTo(this.container);
  81. this.availableContainer = $('<div class="available"></div>')[this.options.availableFirst?'prependTo': 'appendTo'](this.container);
  82. 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);
  83. 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);
  84. this.selectedList = $('<ul class="selected connected-list"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.selectedContainer);
  85. this.availableList = $('<ul class="available connected-list"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.availableContainer);
  86. var that = this;
  87. // set dimensions
  88. this.container.width(this.element.width()+1);
  89. this.selectedContainer.width(Math.floor(this.element.width()*this.options.dividerLocation));
  90. this.availableContainer.width(Math.floor(this.element.width()*(1-this.options.dividerLocation)));
  91. // fix list height to match <option> depending on their individual header's heights
  92. this.selectedList.height(Math.max(this.element.height()-this.selectedActions.height(),1));
  93. this.availableList.height(Math.max(this.element.height()-this.availableActions.height(),1));
  94. if ( !this.options.animated ) {
  95. this.options.show = 'show';
  96. this.options.hide = 'hide';
  97. }
  98. // init lists
  99. this._populateLists(this.element.find('option'));
  100. // make selection sortable
  101. if (this.options.sortable) {
  102. this.selectedList.sortable({
  103. placeholder: 'ui-state-highlight',
  104. axis: 'y',
  105. update: function(event, ui) {
  106. // apply the new sort order to the original selectbox
  107. that.selectedList.find('li').each(function() {
  108. if ($(this).data('optionLink')) {
  109. $(this).data('optionLink').remove().appendTo(that.element);
  110. }
  111. });
  112. },
  113. receive: function(event, ui) {
  114. ui.item.data('optionLink').prop('selected', true);
  115. // increment count
  116. that.count += 1;
  117. that._updateCount();
  118. // workaround, because there's no way to reference
  119. // the new element, see http://dev.jqueryui.com/ticket/4303
  120. that.selectedList.children('.ui-draggable').each(function() {
  121. $(this).removeClass('ui-draggable');
  122. $(this).data('optionLink', ui.item.data('optionLink'));
  123. $(this).data('idx', ui.item.data('idx'));
  124. that._applyItemState($(this), true);
  125. });
  126. // workaround according to http://dev.jqueryui.com/ticket/4088
  127. setTimeout(function() { ui.item.remove(); }, 1);
  128. }
  129. });
  130. }
  131. // set up livesearch
  132. if (this.options.searchable) {
  133. this._registerSearchEvents(this.availableContainer.find('input.search'));
  134. } else {
  135. $('.search').hide();
  136. }
  137. // batch actions
  138. this.container.find(".remove-all").click(function() {
  139. that._populateLists(that.element.find('option').prop('selected', false));
  140. return false;
  141. });
  142. this.container.find(".add-all").click(function() {
  143. var options = that.element.find('option').not(":selected");
  144. if (that.availableList.children('li:hidden').length > 1) {
  145. that.availableList.children('li').each(function(i) {
  146. if ($(this).is(":visible")) {
  147. $(options[i-1]).prop('selected', true);
  148. }
  149. });
  150. } else {
  151. options.prop('selected', true);
  152. }
  153. that._populateLists(that.element.find('option'));
  154. return false;
  155. });
  156. },
  157. destroy: function() {
  158. this.element.show();
  159. this.container.remove();
  160. $.Widget.prototype.destroy.apply(this, arguments);
  161. },
  162. _populateLists: function(options) {
  163. this.selectedList.children('.ui-element').remove();
  164. this.availableList.children('.ui-element').remove();
  165. this.count = 0;
  166. var that = this;
  167. var items = $(options.map(function(i) {
  168. var isSelected = $(this).is(":selected"), item = that._getOptionNode(this).appendTo(isSelected ? that.selectedList : that.availableList).show();
  169. if (isSelected) {
  170. that.count += 1;
  171. }
  172. that._applyItemState(item, isSelected);
  173. item.data('idx', i);
  174. return item[0];
  175. }));
  176. // update count
  177. this._updateCount();
  178. that._filter.apply(this.availableContainer.find('input.search'), [that.availableList]);
  179. },
  180. _updateCount: function() {
  181. this.element.trigger('change');
  182. this.selectedContainer.find('span.count').text(this.count+" "+$.ui.multiselect.locale.itemsCount);
  183. },
  184. _getOptionNode: function(option) {
  185. option = $(option);
  186. var node = $('<li class="ui-state-default ui-element" title="'+(option.attr("title") || option.text())+'"><span class="ui-icon"/>'+option.text()+'<a href="#" class="action"><span class="ui-corner-all ui-icon"/></a></li>').hide();
  187. node.data('optionLink', option);
  188. return node;
  189. },
  190. // clones an item with associated data
  191. // didn't find a smarter away around this
  192. _cloneWithData: function(clonee) {
  193. var clone = clonee.clone(false,false);
  194. clone.data('optionLink', clonee.data('optionLink'));
  195. clone.data('idx', clonee.data('idx'));
  196. return clone;
  197. },
  198. _setSelected: function(item, selected) {
  199. item.data('optionLink').prop('selected', selected);
  200. if (selected) {
  201. var selectedItem = this._cloneWithData(item);
  202. item[this.options.hide](this.options.animated, function() { $(this).remove(); });
  203. selectedItem.appendTo(this.selectedList).hide()[this.options.show](this.options.animated);
  204. this._applyItemState(selectedItem, true);
  205. return selectedItem;
  206. } else {
  207. // look for successor based on initial option index
  208. var items = this.availableList.find('li'), comparator = this.options.nodeComparator;
  209. var succ = null, i = item.data('idx'), direction = comparator(item, $(items[i]));
  210. // TODO: test needed for dynamic list populating
  211. if ( direction ) {
  212. while (i>=0 && i<items.length) {
  213. direction > 0 ? i++ : i--;
  214. if ( direction != comparator(item, $(items[i])) ) {
  215. // going up, go back one item down, otherwise leave as is
  216. succ = items[direction > 0 ? i : i+1];
  217. break;
  218. }
  219. }
  220. } else {
  221. succ = items[i];
  222. }
  223. var availableItem = this._cloneWithData(item);
  224. succ ? availableItem.insertBefore($(succ)) : availableItem.appendTo(this.availableList);
  225. item[this.options.hide](this.options.animated, function() { $(this).remove(); });
  226. availableItem.hide()[this.options.show](this.options.animated);
  227. this._applyItemState(availableItem, false);
  228. return availableItem;
  229. }
  230. },
  231. _applyItemState: function(item, selected) {
  232. if (selected) {
  233. if (this.options.sortable) {
  234. item.children('span').addClass('ui-icon-arrowthick-2-n-s').removeClass('ui-helper-hidden').addClass('ui-icon');
  235. } else {
  236. item.children('span').removeClass('ui-icon-arrowthick-2-n-s').addClass('ui-helper-hidden').removeClass('ui-icon');
  237. }
  238. item.find('a.action span').addClass('ui-icon-minus').removeClass('ui-icon-plus');
  239. this._registerRemoveEvents(item.find('a.action'));
  240. } else {
  241. item.children('span').removeClass('ui-icon-arrowthick-2-n-s').addClass('ui-helper-hidden').removeClass('ui-icon');
  242. item.find('a.action span').addClass('ui-icon-plus').removeClass('ui-icon-minus');
  243. this._registerAddEvents(item.find('a.action'));
  244. }
  245. this._registerDoubleClickEvents(item);
  246. this._registerHoverEvents(item);
  247. },
  248. // taken from John Resig's liveUpdate script
  249. _filter: function(list) {
  250. var input = $(this);
  251. var rows = list.children('li'),
  252. cache = rows.map(function(){
  253. return $(this).text().toLowerCase();
  254. });
  255. var term = $.trim(input.val().toLowerCase()), scores = [];
  256. if (!term) {
  257. rows.show();
  258. } else {
  259. rows.hide();
  260. cache.each(function(i) {
  261. if (this.indexOf(term)>-1) {
  262. scores.push(i);
  263. }
  264. });
  265. $.each(scores, function() {
  266. $(rows[this]).show();
  267. });
  268. }
  269. },
  270. _registerDoubleClickEvents: function(elements) {
  271. if (!this.options.doubleClickable) {
  272. return;
  273. }
  274. elements.dblclick(function(ev) {
  275. if ($(ev.target).closest('.action').length === 0) {
  276. // This may be triggered with rapid clicks on actions as well. In that
  277. // case don't trigger an additional click.
  278. elements.find('a.action').click();
  279. }
  280. });
  281. },
  282. _registerHoverEvents: function(elements) {
  283. elements.removeClass('ui-state-hover');
  284. elements.mouseover(function() {
  285. $(this).addClass('ui-state-hover');
  286. });
  287. elements.mouseout(function() {
  288. $(this).removeClass('ui-state-hover');
  289. });
  290. },
  291. _registerAddEvents: function(elements) {
  292. var that = this;
  293. elements.click(function() {
  294. var item = that._setSelected($(this).parent(), true);
  295. that.count += 1;
  296. that._updateCount();
  297. return false;
  298. });
  299. // make draggable
  300. if (this.options.sortable) {
  301. elements.each(function() {
  302. $(this).parent().draggable({
  303. connectToSortable: that.selectedList,
  304. helper: function() {
  305. var selectedItem = that._cloneWithData($(this)).width($(this).width() - 50);
  306. selectedItem.width($(this).width());
  307. return selectedItem;
  308. },
  309. appendTo: that.container,
  310. containment: that.container,
  311. revert: 'invalid'
  312. });
  313. });
  314. }
  315. },
  316. _registerRemoveEvents: function(elements) {
  317. var that = this;
  318. elements.click(function() {
  319. that._setSelected($(this).parent(), false);
  320. that.count -= 1;
  321. that._updateCount();
  322. return false;
  323. });
  324. },
  325. _registerSearchEvents: function(input) {
  326. var that = this;
  327. input.focus(function() {
  328. $(this).addClass('ui-state-active');
  329. })
  330. .blur(function() {
  331. $(this).removeClass('ui-state-active');
  332. })
  333. .keypress(function(e) {
  334. if (e.keyCode == 13) {
  335. return false;
  336. }
  337. })
  338. .keyup(function() {
  339. that._filter.apply(this, [that.availableList]);
  340. });
  341. }
  342. });
  343. $.extend($.ui.multiselect, {
  344. locale: {
  345. addAll:'Add all',
  346. removeAll:'Remove all',
  347. itemsCount:'items selected'
  348. }
  349. });
  350. }));