jquery.sumoselect.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. /*!
  2. * jquery.sumoselect - v2.1.0
  3. * http://hemantnegi.github.io/jquery.sumoselect
  4. * 2014-04-08
  5. *
  6. * Copyright 2015 Hemant Negi
  7. * Email : hemant.frnz@gmail.com
  8. * Compressor http://refresh-sf.com/
  9. */
  10. (function ($) {
  11. 'namespace sumo';
  12. $.fn.SumoSelect = function (options) {
  13. // var is_visible_default = false;
  14. //$(document).click(function () { is_visible_default = false; });
  15. // This is the easiest way to have default options.
  16. var settings = $.extend({
  17. placeholder: 'Select Here', // Dont change it here.
  18. csvDispCount: 3, // display no. of items in multiselect. 0 to display all.
  19. captionFormat:'{0} Selected', // format of caption text. you can set your locale.
  20. floatWidth: 400, // Screen width of device at which the list is rendered in floating popup fashion.
  21. forceCustomRendering: false, // force the custom modal on all devices below floatWidth resolution.
  22. nativeOnDevice: ['Android', 'BlackBerry', 'iPhone', 'iPad', 'iPod', 'Opera Mini', 'IEMobile', 'Silk'], //
  23. outputAsCSV: false, // true to POST data as csv ( false for Html control array ie. deafault select )
  24. csvSepChar: ',', // seperation char in csv mode
  25. okCancelInMulti: false, //display ok cancel buttons in desktop mode multiselect also.
  26. triggerChangeCombined: true, // im multi select mode wether to trigger change event on individual selection or combined selection.
  27. selectAll: false, // to display select all button in multiselect mode.|| also select all will not be available on mobile devices.
  28. selectAlltext: 'Select All' // the text to display for select all.
  29. }, options);
  30. var ret = this.each(function () {
  31. var selObj = this; // the original select object.
  32. if (this.sumo || !$(this).is('select')) return; //already initialized
  33. this.sumo = {
  34. E: $(selObj), //the jquery object of original select element.
  35. is_multi: $(selObj).attr('multiple'), //if its a mmultiple select
  36. select: '',
  37. caption: '',
  38. placeholder: '',
  39. optDiv: '',
  40. CaptionCont: '',
  41. is_floating: false,
  42. is_opened: false,
  43. //backdrop: '',
  44. mob:false, // if to open device default select
  45. Pstate: [],
  46. createElems: function () {
  47. var O = this;
  48. O.E.wrap('<div class="SumoSelect" tabindex="0">');
  49. O.select = O.E.parent();
  50. O.caption = $('<span></span>');
  51. O.CaptionCont = $('<p class="CaptionCont"><label><i></i></label></p>').addClass('SlectBox').attr('style', O.E.attr('style')).prepend(O.caption);
  52. O.select.append(O.CaptionCont);
  53. if(O.E.attr('disabled'))
  54. O.select.addClass('disabled').removeAttr('tabindex');
  55. //if output as csv and is a multiselect.
  56. if (settings.outputAsCSV && O.is_multi && O.E.attr('name')) {
  57. //create a hidden field to store csv value.
  58. O.select.append($('<input class="HEMANT123" type="hidden" />').attr('name', O.E.attr('name')).val(O.getSelStr()));
  59. // so it can not post the original select.
  60. O.E.removeAttr('name');
  61. }
  62. //break for mobile rendring.. if forceCustomRendering is false
  63. if (O.isMobile() && !settings.forceCustomRendering) {
  64. O.setNativeMobile();
  65. return;
  66. }
  67. //hide original select
  68. O.E.hide();
  69. //## Creating the list...
  70. O.optDiv = $('<div class="optWrapper">');
  71. //branch for floating list in low res devices.
  72. O.floatingList();
  73. //Creating the markup for the available options
  74. ul = $('<ul class="options">');
  75. O.optDiv.append(ul);
  76. // Select all functionality
  77. if (settings.selectAll) O.selAll();
  78. $(O.E.children('option')).each(function (i, opt) { // parsing options to li
  79. opt = $(opt);
  80. O.createLi(opt);
  81. });
  82. //if multiple then add the class multiple and add OK / CANCEL button
  83. if (O.is_multi) O.multiSelelect();
  84. O.select.append(O.optDiv);
  85. O.basicEvents();
  86. O.selAllState();
  87. },
  88. //## Creates a LI element from a given option and binds events to it
  89. //## Adds it to UL at a given index (Last by default)
  90. createLi: function (opt,i) {
  91. var O = this;
  92. if(!opt.attr('value'))opt.attr('value',opt.val());
  93. li = $('<li data-val="' + opt.val() + '"><label>' + opt.text() + '</label></li>');
  94. if (O.is_multi) li.prepend('<span><i></i></span>');
  95. if (opt[0].disabled)
  96. li = li.addClass('disabled');
  97. O.onOptClick(li);
  98. if (opt[0].selected)
  99. li.addClass('selected');
  100. if (opt.attr('class'))
  101. li.addClass(opt.attr('class'));
  102. ul = O.optDiv.children('ul.options');
  103. if (typeof i == "undefined")
  104. ul.append(li);
  105. else
  106. ul.children('li').eq(i).before(li);
  107. return li;
  108. },
  109. //## Returns the selected items as string in a Multiselect.
  110. getSelStr: function () {
  111. // get the pre selected items.
  112. sopt = [];
  113. this.E.children('option:selected').each(function () { sopt.push($(this).val()); });
  114. return sopt.join(settings.csvSepChar);
  115. },
  116. //## THOSE OK/CANCEL BUTTONS ON MULTIPLE SELECT.
  117. multiSelelect: function () {
  118. var O = this;
  119. O.optDiv.addClass('multiple');
  120. O.okbtn = $('<p class="btnOk">OK</p>').click(function () {
  121. //if combined change event is set.
  122. if (settings.triggerChangeCombined) {
  123. //check for a change in the selection.
  124. changed = false;
  125. if (O.E.children('option:selected').length != O.Pstate.length) {
  126. changed = true;
  127. }
  128. else {
  129. O.E.children('option:selected').each(function () {
  130. if (O.Pstate.indexOf($(this).val()) < 0) changed = true;
  131. });
  132. }
  133. if (changed) {
  134. O.E.trigger('change').trigger('click');
  135. O.setText();
  136. }
  137. }
  138. O.hideOpts();
  139. });
  140. O.cancelBtn = $('<p class="btnCancel">Cancel</p>').click(function () {
  141. O._cnbtn();
  142. O.hideOpts();
  143. });
  144. O.optDiv.append($('<div class="MultiControls">').append(O.okbtn).append(O.cancelBtn));
  145. },
  146. _cnbtn:function(){
  147. var O = this;
  148. //remove all selections
  149. O.E.children('option:selected').each(function () { this.selected = false; });
  150. O.optDiv.find('li.selected').removeClass('selected')
  151. //restore selections from saved state.
  152. for (i = 0; i < O.Pstate.length; i++) {
  153. O.E.children('option[value="' + O.Pstate[i] + '"]')[0].selected = true;
  154. O.optDiv.find('li[data-val="' + O.Pstate[i] + '"]').addClass('selected');
  155. }
  156. O.selAllState();
  157. },
  158. selAll:function(){
  159. var O = this;
  160. if(!O.is_multi)return;
  161. O.chkAll = $('<i>');
  162. O.selAll = $('<p class="select-all"><label>' + settings.selectAlltext + '</label></p>').prepend($('<span></span>').append(O.chkAll));
  163. O.chkAll.on('click',function(){
  164. //O.toggSelAll(!);
  165. O.selAll.toggleClass('selected');
  166. O.optDiv.find('ul.options li').each(function(ix,e){
  167. e = $(e);
  168. if(O.selAll.hasClass('selected')){
  169. if(!e.hasClass('selected'))e.trigger('click');
  170. }
  171. else
  172. if(e.hasClass('selected'))e.trigger('click');
  173. });
  174. });
  175. O.optDiv.prepend(O.selAll);
  176. },
  177. selAllState: function () {
  178. var O = this;
  179. if (settings.selectAll) {
  180. var sc = 0, vc = 0;
  181. O.optDiv.find('ul.options li').each(function (ix, e) {
  182. if ($(e).hasClass('selected')) sc++;
  183. if (!$(e).hasClass('disabled')) vc++;
  184. });
  185. //select all checkbox state change.
  186. if (sc == vc) O.selAll.removeClass('partial').addClass('selected');
  187. else if (sc == 0) O.selAll.removeClass('selected partial');
  188. else O.selAll.addClass('partial')//.removeClass('selected');
  189. }
  190. },
  191. showOpts: function () {
  192. var O = this;
  193. if (O.E.attr('disabled')) return; // if select is disabled then retrun
  194. O.is_opened = true;
  195. //O.backdrop.show();
  196. O.optDiv.addClass('open');
  197. // hide options on click outside.
  198. $(document).on('click.sumo', function (e) {
  199. if (!O.select.is(e.target) // if the target of the click isn't the container...
  200. && O.select.has(e.target).length === 0){ // ... nor a descendant of the container
  201. // if (O.is_multi && settings.okCancelInMulti)
  202. // O._cnbtn();
  203. // O.hideOpts();
  204. if(!O.is_opened)return;
  205. O.hideOpts();
  206. if (O.is_multi && settings.okCancelInMulti)O._cnbtn();
  207. }
  208. });
  209. if (O.is_floating) {
  210. H = O.optDiv.children('ul').outerHeight() + 2; // +2 is clear fix
  211. if (O.is_multi) H = H + parseInt(O.optDiv.css('padding-bottom'));
  212. O.optDiv.css('height', H);
  213. }
  214. //maintain state when ok/cancel buttons are available.
  215. if (O.is_multi && (O.is_floating || settings.okCancelInMulti)) {
  216. O.Pstate = [];
  217. O.E.children('option:selected').each(function () { O.Pstate.push($(this).val()); });
  218. }
  219. },
  220. hideOpts: function () {
  221. var O = this;
  222. O.is_opened = false;
  223. O.optDiv.removeClass('open').find('ul li.sel').removeClass('sel');
  224. $(document).off('click.sumo');
  225. },
  226. setOnOpen: function () {
  227. var O = this;
  228. var li = O.optDiv.find('ul li').eq(O.E[0].selectedIndex);
  229. li.addClass('sel');
  230. O.showOpts();
  231. },
  232. nav: function (up) {
  233. var O = this, c;
  234. var sel = O.optDiv.find('ul li.sel');
  235. if (O.is_opened && sel.length) {
  236. if (up)
  237. c = sel.prevAll('li:not(.disabled)');
  238. else
  239. c = sel.nextAll('li:not(.disabled)');
  240. if (!c.length)return;
  241. sel.removeClass('sel');
  242. sel = c.first().addClass('sel');
  243. // setting sel item to visible view.
  244. var ul = O.optDiv.find('ul'),
  245. st = ul.scrollTop(),
  246. t = sel.position().top + st;
  247. if(t >= st + ul.height()-sel.outerHeight())
  248. ul.scrollTop(t - ul.height() + sel.outerHeight());
  249. if(t<st)
  250. ul.scrollTop(t);
  251. }
  252. else
  253. O.setOnOpen();
  254. },
  255. basicEvents: function () {
  256. var O = this;
  257. O.CaptionCont.click(function (evt) {
  258. O.E.trigger('click');
  259. if (O.is_opened) O.hideOpts(); else O.showOpts();
  260. evt.stopPropagation();
  261. });
  262. /* O.select.on('blur focusout', function () {
  263. if(!O.is_opened)return;
  264. //O.hideOpts();
  265. O.hideOpts();
  266. if (O.is_multi && settings.okCancelInMulti)
  267. O._cnbtn();
  268. })*/
  269. O.select.on('keydown', function (e) {
  270. switch (e.which) {
  271. case 38: // up
  272. O.nav(true);
  273. break;
  274. case 40: // down
  275. O.nav(false);
  276. break;
  277. case 32: // space
  278. case 13: // enter
  279. if (O.is_opened)
  280. O.optDiv.find('ul li.sel').trigger('click');
  281. else
  282. O.setOnOpen();
  283. break;
  284. case 9: //tab
  285. case 27: // esc
  286. if (O.is_multi && settings.okCancelInMulti)O._cnbtn();
  287. O.hideOpts();
  288. return;
  289. default:
  290. return; // exit this handler for other keys
  291. }
  292. e.preventDefault(); // prevent the default action (scroll / move caret)
  293. });
  294. $(window).on('resize.sumo', function () {
  295. O.floatingList();
  296. });
  297. },
  298. onOptClick: function (li) {
  299. var O = this;
  300. li.click(function () {
  301. var li = $(this);
  302. if(li.hasClass('disabled'))return;
  303. txt = "";
  304. if (O.is_multi) {
  305. li.toggleClass('selected');
  306. O.E.children('option[value="' + li.data('val') + '"]')[0].selected = li.hasClass('selected');
  307. O.selAllState();
  308. }
  309. else {
  310. li.parent().find('li.selected').removeClass('selected'); //if not multiselect then remove all selections from this list
  311. li.toggleClass('selected');
  312. O.E.val(li.attr('data-val')); //set the value of select element
  313. }
  314. //branch for combined change event.
  315. if (!(O.is_multi && settings.triggerChangeCombined && (O.is_floating || settings.okCancelInMulti))) {
  316. O.setText();
  317. O.E.trigger('change').trigger('click');
  318. }
  319. if (!O.is_multi) O.hideOpts(); //if its not a multiselect then hide on single select.
  320. });
  321. },
  322. setText: function () {
  323. var O = this;
  324. O.placeholder = "";
  325. if (O.is_multi) {
  326. sels = O.E.children(':selected').not(':disabled'); //selected options.
  327. for (i = 0; i < sels.length; i++) {
  328. if (i + 1 >= settings.csvDispCount && settings.csvDispCount) {
  329. O.placeholder = settings.captionFormat.replace('{0}', sels.length);
  330. //O.placeholder = i + '+ Selected';
  331. break;
  332. }
  333. else O.placeholder += $(sels[i]).text() + ", ";
  334. }
  335. O.placeholder = O.placeholder.replace(/,([^,]*)$/, '$1'); //remove unexpected "," from last.
  336. }
  337. else {
  338. O.placeholder = O.E.children(':selected').not(':disabled').text();
  339. }
  340. is_placeholder = false;
  341. if (!O.placeholder) {
  342. is_placeholder = true;
  343. O.placeholder = O.E.attr('placeholder');
  344. if (!O.placeholder) //if placeholder is there then set it
  345. {
  346. O.placeholder = O.E.children('option:disabled:selected').text();
  347. //if (!O.placeholder && settings.placeholder === 'Select Here')
  348. // O.placeholder = O.E.val();
  349. }
  350. }
  351. O.placeholder = O.placeholder ? O.placeholder : settings.placeholder
  352. //set display text
  353. O.caption.text(O.placeholder);
  354. //set the hidden field if post as csv is true.
  355. csvField = O.select.find('input.HEMANT123');
  356. if (csvField.length) csvField.val(O.getSelStr());
  357. //add class placeholder if its a placeholder text.
  358. if (is_placeholder) O.caption.addClass('placeholder'); else O.caption.removeClass('placeholder');
  359. return O.placeholder;
  360. },
  361. isMobile: function () {
  362. // Adapted from http://www.detectmobilebrowsers.com
  363. var ua = navigator.userAgent || navigator.vendor || window.opera;
  364. // Checks for iOs, Android, Blackberry, Opera Mini, and Windows mobile devices
  365. for (var i = 0; i < settings.nativeOnDevice.length; i++) if (ua.toString().toLowerCase().indexOf(settings.nativeOnDevice[i].toLowerCase()) > 0) return settings.nativeOnDevice[i];
  366. return false;
  367. },
  368. setNativeMobile: function () {
  369. var O = this;
  370. O.E.addClass('SelectClass')//.css('height', O.select.outerHeight());
  371. O.mob = true;
  372. O.E.change(function () {
  373. O.setText();
  374. });
  375. },
  376. floatingList: function () {
  377. var O = this;
  378. //called on init and also on resize.
  379. //O.is_floating = true if window width is < specified float width
  380. O.is_floating = $(window).width() <= settings.floatWidth;
  381. //set class isFloating
  382. O.optDiv.toggleClass('isFloating', O.is_floating);
  383. //remove height if not floating
  384. if (!O.is_floating) O.optDiv.css('height', '');
  385. //toggle class according to okCancelInMulti flag only when it is not floating
  386. O.optDiv.toggleClass('okCancelInMulti', settings.okCancelInMulti && !O.is_floating);
  387. },
  388. //HELPERS FOR OUTSIDERS
  389. // validates range of given item operations
  390. vRange: function (i) {
  391. var O = this;
  392. opts = O.E.children('option');
  393. if (opts.length <= i || i < 0) throw "index out of bounds"
  394. return O;
  395. },
  396. //toggles selection on c as boolean.
  397. toggSel: function (c, i) {
  398. var O = this.vRange(i);
  399. if (O.E.children('option')[i].disabled) return;
  400. O.E.children('option')[i].selected = c;
  401. if(!O.mob)O.optDiv.find('ul.options li').eq(i).toggleClass('selected',c);
  402. O.setText();
  403. },
  404. //toggles disabled on c as boolean.
  405. toggDis: function (c, i) {
  406. var O = this.vRange(i);
  407. O.E.children('option')[i].disabled = c;
  408. if(c)O.E.children('option')[i].selected = false;
  409. if(!O.mob)O.optDiv.find('ul.options li').eq(i).toggleClass('disabled', c).removeClass('selected');
  410. O.setText();
  411. },
  412. // toggle disable/enable on complete select control
  413. toggSumo: function(val) {
  414. var O = this;
  415. O.enabled = val;
  416. O.select.toggleClass('disabled', val);
  417. if (val) {
  418. O.E.attr('disabled', 'disabled');
  419. O.select.removeAttr('tabindex');
  420. }
  421. else{
  422. O.E.removeAttr('disabled');
  423. O.select.attr('tabindex','0');
  424. }
  425. return O;
  426. },
  427. //toggles alloption on c as boolean.
  428. toggSelAll: function (c) {
  429. var O = this;
  430. O.E.find('option').each(function (ix, el) {
  431. if (O.E.find('option')[$(this).index()].disabled) return;
  432. O.E.find('option')[$(this).index()].selected = c;
  433. if (!O.mob)
  434. O.optDiv.find('ul.options li').eq($(this).index()).toggleClass('selected', c);
  435. O.setText();
  436. });
  437. if(!O.mob && settings.selectAll)O.selAll.removeClass('partial').toggleClass('selected',c);
  438. },
  439. /* outside accessibility options
  440. which can be accessed from the element instance.
  441. */
  442. reload:function(){
  443. var elm = this.unload();
  444. return $(elm).SumoSelect(settings);
  445. },
  446. unload: function () {
  447. var O = this;
  448. O.select.before(O.E);
  449. O.E.show();
  450. if (settings.outputAsCSV && O.is_multi && O.select.find('input.HEMANT123').length) {
  451. O.E.attr('name', O.select.find('input.HEMANT123').attr('name')); // restore the name;
  452. }
  453. O.select.remove();
  454. delete selObj.sumo;
  455. return selObj;
  456. },
  457. //## add a new option to select at a given index.
  458. add: function (val, txt, i) {
  459. if (typeof val == "undefined") throw "No value to add"
  460. var O = this;
  461. opts=O.E.children('option')
  462. if (typeof txt == "number") { i = txt; txt = val; }
  463. if (typeof txt == "undefined") { txt = val; }
  464. opt = $("<option></option>").val(val).html(txt);
  465. if (opts.length < i) throw "index out of bounds"
  466. if (typeof i == "undefined" || opts.length == i) { // add it to the last if given index is last no or no index provides.
  467. O.E.append(opt);
  468. if(!O.mob)O.createLi(opt);
  469. }
  470. else {
  471. opts.eq(i).before(opt);
  472. if(!O.mob)O.createLi(opt, i);
  473. }
  474. return selObj;
  475. },
  476. //## removes an item at a given index.
  477. remove: function (i) {
  478. var O = this.vRange(i);
  479. O.E.children('option').eq(i).remove();
  480. if(!O.mob)O.optDiv.find('ul.options li').eq(i).remove();
  481. O.setText();
  482. },
  483. //## Select an item at a given index.
  484. selectItem: function (i) { this.toggSel(true, i); },
  485. //## UnSelect an iten at a given index.
  486. unSelectItem: function (i) { this.toggSel(false, i); },
  487. //## Select all items of the select.
  488. selectAll: function () { this.toggSelAll(true); },
  489. //## UnSelect all items of the select.
  490. unSelectAll: function () { this.toggSelAll(false); },
  491. //## Disable an iten at a given index.
  492. disableItem: function (i) { this.toggDis(true, i) },
  493. //## Removes disabled an iten at a given index.
  494. enableItem: function (i) { this.toggDis(false, i) },
  495. //## New simple methods as getter and setter are not working fine in ie8-
  496. //## variable to check state of control if enabled or disabled.
  497. enabled : true,
  498. //## Enables the control
  499. enable: function(){return this.toggSumo(false)},
  500. //## Disables the control
  501. disable: function(){return this.toggSumo(true)},
  502. init: function () {
  503. var O = this;
  504. O.createElems();
  505. O.setText();
  506. return O
  507. }
  508. };
  509. selObj.sumo.init();
  510. });
  511. return ret.length == 1 ? ret[0] : ret;
  512. };
  513. }(jQuery));