bootstrap-select.js 97 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710
  1. (function ($) {
  2. 'use strict';
  3. var testElement = document.createElement('_');
  4. testElement.classList.toggle('c3', false);
  5. // Polyfill for IE 10 and Firefox <24, where classList.toggle does not
  6. // support the second argument.
  7. if (testElement.classList.contains('c3')) {
  8. var _toggle = DOMTokenList.prototype.toggle;
  9. DOMTokenList.prototype.toggle = function(token, force) {
  10. if (1 in arguments && !this.contains(token) === !force) {
  11. return force;
  12. } else {
  13. return _toggle.call(this, token);
  14. }
  15. };
  16. }
  17. // shallow array comparison
  18. function isEqual (array1, array2) {
  19. return array1.length === array2.length && array1.every(function(element, index) {
  20. return element === array2[index];
  21. });
  22. };
  23. //<editor-fold desc="Shims">
  24. if (!String.prototype.startsWith) {
  25. (function () {
  26. 'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
  27. var defineProperty = (function () {
  28. // IE 8 only supports `Object.defineProperty` on DOM elements
  29. try {
  30. var object = {};
  31. var $defineProperty = Object.defineProperty;
  32. var result = $defineProperty(object, object, object) && $defineProperty;
  33. } catch (error) {
  34. }
  35. return result;
  36. }());
  37. var toString = {}.toString;
  38. var startsWith = function (search) {
  39. if (this == null) {
  40. throw new TypeError();
  41. }
  42. var string = String(this);
  43. if (search && toString.call(search) == '[object RegExp]') {
  44. throw new TypeError();
  45. }
  46. var stringLength = string.length;
  47. var searchString = String(search);
  48. var searchLength = searchString.length;
  49. var position = arguments.length > 1 ? arguments[1] : undefined;
  50. // `ToInteger`
  51. var pos = position ? Number(position) : 0;
  52. if (pos != pos) { // better `isNaN`
  53. pos = 0;
  54. }
  55. var start = Math.min(Math.max(pos, 0), stringLength);
  56. // Avoid the `indexOf` call if no match is possible
  57. if (searchLength + start > stringLength) {
  58. return false;
  59. }
  60. var index = -1;
  61. while (++index < searchLength) {
  62. if (string.charCodeAt(start + index) != searchString.charCodeAt(index)) {
  63. return false;
  64. }
  65. }
  66. return true;
  67. };
  68. if (defineProperty) {
  69. defineProperty(String.prototype, 'startsWith', {
  70. 'value': startsWith,
  71. 'configurable': true,
  72. 'writable': true
  73. });
  74. } else {
  75. String.prototype.startsWith = startsWith;
  76. }
  77. }());
  78. }
  79. if (!Object.keys) {
  80. Object.keys = function (
  81. o, // object
  82. k, // key
  83. r // result array
  84. ){
  85. // initialize object and result
  86. r=[];
  87. // iterate over object keys
  88. for (k in o)
  89. // fill result array with non-prototypical keys
  90. r.hasOwnProperty.call(o, k) && r.push(k);
  91. // return result
  92. return r;
  93. };
  94. }
  95. // much faster than $.val()
  96. function getSelectValues(select) {
  97. var result = [];
  98. var options = select && select.options;
  99. var opt;
  100. if (select.multiple) {
  101. for (var i = 0, len = options.length; i < len; i++) {
  102. opt = options[i];
  103. if (opt.selected) {
  104. result.push(opt.value || opt.text);
  105. }
  106. }
  107. } else {
  108. result = select.value;
  109. }
  110. return result;
  111. }
  112. // set data-selected on select element if the value has been programmatically selected
  113. // prior to initialization of bootstrap-select
  114. // * consider removing or replacing an alternative method *
  115. var valHooks = {
  116. useDefault: false,
  117. _set: $.valHooks.select.set
  118. };
  119. $.valHooks.select.set = function (elem, value) {
  120. if (value && !valHooks.useDefault) $(elem).data('selected', true);
  121. return valHooks._set.apply(this, arguments);
  122. };
  123. var changed_arguments = null;
  124. var EventIsSupported = (function () {
  125. try {
  126. new Event('change');
  127. return true;
  128. } catch (e) {
  129. return false;
  130. }
  131. })();
  132. $.fn.triggerNative = function (eventName) {
  133. var el = this[0],
  134. event;
  135. if (el.dispatchEvent) { // for modern browsers & IE9+
  136. if (EventIsSupported) {
  137. // For modern browsers
  138. event = new Event(eventName, {
  139. bubbles: true
  140. });
  141. } else {
  142. // For IE since it doesn't support Event constructor
  143. event = document.createEvent('Event');
  144. event.initEvent(eventName, true, false);
  145. }
  146. el.dispatchEvent(event);
  147. } else if (el.fireEvent) { // for IE8
  148. event = document.createEventObject();
  149. event.eventType = eventName;
  150. el.fireEvent('on' + eventName, event);
  151. } else {
  152. // fall back to jQuery.trigger
  153. this.trigger(eventName);
  154. }
  155. };
  156. //</editor-fold>
  157. function stringSearch(li, searchString, method, normalize) {
  158. var stringTypes = [
  159. 'content',
  160. 'subtext',
  161. 'tokens'
  162. ],
  163. searchSuccess = false;
  164. for (var i = 0; i < stringTypes.length; i++) {
  165. var stringType = stringTypes[i],
  166. string = li[stringType];
  167. if (string) {
  168. string = string.toString();
  169. // Strip HTML tags. This isn't perfect, but it's much faster than any other method
  170. if (stringType === 'content') {
  171. string = string.replace(/<[^>]+>/g, '');
  172. }
  173. if (normalize) string = normalizeToBase(string);
  174. string = string.toUpperCase();
  175. if (method === 'contains') {
  176. searchSuccess = string.indexOf(searchString) >= 0;
  177. } else {
  178. searchSuccess = string.startsWith(searchString);
  179. }
  180. if (searchSuccess) break;
  181. }
  182. }
  183. return searchSuccess;
  184. }
  185. function toInteger(value) {
  186. return parseInt(value, 10) || 0;
  187. }
  188. /**
  189. * Remove all diatrics from the given text.
  190. * @access private
  191. * @param {String} text
  192. * @returns {String}
  193. */
  194. function normalizeToBase(text) {
  195. var rExps = [
  196. {re: /[\xC0-\xC6]/g, ch: "A"},
  197. {re: /[\xE0-\xE6]/g, ch: "a"},
  198. {re: /[\xC8-\xCB]/g, ch: "E"},
  199. {re: /[\xE8-\xEB]/g, ch: "e"},
  200. {re: /[\xCC-\xCF]/g, ch: "I"},
  201. {re: /[\xEC-\xEF]/g, ch: "i"},
  202. {re: /[\xD2-\xD6]/g, ch: "O"},
  203. {re: /[\xF2-\xF6]/g, ch: "o"},
  204. {re: /[\xD9-\xDC]/g, ch: "U"},
  205. {re: /[\xF9-\xFC]/g, ch: "u"},
  206. {re: /[\xC7-\xE7]/g, ch: "c"},
  207. {re: /[\xD1]/g, ch: "N"},
  208. {re: /[\xF1]/g, ch: "n"}
  209. ];
  210. $.each(rExps, function () {
  211. text = text ? text.replace(this.re, this.ch) : '';
  212. });
  213. return text;
  214. }
  215. // List of HTML entities for escaping.
  216. var escapeMap = {
  217. '&': '&amp;',
  218. '<': '&lt;',
  219. '>': '&gt;',
  220. '"': '&quot;',
  221. "'": '&#x27;',
  222. '`': '&#x60;'
  223. };
  224. var unescapeMap = {
  225. '&amp;': '&',
  226. '&lt;': '<',
  227. '&gt;': '>',
  228. '&quot;': '"',
  229. '&#x27;': "'",
  230. '&#x60;': '`'
  231. };
  232. // Functions for escaping and unescaping strings to/from HTML interpolation.
  233. var createEscaper = function (map) {
  234. var escaper = function (match) {
  235. return map[match];
  236. };
  237. // Regexes for identifying a key that needs to be escaped.
  238. var source = '(?:' + Object.keys(map).join('|') + ')';
  239. var testRegexp = RegExp(source);
  240. var replaceRegexp = RegExp(source, 'g');
  241. return function (string) {
  242. string = string == null ? '' : '' + string;
  243. return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
  244. };
  245. };
  246. var htmlEscape = createEscaper(escapeMap);
  247. var htmlUnescape = createEscaper(unescapeMap);
  248. /**
  249. * ------------------------------------------------------------------------
  250. * Constants
  251. * ------------------------------------------------------------------------
  252. */
  253. var keyCodeMap = {
  254. 32: ' ',
  255. 48: '0',
  256. 49: '1',
  257. 50: '2',
  258. 51: '3',
  259. 52: '4',
  260. 53: '5',
  261. 54: '6',
  262. 55: '7',
  263. 56: '8',
  264. 57: '9',
  265. 59: ';',
  266. 65: 'A',
  267. 66: 'B',
  268. 67: 'C',
  269. 68: 'D',
  270. 69: 'E',
  271. 70: 'F',
  272. 71: 'G',
  273. 72: 'H',
  274. 73: 'I',
  275. 74: 'J',
  276. 75: 'K',
  277. 76: 'L',
  278. 77: 'M',
  279. 78: 'N',
  280. 79: 'O',
  281. 80: 'P',
  282. 81: 'Q',
  283. 82: 'R',
  284. 83: 'S',
  285. 84: 'T',
  286. 85: 'U',
  287. 86: 'V',
  288. 87: 'W',
  289. 88: 'X',
  290. 89: 'Y',
  291. 90: 'Z',
  292. 96: '0',
  293. 97: '1',
  294. 98: '2',
  295. 99: '3',
  296. 100: '4',
  297. 101: '5',
  298. 102: '6',
  299. 103: '7',
  300. 104: '8',
  301. 105: '9'
  302. };
  303. var keyCodes = {
  304. ESCAPE: 27, // KeyboardEvent.which value for Escape (Esc) key
  305. ENTER: 13, // KeyboardEvent.which value for Enter key
  306. SPACE: 32, // KeyboardEvent.which value for space key
  307. TAB: 9, // KeyboardEvent.which value for tab key
  308. ARROW_UP: 38, // KeyboardEvent.which value for up arrow key
  309. ARROW_DOWN: 40 // KeyboardEvent.which value for down arrow key
  310. }
  311. var version = {
  312. success: false,
  313. major: '3'
  314. };
  315. try {
  316. version.full = ($.fn.dropdown.Constructor.VERSION || '').split(' ')[0].split('.');
  317. version.major = version.full[0];
  318. version.success = true;
  319. }
  320. catch(err) {
  321. console.warn(
  322. 'There was an issue retrieving Bootstrap\'s version. ' +
  323. 'Ensure Bootstrap is being loaded before bootstrap-select and there is no namespace collision. ' +
  324. 'If loading Bootstrap asynchronously, the version may need to be manually specified via $.fn.selectpicker.Constructor.BootstrapVersion.'
  325. , err);
  326. }
  327. var classNames = {
  328. DISABLED: 'disabled',
  329. DIVIDER: 'divider',
  330. SHOW: 'open',
  331. DROPUP: 'dropup',
  332. MENU: 'dropdown-menu',
  333. MENURIGHT: 'dropdown-menu-right',
  334. MENULEFT: 'dropdown-menu-left',
  335. // to-do: replace with more advanced template/customization options
  336. BUTTONCLASS: 'btn-default',
  337. POPOVERHEADER: 'popover-title'
  338. }
  339. var Selector = {
  340. MENU: '.' + classNames.MENU
  341. }
  342. if (version.major === '4') {
  343. classNames.DIVIDER = 'dropdown-divider';
  344. classNames.SHOW = 'show';
  345. classNames.BUTTONCLASS = 'btn-light';
  346. classNames.POPOVERHEADER = 'popover-header';
  347. }
  348. var REGEXP_ARROW = new RegExp(keyCodes.ARROW_UP + '|' + keyCodes.ARROW_DOWN);
  349. var REGEXP_TAB_OR_ESCAPE = new RegExp('^' + keyCodes.TAB + '$|' + keyCodes.ESCAPE);
  350. var REGEXP_ENTER_OR_SPACE = new RegExp(keyCodes.ENTER + '|' + keyCodes.SPACE);
  351. var Selectpicker = function (element, options) {
  352. var that = this;
  353. // bootstrap-select has been initialized - revert valHooks.select.set back to its original function
  354. if (!valHooks.useDefault) {
  355. $.valHooks.select.set = valHooks._set;
  356. valHooks.useDefault = true;
  357. }
  358. this.$element = $(element);
  359. this.$newElement = null;
  360. this.$button = null;
  361. this.$menu = null;
  362. this.options = options;
  363. this.selectpicker = {
  364. main: {
  365. // store originalIndex (key) and newIndex (value) in this.selectpicker.main.map.newIndex for fast accessibility
  366. // allows us to do this.main.elements[this.selectpicker.main.map.newIndex[index]] to select an element based on the originalIndex
  367. map: {
  368. newIndex: {},
  369. originalIndex: {}
  370. }
  371. },
  372. current: {
  373. map: {}
  374. }, // current changes if a search is in progress
  375. search: {
  376. map: {}
  377. },
  378. view: {},
  379. keydown: {
  380. keyHistory: '',
  381. resetKeyHistory: {
  382. start: function () {
  383. return setTimeout(function () {
  384. that.selectpicker.keydown.keyHistory = '';
  385. }, 800);
  386. }
  387. }
  388. }
  389. };
  390. // If we have no title yet, try to pull it from the html title attribute (jQuery doesnt' pick it up as it's not a
  391. // data-attribute)
  392. if (this.options.title === null) {
  393. this.options.title = this.$element.attr('title');
  394. }
  395. // Format window padding
  396. var winPad = this.options.windowPadding;
  397. if (typeof winPad === 'number') {
  398. this.options.windowPadding = [winPad, winPad, winPad, winPad];
  399. }
  400. //Expose public methods
  401. this.val = Selectpicker.prototype.val;
  402. this.render = Selectpicker.prototype.render;
  403. this.refresh = Selectpicker.prototype.refresh;
  404. this.setStyle = Selectpicker.prototype.setStyle;
  405. this.selectAll = Selectpicker.prototype.selectAll;
  406. this.deselectAll = Selectpicker.prototype.deselectAll;
  407. this.destroy = Selectpicker.prototype.destroy;
  408. this.remove = Selectpicker.prototype.remove;
  409. this.show = Selectpicker.prototype.show;
  410. this.hide = Selectpicker.prototype.hide;
  411. this.init();
  412. };
  413. Selectpicker.VERSION = '1.13.2';
  414. Selectpicker.BootstrapVersion = version.major;
  415. // part of this is duplicated in i18n/defaults-en_US.js. Make sure to update both.
  416. Selectpicker.DEFAULTS = {
  417. noneSelectedText: 'Nothing selected',
  418. noneResultsText: 'No results matched {0}',
  419. countSelectedText: function (numSelected, numTotal) {
  420. return (numSelected == 1) ? "{0} item selected" : "{0} items selected";
  421. },
  422. maxOptionsText: function (numAll, numGroup) {
  423. return [
  424. (numAll == 1) ? 'Limit reached ({n} item max)' : 'Limit reached ({n} items max)',
  425. (numGroup == 1) ? 'Group limit reached ({n} item max)' : 'Group limit reached ({n} items max)'
  426. ];
  427. },
  428. selectAllText: 'Select All',
  429. deselectAllText: 'Deselect All',
  430. doneButton: false,
  431. doneButtonText: 'Close',
  432. multipleSeparator: ', ',
  433. styleBase: 'btn',
  434. style: classNames.BUTTONCLASS,
  435. size: 'auto',
  436. title: null,
  437. selectedTextFormat: 'values',
  438. width: false,
  439. container: false,
  440. hideDisabled: false,
  441. showSubtext: false,
  442. showIcon: true,
  443. showContent: true,
  444. dropupAuto: true,
  445. header: false,
  446. liveSearch: false,
  447. liveSearchPlaceholder: null,
  448. liveSearchNormalize: false,
  449. liveSearchStyle: 'contains',
  450. actionsBox: false,
  451. iconBase: 'glyphicon',
  452. tickIcon: 'glyphicon-ok',
  453. showTick: false,
  454. template: {
  455. caret: '<span class="caret"></span>'
  456. },
  457. maxOptions: false,
  458. mobile: false,
  459. selectOnTab: false,
  460. dropdownAlignRight: false,
  461. windowPadding: 0,
  462. virtualScroll: 600,
  463. display: false
  464. };
  465. if (version.major === '4') {
  466. Selectpicker.DEFAULTS.style = 'btn-light';
  467. Selectpicker.DEFAULTS.iconBase = '';
  468. Selectpicker.DEFAULTS.tickIcon = 'bs-ok-default';
  469. }
  470. Selectpicker.prototype = {
  471. constructor: Selectpicker,
  472. init: function () {
  473. var that = this,
  474. id = this.$element.attr('id');
  475. this.$element.addClass('bs-select-hidden');
  476. this.multiple = this.$element.prop('multiple');
  477. this.autofocus = this.$element.prop('autofocus');
  478. this.$newElement = this.createDropdown();
  479. this.createLi();
  480. this.$element
  481. .after(this.$newElement)
  482. .prependTo(this.$newElement);
  483. this.$button = this.$newElement.children('button');
  484. this.$menu = this.$newElement.children(Selector.MENU);
  485. this.$menuInner = this.$menu.children('.inner');
  486. this.$searchbox = this.$menu.find('input');
  487. this.$element.removeClass('bs-select-hidden');
  488. if (this.options.dropdownAlignRight === true) this.$menu.addClass(classNames.MENURIGHT);
  489. if (typeof id !== 'undefined') {
  490. this.$button.attr('data-id', id);
  491. }
  492. this.checkDisabled();
  493. this.clickListener();
  494. if (this.options.liveSearch) this.liveSearchListener();
  495. this.render();
  496. this.setStyle();
  497. this.setWidth();
  498. if (this.options.container) {
  499. this.selectPosition();
  500. } else {
  501. this.$element.on('hide.bs.select', function () {
  502. if (that.isVirtual()) {
  503. // empty menu on close
  504. var menuInner = that.$menuInner[0],
  505. emptyMenu = menuInner.firstChild.cloneNode(false);
  506. // replace the existing UL with an empty one - this is faster than $.empty() or innerHTML = ''
  507. menuInner.replaceChild(emptyMenu, menuInner.firstChild);
  508. menuInner.scrollTop = 0;
  509. }
  510. });
  511. }
  512. this.$menu.data('this', this);
  513. this.$newElement.data('this', this);
  514. if (this.options.mobile) this.mobile();
  515. this.$newElement.on({
  516. 'hide.bs.dropdown': function (e) {
  517. that.$menuInner.attr('aria-expanded', false);
  518. that.$element.trigger('hide.bs.select', e);
  519. },
  520. 'hidden.bs.dropdown': function (e) {
  521. that.$element.trigger('hidden.bs.select', e);
  522. },
  523. 'show.bs.dropdown': function (e) {
  524. that.$menuInner.attr('aria-expanded', true);
  525. that.$element.trigger('show.bs.select', e);
  526. },
  527. 'shown.bs.dropdown': function (e) {
  528. that.$element.trigger('shown.bs.select', e);
  529. }
  530. });
  531. if (that.$element[0].hasAttribute('required')) {
  532. this.$element.on('invalid', function () {
  533. that.$button.addClass('bs-invalid');
  534. that.$element.on({
  535. 'shown.bs.select': function () {
  536. that.$element
  537. .val(that.$element.val()) // set the value to hide the validation message in Chrome when menu is opened
  538. .off('shown.bs.select');
  539. },
  540. 'rendered.bs.select': function () {
  541. // if select is no longer invalid, remove the bs-invalid class
  542. if (this.validity.valid) that.$button.removeClass('bs-invalid');
  543. that.$element.off('rendered.bs.select');
  544. }
  545. });
  546. that.$button.on('blur.bs.select', function () {
  547. that.$element.focus().blur();
  548. that.$button.off('blur.bs.select');
  549. });
  550. });
  551. }
  552. setTimeout(function () {
  553. that.$element.trigger('loaded.bs.select');
  554. });
  555. },
  556. createDropdown: function () {
  557. // Options
  558. // If we are multiple or showTick option is set, then add the show-tick class
  559. var showTick = (this.multiple || this.options.showTick) ? ' show-tick' : '',
  560. autofocus = this.autofocus ? ' autofocus' : '';
  561. // Elements
  562. var header = this.options.header ? '<div class="' + classNames.POPOVERHEADER + '"><button type="button" class="close" aria-hidden="true">&times;</button>' + this.options.header + '</div>' : '';
  563. var searchbox = this.options.liveSearch ?
  564. '<div class="bs-searchbox">' +
  565. '<input type="text" class="form-control" autocomplete="off"' +
  566. (null === this.options.liveSearchPlaceholder ? '' : ' placeholder="' + htmlEscape(this.options.liveSearchPlaceholder) + '"') + ' role="textbox" aria-label="Search">' +
  567. '</div>'
  568. : '';
  569. var actionsbox = this.multiple && this.options.actionsBox ?
  570. '<div class="bs-actionsbox">' +
  571. '<div class="btn-group btn-group-sm btn-block">' +
  572. '<button type="button" class="actions-btn bs-select-all btn ' + classNames.BUTTONCLASS + '">' +
  573. this.options.selectAllText +
  574. '</button>' +
  575. '<button type="button" class="actions-btn bs-deselect-all btn ' + classNames.BUTTONCLASS + '">' +
  576. this.options.deselectAllText +
  577. '</button>' +
  578. '</div>' +
  579. '</div>'
  580. : '';
  581. var donebutton = this.multiple && this.options.doneButton ?
  582. '<div class="bs-donebutton">' +
  583. '<div class="btn-group btn-block">' +
  584. '<button type="button" class="btn btn-sm ' + classNames.BUTTONCLASS + '">' +
  585. this.options.doneButtonText +
  586. '</button>' +
  587. '</div>' +
  588. '</div>'
  589. : '';
  590. var drop =
  591. '<div class="dropdown bootstrap-select' + showTick + '">' +
  592. '<button type="button" class="' + this.options.styleBase + ' dropdown-toggle" ' + (this.options.display === 'static' ? 'data-display="static"' : '') + 'data-toggle="dropdown"' + autofocus + ' role="button">' +
  593. '<div class="filter-option">' +
  594. '<div class="filter-option-inner">' +
  595. '<div class="filter-option-inner-inner"></div>' +
  596. '</div> ' +
  597. '</div>' +
  598. (version.major === '4' ?
  599. '' :
  600. '<span class="bs-caret">' +
  601. this.options.template.caret +
  602. '</span>'
  603. ) +
  604. '</button>' +
  605. '<div class="' + classNames.MENU + ' ' + (version.major === '4' ? '' : classNames.SHOW) + '" role="combobox">' +
  606. header +
  607. searchbox +
  608. actionsbox +
  609. '<div class="inner ' + classNames.SHOW + '" role="listbox" aria-expanded="false" tabindex="-1">' +
  610. '<ul class="' + classNames.MENU + ' inner ' + (version.major === '4' ? classNames.SHOW : '') + '">' +
  611. '</ul>' +
  612. '</div>' +
  613. donebutton +
  614. '</div>' +
  615. '</div>';
  616. return $(drop);
  617. },
  618. setPositionData: function () {
  619. this.selectpicker.view.canHighlight = [];
  620. for (var i = 0; i < this.selectpicker.current.data.length; i++) {
  621. var li = this.selectpicker.current.data[i],
  622. canHighlight = true;
  623. if (li.type === 'divider') {
  624. canHighlight = false;
  625. li.height = this.sizeInfo.dividerHeight;
  626. } else if (li.type === 'optgroup-label') {
  627. canHighlight = false;
  628. li.height = this.sizeInfo.dropdownHeaderHeight;
  629. } else {
  630. li.height = this.sizeInfo.liHeight;
  631. }
  632. if (li.disabled) canHighlight = false;
  633. this.selectpicker.view.canHighlight.push(canHighlight);
  634. li.position = (i === 0 ? 0 : this.selectpicker.current.data[i - 1].position) + li.height;
  635. }
  636. },
  637. isVirtual: function () {
  638. return (this.options.virtualScroll !== false) && this.selectpicker.main.elements.length >= this.options.virtualScroll || this.options.virtualScroll === true;
  639. },
  640. createView: function (isSearching, scrollTop) {
  641. scrollTop = scrollTop || 0;
  642. var that = this;
  643. this.selectpicker.current = isSearching ? this.selectpicker.search : this.selectpicker.main;
  644. var $lis;
  645. var active = [];
  646. var selected;
  647. var prevActive;
  648. var activeIndex;
  649. var prevActiveIndex;
  650. this.setPositionData();
  651. scroll(scrollTop, true);
  652. this.$menuInner.off('scroll.createView').on('scroll.createView', function (e, updateValue) {
  653. if (!that.noScroll) scroll(this.scrollTop, updateValue);
  654. that.noScroll = false;
  655. });
  656. function scroll(scrollTop, init) {
  657. var size = that.selectpicker.current.elements.length,
  658. chunks = [],
  659. chunkSize,
  660. chunkCount,
  661. firstChunk,
  662. lastChunk,
  663. currentChunk = undefined,
  664. prevPositions,
  665. positionIsDifferent,
  666. previousElements,
  667. menuIsDifferent = true,
  668. isVirtual = that.isVirtual();
  669. that.selectpicker.view.scrollTop = scrollTop;
  670. if (isVirtual === true) {
  671. // if an option that is encountered that is wider than the current menu width, update the menu width accordingly
  672. if (that.sizeInfo.hasScrollBar && that.$menu[0].offsetWidth > that.sizeInfo.totalMenuWidth) {
  673. that.sizeInfo.menuWidth = that.$menu[0].offsetWidth;
  674. that.sizeInfo.totalMenuWidth = that.sizeInfo.menuWidth + that.sizeInfo.scrollBarWidth;
  675. that.$menu.css('min-width', that.sizeInfo.menuWidth);
  676. }
  677. }
  678. chunkSize = Math.ceil(that.sizeInfo.menuInnerHeight / that.sizeInfo.liHeight * 1.5); // number of options in a chunk
  679. chunkCount = Math.round(size / chunkSize) || 1; // number of chunks
  680. for (var i = 0; i < chunkCount; i++) {
  681. var end_of_chunk = (i + 1) * chunkSize;
  682. if (i === chunkCount - 1) {
  683. end_of_chunk = size;
  684. }
  685. chunks[i] = [
  686. (i) * chunkSize + (!i ? 0 : 1),
  687. end_of_chunk
  688. ];
  689. if (!size) break;
  690. if (currentChunk === undefined && scrollTop <= that.selectpicker.current.data[end_of_chunk - 1].position - that.sizeInfo.menuInnerHeight) {
  691. currentChunk = i;
  692. }
  693. }
  694. if (currentChunk === undefined) currentChunk = 0;
  695. prevPositions = [that.selectpicker.view.position0, that.selectpicker.view.position1];
  696. // always display previous, current, and next chunks
  697. firstChunk = Math.max(0, currentChunk - 1);
  698. lastChunk = Math.min(chunkCount - 1, currentChunk + 1);
  699. that.selectpicker.view.position0 = Math.max(0, chunks[firstChunk][0]) || 0;
  700. that.selectpicker.view.position1 = Math.min(size, chunks[lastChunk][1]) || 0;
  701. positionIsDifferent = prevPositions[0] !== that.selectpicker.view.position0 || prevPositions[1] !== that.selectpicker.view.position1;
  702. if (that.activeIndex !== undefined) {
  703. prevActive = that.selectpicker.current.elements[that.selectpicker.current.map.newIndex[that.prevActiveIndex]];
  704. active = that.selectpicker.current.elements[that.selectpicker.current.map.newIndex[that.activeIndex]];
  705. selected = that.selectpicker.current.elements[that.selectpicker.current.map.newIndex[that.selectedIndex]];
  706. if (init) {
  707. if (that.activeIndex !== that.selectedIndex) {
  708. active.classList.remove('active');
  709. if (active.firstChild) active.firstChild.classList.remove('active');
  710. }
  711. that.activeIndex = undefined;
  712. }
  713. if (that.activeIndex && that.activeIndex !== that.selectedIndex && selected && selected.length) {
  714. selected.classList.remove('active');
  715. if (selected.firstChild) selected.firstChild.classList.remove('active');
  716. }
  717. }
  718. if (that.prevActiveIndex !== undefined && that.prevActiveIndex !== that.activeIndex && that.prevActiveIndex !== that.selectedIndex && prevActive && prevActive.length) {
  719. prevActive.classList.remove('active');
  720. if (prevActive.firstChild) prevActive.firstChild.classList.remove('active');
  721. }
  722. if (init || positionIsDifferent) {
  723. previousElements = that.selectpicker.view.visibleElements ? that.selectpicker.view.visibleElements.slice() : [];
  724. that.selectpicker.view.visibleElements = that.selectpicker.current.elements.slice(that.selectpicker.view.position0, that.selectpicker.view.position1);
  725. that.setOptionStatus();
  726. // if searching, check to make sure the list has actually been updated before updating DOM
  727. // this prevents unnecessary repaints
  728. if ( isSearching || (isVirtual === false && init) ) menuIsDifferent = !isEqual(previousElements, that.selectpicker.view.visibleElements);
  729. // if virtual scroll is disabled and not searching,
  730. // menu should never need to be updated more than once
  731. if ( (init || isVirtual === true) && menuIsDifferent ) {
  732. var menuInner = that.$menuInner[0],
  733. menuFragment = document.createDocumentFragment(),
  734. emptyMenu = menuInner.firstChild.cloneNode(false),
  735. marginTop,
  736. marginBottom,
  737. elements = isVirtual === true ? that.selectpicker.view.visibleElements : that.selectpicker.current.elements;
  738. // replace the existing UL with an empty one - this is faster than $.empty()
  739. menuInner.replaceChild(emptyMenu, menuInner.firstChild);
  740. for (var i = 0, visibleElementsLen = elements.length; i < visibleElementsLen; i++) {
  741. menuFragment.appendChild(elements[i]);
  742. }
  743. if (isVirtual === true) {
  744. marginTop = (that.selectpicker.view.position0 === 0 ? 0 : that.selectpicker.current.data[that.selectpicker.view.position0 - 1].position),
  745. marginBottom = (that.selectpicker.view.position1 > size - 1 ? 0 : that.selectpicker.current.data[size - 1].position - that.selectpicker.current.data[that.selectpicker.view.position1 - 1].position);
  746. menuInner.firstChild.style.marginTop = marginTop + 'px';
  747. menuInner.firstChild.style.marginBottom = marginBottom + 'px';
  748. }
  749. menuInner.firstChild.appendChild(menuFragment);
  750. }
  751. }
  752. that.prevActiveIndex = that.activeIndex;
  753. if (!that.options.liveSearch) {
  754. that.$menuInner.focus();
  755. } else if (isSearching && init) {
  756. var index = 0,
  757. newActive;
  758. if (!that.selectpicker.view.canHighlight[index]) {
  759. index = 1 + that.selectpicker.view.canHighlight.slice(1).indexOf(true);
  760. }
  761. newActive = that.selectpicker.view.visibleElements[index];
  762. if (that.selectpicker.view.currentActive) {
  763. that.selectpicker.view.currentActive.classList.remove('active');
  764. if (that.selectpicker.view.currentActive.firstChild) that.selectpicker.view.currentActive.firstChild.classList.remove('active');
  765. }
  766. if (newActive) {
  767. newActive.classList.add('active');
  768. if (newActive.firstChild) newActive.firstChild.classList.add('active');
  769. }
  770. that.activeIndex = that.selectpicker.current.map.originalIndex[index];
  771. }
  772. }
  773. $(window).off('resize.createView').on('resize.createView', function () {
  774. scroll(that.$menuInner[0].scrollTop);
  775. });
  776. },
  777. createLi: function () {
  778. var that = this,
  779. mainElements = [],
  780. widestOption,
  781. availableOptionsCount = 0,
  782. widestOptionLength = 0,
  783. mainData = [],
  784. optID = 0,
  785. headerIndex = 0,
  786. liIndex = -1; // increment liIndex whenever a new <li> element is created to ensure newIndex is correct
  787. if (!this.selectpicker.view.titleOption) this.selectpicker.view.titleOption = document.createElement('option');
  788. var elementTemplates = {
  789. span: document.createElement('span'),
  790. subtext: document.createElement('small'),
  791. a: document.createElement('a'),
  792. li: document.createElement('li'),
  793. whitespace: document.createTextNode("\u00A0")
  794. },
  795. checkMark = elementTemplates.span.cloneNode(false),
  796. fragment = document.createDocumentFragment();
  797. checkMark.className = that.options.iconBase + ' ' + that.options.tickIcon + ' check-mark';
  798. elementTemplates.a.appendChild(checkMark);
  799. elementTemplates.a.setAttribute('role', 'option');
  800. elementTemplates.subtext.className = 'text-muted';
  801. elementTemplates.text = elementTemplates.span.cloneNode(false);
  802. elementTemplates.text.className = 'text';
  803. // Helper functions
  804. /**
  805. * @param content
  806. * @param [index]
  807. * @param [classes]
  808. * @param [optgroup]
  809. * @returns {HTMLElement}
  810. */
  811. var generateLI = function (content, index, classes, optgroup) {
  812. var li = elementTemplates.li.cloneNode(false);
  813. if (content) {
  814. if (content.nodeType === 1 || content.nodeType === 11) {
  815. li.appendChild(content);
  816. } else {
  817. li.innerHTML = content;
  818. }
  819. }
  820. if (typeof classes !== 'undefined' && '' !== classes) li.className = classes;
  821. if (typeof optgroup !== 'undefined' && null !== optgroup) li.classList.add('optgroup-' + optgroup);
  822. return li;
  823. };
  824. /**
  825. * @param text
  826. * @param [classes]
  827. * @param [inline]
  828. * @returns {string}
  829. */
  830. var generateA = function (text, classes, inline) {
  831. var a = elementTemplates.a.cloneNode(true);
  832. if (text) {
  833. if (text.nodeType === 11) {
  834. a.appendChild(text);
  835. } else {
  836. a.insertAdjacentHTML('beforeend', text);
  837. }
  838. }
  839. if (typeof classes !== 'undefined' & '' !== classes) a.className = classes;
  840. if (version.major === '4') a.classList.add('dropdown-item');
  841. if (inline) a.setAttribute('style', inline);
  842. return a;
  843. };
  844. var generateText = function (options) {
  845. var textElement = elementTemplates.text.cloneNode(false),
  846. optionSubtextElement,
  847. optionIconElement;
  848. if (options.optionContent) {
  849. textElement.innerHTML = options.optionContent;
  850. } else {
  851. textElement.textContent = options.text;
  852. if (options.optionIcon) {
  853. var whitespace = elementTemplates.whitespace.cloneNode(false);
  854. optionIconElement = elementTemplates.span.cloneNode(false);
  855. optionIconElement.className = that.options.iconBase + ' ' + options.optionIcon;
  856. fragment.appendChild(optionIconElement);
  857. fragment.appendChild(whitespace);
  858. }
  859. if (options.optionSubtext) {
  860. optionSubtextElement = elementTemplates.subtext.cloneNode(false);
  861. optionSubtextElement.innerHTML = options.optionSubtext;
  862. textElement.appendChild(optionSubtextElement);
  863. }
  864. }
  865. fragment.appendChild(textElement);
  866. return fragment;
  867. };
  868. var generateLabel = function (options) {
  869. var labelTextElement = elementTemplates.text.cloneNode(false),
  870. labelSubtextElement,
  871. labelIconElement;
  872. labelTextElement.innerHTML = options.labelEscaped;
  873. if (options.labelIcon) {
  874. var whitespace = elementTemplates.whitespace.cloneNode(false);
  875. labelIconElement = elementTemplates.span.cloneNode(false);
  876. labelIconElement.className = that.options.iconBase + ' ' + options.labelIcon;
  877. fragment.appendChild(labelIconElement);
  878. fragment.appendChild(whitespace);
  879. }
  880. if (options.labelSubtext) {
  881. labelSubtextElement = elementTemplates.subtext.cloneNode(false);
  882. labelSubtextElement.textContent = options.labelSubtext;
  883. labelTextElement.appendChild(labelSubtextElement);
  884. }
  885. fragment.appendChild(labelTextElement);
  886. return fragment;
  887. }
  888. if (this.options.title && !this.multiple) {
  889. // this option doesn't create a new <li> element, but does add a new option, so liIndex is decreased
  890. // since newIndex is recalculated on every refresh, liIndex needs to be decreased even if the titleOption is already appended
  891. liIndex--;
  892. var element = this.$element[0],
  893. isSelected = false,
  894. titleNotAppended = !this.selectpicker.view.titleOption.parentNode;
  895. if (titleNotAppended) {
  896. // Use native JS to prepend option (faster)
  897. this.selectpicker.view.titleOption.className = 'bs-title-option';
  898. this.selectpicker.view.titleOption.value = '';
  899. // Check if selected or data-selected attribute is already set on an option. If not, select the titleOption option.
  900. // the selected item may have been changed by user or programmatically before the bootstrap select plugin runs,
  901. // if so, the select will have the data-selected attribute
  902. var $opt = $(element.options[element.selectedIndex]);
  903. isSelected = $opt.attr('selected') === undefined && this.$element.data('selected') === undefined;
  904. }
  905. if (titleNotAppended || this.selectpicker.view.titleOption.index !== 0) {
  906. element.insertBefore(this.selectpicker.view.titleOption, element.firstChild);
  907. }
  908. // Set selected *after* appending to select,
  909. // otherwise the option doesn't get selected in IE
  910. // set using selectedIndex, as setting the selected attr to true here doesn't work in IE11
  911. if (isSelected) element.selectedIndex = 0;
  912. }
  913. var $selectOptions = this.$element.find('option');
  914. $selectOptions.each(function (index) {
  915. var $this = $(this);
  916. liIndex++;
  917. if ($this.hasClass('bs-title-option')) return;
  918. var thisData = $this.data();
  919. // Get the class and text for the option
  920. var optionClass = this.className || '',
  921. inline = htmlEscape(this.style.cssText),
  922. optionContent = thisData.content,
  923. text = this.textContent,
  924. tokens = thisData.tokens,
  925. subtext = thisData.subtext,
  926. icon = thisData.icon,
  927. $parent = $this.parent(),
  928. parent = $parent[0],
  929. isOptgroup = parent.tagName === 'OPTGROUP',
  930. isOptgroupDisabled = isOptgroup && parent.disabled,
  931. isDisabled = this.disabled || isOptgroupDisabled,
  932. prevHiddenIndex,
  933. showDivider = this.previousElementSibling && this.previousElementSibling.tagName === 'OPTGROUP',
  934. textElement;
  935. var parentData = $parent.data();
  936. if (thisData.hidden === true || that.options.hideDisabled && (isDisabled && !isOptgroup || isOptgroupDisabled)) {
  937. // set prevHiddenIndex - the index of the first hidden option in a group of hidden options
  938. // used to determine whether or not a divider should be placed after an optgroup if there are
  939. // hidden options between the optgroup and the first visible option
  940. prevHiddenIndex = thisData.prevHiddenIndex;
  941. $this.next().data('prevHiddenIndex', (prevHiddenIndex !== undefined ? prevHiddenIndex : index));
  942. liIndex--;
  943. // if previous element is not an optgroup
  944. if (!showDivider) {
  945. if (prevHiddenIndex !== undefined) {
  946. // select the element **before** the first hidden element in the group
  947. var prevHidden = $selectOptions[prevHiddenIndex].previousElementSibling;
  948. if (prevHidden && prevHidden.tagName === 'OPTGROUP' && !prevHidden.disabled) {
  949. showDivider = true;
  950. }
  951. }
  952. }
  953. if (showDivider && mainData[mainData.length - 1].type !== 'divider') {
  954. liIndex++;
  955. mainElements.push(
  956. generateLI(
  957. false,
  958. null,
  959. classNames.DIVIDER,
  960. optID + 'div'
  961. )
  962. );
  963. mainData.push({
  964. type: 'divider',
  965. optID: optID
  966. });
  967. }
  968. return;
  969. }
  970. if (isOptgroup && thisData.divider !== true) {
  971. if (that.options.hideDisabled && isDisabled) {
  972. if (parentData.allOptionsDisabled === undefined) {
  973. var $options = $parent.children();
  974. $parent.data('allOptionsDisabled', $options.filter(':disabled').length === $options.length);
  975. }
  976. if ($parent.data('allOptionsDisabled')) {
  977. liIndex--;
  978. return;
  979. }
  980. }
  981. var optGroupClass = ' ' + parent.className || '';
  982. if (!this.previousElementSibling) { // Is it the first option of the optgroup?
  983. optID += 1;
  984. // Get the opt group label
  985. var label = parent.label,
  986. labelEscaped = htmlEscape(label),
  987. labelSubtext = parentData.subtext,
  988. labelIcon = parentData.icon;
  989. if (index !== 0 && mainElements.length > 0) { // Is it NOT the first option of the select && are there elements in the dropdown?
  990. liIndex++;
  991. mainElements.push(
  992. generateLI(
  993. false,
  994. null,
  995. classNames.DIVIDER,
  996. optID + 'div'
  997. )
  998. );
  999. mainData.push({
  1000. type: 'divider',
  1001. optID: optID
  1002. });
  1003. }
  1004. liIndex++;
  1005. var labelElement = generateLabel({
  1006. labelEscaped: labelEscaped,
  1007. labelSubtext: labelSubtext,
  1008. labelIcon: labelIcon
  1009. });
  1010. mainElements.push(generateLI(labelElement, null, 'dropdown-header' + optGroupClass, optID));
  1011. mainData.push({
  1012. content: labelEscaped,
  1013. subtext: labelSubtext,
  1014. type: 'optgroup-label',
  1015. optID: optID
  1016. });
  1017. headerIndex = liIndex - 1;
  1018. }
  1019. if (that.options.hideDisabled && isDisabled || thisData.hidden === true) {
  1020. liIndex--;
  1021. return;
  1022. }
  1023. textElement = generateText({
  1024. text: text,
  1025. optionContent: optionContent,
  1026. optionSubtext: subtext,
  1027. optionIcon: icon
  1028. });
  1029. mainElements.push(generateLI(generateA(textElement, 'opt ' + optionClass + optGroupClass, inline), index, '', optID));
  1030. mainData.push({
  1031. content: optionContent || text,
  1032. subtext: subtext,
  1033. tokens: tokens,
  1034. type: 'option',
  1035. optID: optID,
  1036. headerIndex: headerIndex,
  1037. lastIndex: headerIndex + parent.childElementCount,
  1038. originalIndex: index,
  1039. data: thisData
  1040. });
  1041. availableOptionsCount++;
  1042. } else if (thisData.divider === true) {
  1043. mainElements.push(generateLI(false, index, classNames.DIVIDER));
  1044. mainData.push({
  1045. type: 'divider',
  1046. originalIndex: index,
  1047. data: thisData
  1048. });
  1049. } else {
  1050. // if previous element is not an optgroup and hideDisabled is true
  1051. if (!showDivider && that.options.hideDisabled) {
  1052. prevHiddenIndex = thisData.prevHiddenIndex;
  1053. if (prevHiddenIndex !== undefined) {
  1054. // select the element **before** the first hidden element in the group
  1055. var prevHidden = $selectOptions[prevHiddenIndex].previousElementSibling;
  1056. if (prevHidden && prevHidden.tagName === 'OPTGROUP' && !prevHidden.disabled) {
  1057. showDivider = true;
  1058. }
  1059. }
  1060. }
  1061. if (showDivider && mainData[mainData.length - 1].type !== 'divider') {
  1062. liIndex++;
  1063. mainElements.push(
  1064. generateLI(
  1065. false,
  1066. null,
  1067. classNames.DIVIDER,
  1068. optID + 'div'
  1069. )
  1070. );
  1071. mainData.push({
  1072. type: 'divider',
  1073. optID: optID
  1074. });
  1075. }
  1076. textElement = generateText({
  1077. text: text,
  1078. optionContent: optionContent,
  1079. optionSubtext: subtext,
  1080. optionIcon: icon
  1081. });
  1082. mainElements.push(generateLI(generateA(textElement, optionClass, inline), index));
  1083. mainData.push({
  1084. content: optionContent || text,
  1085. subtext: subtext,
  1086. tokens: tokens,
  1087. type: 'option',
  1088. originalIndex: index,
  1089. data: thisData
  1090. });
  1091. availableOptionsCount++;
  1092. }
  1093. that.selectpicker.main.map.newIndex[index] = liIndex;
  1094. that.selectpicker.main.map.originalIndex[liIndex] = index;
  1095. // get the most recent option info added to mainData
  1096. var _mainDataLast = mainData[mainData.length - 1];
  1097. _mainDataLast.disabled = isDisabled;
  1098. var combinedLength = 0;
  1099. // count the number of characters in the option - not perfect, but should work in most cases
  1100. if (_mainDataLast.content) combinedLength += _mainDataLast.content.length;
  1101. if (_mainDataLast.subtext) combinedLength += _mainDataLast.subtext.length;
  1102. // if there is an icon, ensure this option's width is checked
  1103. if (icon) combinedLength += 1;
  1104. if (combinedLength > widestOptionLength) {
  1105. widestOptionLength = combinedLength;
  1106. // guess which option is the widest
  1107. // use this when calculating menu width
  1108. // not perfect, but it's fast, and the width will be updating accordingly when scrolling
  1109. widestOption = mainElements[mainElements.length - 1];
  1110. }
  1111. });
  1112. this.selectpicker.main.elements = mainElements;
  1113. this.selectpicker.main.data = mainData;
  1114. this.selectpicker.current = this.selectpicker.main;
  1115. this.selectpicker.view.widestOption = widestOption;
  1116. this.selectpicker.view.availableOptionsCount = availableOptionsCount; // faster way to get # of available options without filter
  1117. },
  1118. findLis: function () {
  1119. return this.$menuInner.find('.inner > li');
  1120. },
  1121. render: function () {
  1122. var that = this,
  1123. $selectOptions = this.$element.find('option'),
  1124. selectedItems = [],
  1125. selectedItemsInTitle = [];
  1126. this.togglePlaceholder();
  1127. this.tabIndex();
  1128. for (var i = 0, len = this.selectpicker.main.elements.length; i < len; i++) {
  1129. var index = this.selectpicker.main.map.originalIndex[i],
  1130. option = $selectOptions[index];
  1131. if (option && option.selected) {
  1132. selectedItems.push(option);
  1133. if (selectedItemsInTitle.length < 100 && that.options.selectedTextFormat !== 'count' || selectedItems.length === 1) {
  1134. if (that.options.hideDisabled && (option.disabled || option.parentNode.tagName === 'OPTGROUP' && option.parentNode.disabled)) return;
  1135. var thisData = this.selectpicker.main.data[i].data,
  1136. icon = thisData.icon && that.options.showIcon ? '<i class="' + that.options.iconBase + ' ' + thisData.icon + '"></i> ' : '',
  1137. subtext,
  1138. titleItem;
  1139. if (that.options.showSubtext && thisData.subtext && !that.multiple) {
  1140. subtext = ' <small class="text-muted">' + thisData.subtext + '</small>';
  1141. } else {
  1142. subtext = '';
  1143. }
  1144. if (option.title) {
  1145. titleItem = option.title;
  1146. } else if (thisData.content && that.options.showContent) {
  1147. titleItem = thisData.content.toString();
  1148. } else {
  1149. titleItem = icon + option.innerHTML.trim() + subtext;
  1150. }
  1151. selectedItemsInTitle.push(titleItem);
  1152. }
  1153. }
  1154. }
  1155. //Fixes issue in IE10 occurring when no default option is selected and at least one option is disabled
  1156. //Convert all the values into a comma delimited string
  1157. var title = !this.multiple ? selectedItemsInTitle[0] : selectedItemsInTitle.join(this.options.multipleSeparator);
  1158. // add ellipsis
  1159. if (selectedItems.length > 50) title += '...';
  1160. // If this is a multiselect, and selectedTextFormat is count, then show 1 of 2 selected etc..
  1161. if (this.multiple && this.options.selectedTextFormat.indexOf('count') !== -1) {
  1162. var max = this.options.selectedTextFormat.split('>');
  1163. if ((max.length > 1 && selectedItems.length > max[1]) || (max.length === 1 && selectedItems.length >= 2)) {
  1164. var totalCount = this.selectpicker.view.availableOptionsCount,
  1165. tr8nText = (typeof this.options.countSelectedText === 'function') ? this.options.countSelectedText(selectedItems.length, totalCount) : this.options.countSelectedText;
  1166. title = tr8nText.replace('{0}', selectedItems.length.toString()).replace('{1}', totalCount.toString());
  1167. }
  1168. }
  1169. if (this.options.title == undefined) {
  1170. // use .attr to ensure undefined is returned if title attribute is not set
  1171. this.options.title = this.$element.attr('title');
  1172. }
  1173. if (this.options.selectedTextFormat == 'static') {
  1174. title = this.options.title;
  1175. }
  1176. //If we dont have a title, then use the default, or if nothing is set at all, use the not selected text
  1177. if (!title) {
  1178. title = typeof this.options.title !== 'undefined' ? this.options.title : this.options.noneSelectedText;
  1179. }
  1180. //strip all HTML tags and trim the result, then unescape any escaped tags
  1181. this.$button[0].title = htmlUnescape(title.replace(/<[^>]*>?/g, '').trim());
  1182. this.$button.find('.filter-option-inner-inner')[0].innerHTML = title;
  1183. this.$element.trigger('rendered.bs.select');
  1184. },
  1185. /**
  1186. * @param [style]
  1187. * @param [status]
  1188. */
  1189. setStyle: function (style, status) {
  1190. if (this.$element.attr('class')) {
  1191. this.$newElement.addClass(this.$element.attr('class').replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi, ''));
  1192. }
  1193. var buttonClass = style ? style : this.options.style;
  1194. if (status == 'add') {
  1195. this.$button.addClass(buttonClass);
  1196. } else if (status == 'remove') {
  1197. this.$button.removeClass(buttonClass);
  1198. } else {
  1199. this.$button.removeClass(this.options.style);
  1200. this.$button.addClass(buttonClass);
  1201. }
  1202. },
  1203. liHeight: function (refresh) {
  1204. if (!refresh && (this.options.size === false || this.sizeInfo)) return;
  1205. if (!this.sizeInfo) this.sizeInfo = {};
  1206. var newElement = document.createElement('div'),
  1207. menu = document.createElement('div'),
  1208. menuInner = document.createElement('div'),
  1209. menuInnerInner = document.createElement('ul'),
  1210. divider = document.createElement('li'),
  1211. dropdownHeader = document.createElement('li'),
  1212. li = document.createElement('li'),
  1213. a = document.createElement('a'),
  1214. text = document.createElement('span'),
  1215. header = this.options.header && this.$menu.find('.' + classNames.POPOVERHEADER).length > 0 ? this.$menu.find('.' + classNames.POPOVERHEADER)[0].cloneNode(true) : null,
  1216. search = this.options.liveSearch ? document.createElement('div') : null,
  1217. actions = this.options.actionsBox && this.multiple && this.$menu.find('.bs-actionsbox').length > 0 ? this.$menu.find('.bs-actionsbox')[0].cloneNode(true) : null,
  1218. doneButton = this.options.doneButton && this.multiple && this.$menu.find('.bs-donebutton').length > 0 ? this.$menu.find('.bs-donebutton')[0].cloneNode(true) : null;
  1219. this.sizeInfo.selectWidth = this.$newElement[0].offsetWidth;
  1220. text.className = 'text';
  1221. a.className = 'dropdown-item ' + this.$element.find('option')[0].className;
  1222. newElement.className = this.$menu[0].parentNode.className + ' ' + classNames.SHOW;
  1223. newElement.style.width = this.sizeInfo.selectWidth + 'px';
  1224. if (this.options.width === 'auto') menu.style.minWidth = 0;
  1225. menu.className = classNames.MENU + ' ' + classNames.SHOW;
  1226. menuInner.className = 'inner ' + classNames.SHOW;
  1227. menuInnerInner.className = classNames.MENU + ' inner ' + (version.major === '4' ? classNames.SHOW : '');
  1228. divider.className = classNames.DIVIDER;
  1229. dropdownHeader.className = 'dropdown-header';
  1230. text.appendChild(document.createTextNode('Inner text'));
  1231. a.appendChild(text);
  1232. li.appendChild(a);
  1233. dropdownHeader.appendChild(text.cloneNode(true));
  1234. if (this.selectpicker.view.widestOption) {
  1235. menuInnerInner.appendChild(this.selectpicker.view.widestOption.cloneNode(true));
  1236. }
  1237. menuInnerInner.appendChild(li);
  1238. menuInnerInner.appendChild(divider);
  1239. menuInnerInner.appendChild(dropdownHeader);
  1240. if (header) menu.appendChild(header);
  1241. if (search) {
  1242. var input = document.createElement('input');
  1243. search.className = 'bs-searchbox';
  1244. input.className = 'form-control';
  1245. search.appendChild(input);
  1246. menu.appendChild(search);
  1247. }
  1248. if (actions) menu.appendChild(actions);
  1249. menuInner.appendChild(menuInnerInner);
  1250. menu.appendChild(menuInner);
  1251. if (doneButton) menu.appendChild(doneButton);
  1252. newElement.appendChild(menu);
  1253. document.body.appendChild(newElement);
  1254. var liHeight = a.offsetHeight,
  1255. dropdownHeaderHeight = dropdownHeader ? dropdownHeader.offsetHeight : 0,
  1256. headerHeight = header ? header.offsetHeight : 0,
  1257. searchHeight = search ? search.offsetHeight : 0,
  1258. actionsHeight = actions ? actions.offsetHeight : 0,
  1259. doneButtonHeight = doneButton ? doneButton.offsetHeight : 0,
  1260. dividerHeight = $(divider).outerHeight(true),
  1261. // fall back to jQuery if getComputedStyle is not supported
  1262. menuStyle = window.getComputedStyle ? window.getComputedStyle(menu) : false,
  1263. menuWidth = menu.offsetWidth,
  1264. $menu = menuStyle ? null : $(menu),
  1265. menuPadding = {
  1266. vert: toInteger(menuStyle ? menuStyle.paddingTop : $menu.css('paddingTop')) +
  1267. toInteger(menuStyle ? menuStyle.paddingBottom : $menu.css('paddingBottom')) +
  1268. toInteger(menuStyle ? menuStyle.borderTopWidth : $menu.css('borderTopWidth')) +
  1269. toInteger(menuStyle ? menuStyle.borderBottomWidth : $menu.css('borderBottomWidth')),
  1270. horiz: toInteger(menuStyle ? menuStyle.paddingLeft : $menu.css('paddingLeft')) +
  1271. toInteger(menuStyle ? menuStyle.paddingRight : $menu.css('paddingRight')) +
  1272. toInteger(menuStyle ? menuStyle.borderLeftWidth : $menu.css('borderLeftWidth')) +
  1273. toInteger(menuStyle ? menuStyle.borderRightWidth : $menu.css('borderRightWidth'))
  1274. },
  1275. menuExtras = {
  1276. vert: menuPadding.vert +
  1277. toInteger(menuStyle ? menuStyle.marginTop : $menu.css('marginTop')) +
  1278. toInteger(menuStyle ? menuStyle.marginBottom : $menu.css('marginBottom')) + 2,
  1279. horiz: menuPadding.horiz +
  1280. toInteger(menuStyle ? menuStyle.marginLeft : $menu.css('marginLeft')) +
  1281. toInteger(menuStyle ? menuStyle.marginRight : $menu.css('marginRight')) + 2
  1282. },
  1283. scrollBarWidth;
  1284. menuInner.style.overflowY = 'scroll';
  1285. scrollBarWidth = menu.offsetWidth - menuWidth;
  1286. document.body.removeChild(newElement);
  1287. this.sizeInfo.liHeight = liHeight;
  1288. this.sizeInfo.dropdownHeaderHeight = dropdownHeaderHeight;
  1289. this.sizeInfo.headerHeight = headerHeight;
  1290. this.sizeInfo.searchHeight = searchHeight;
  1291. this.sizeInfo.actionsHeight = actionsHeight;
  1292. this.sizeInfo.doneButtonHeight = doneButtonHeight;
  1293. this.sizeInfo.dividerHeight = dividerHeight;
  1294. this.sizeInfo.menuPadding = menuPadding;
  1295. this.sizeInfo.menuExtras = menuExtras;
  1296. this.sizeInfo.menuWidth = menuWidth;
  1297. this.sizeInfo.totalMenuWidth = this.sizeInfo.menuWidth;
  1298. this.sizeInfo.scrollBarWidth = scrollBarWidth;
  1299. this.sizeInfo.selectHeight = this.$newElement[0].offsetHeight;
  1300. this.setPositionData();
  1301. },
  1302. getSelectPosition: function () {
  1303. var that = this,
  1304. $window = $(window),
  1305. pos = that.$newElement.offset(),
  1306. $container = $(that.options.container),
  1307. containerPos;
  1308. if (that.options.container && !$container.is('body')) {
  1309. containerPos = $container.offset();
  1310. containerPos.top += parseInt($container.css('borderTopWidth'));
  1311. containerPos.left += parseInt($container.css('borderLeftWidth'));
  1312. } else {
  1313. containerPos = { top: 0, left: 0 };
  1314. }
  1315. var winPad = that.options.windowPadding;
  1316. this.sizeInfo.selectOffsetTop = pos.top - containerPos.top - $window.scrollTop();
  1317. this.sizeInfo.selectOffsetBot = $window.height() - this.sizeInfo.selectOffsetTop - this.sizeInfo['selectHeight'] - containerPos.top - winPad[2];
  1318. this.sizeInfo.selectOffsetLeft = pos.left - containerPos.left - $window.scrollLeft();
  1319. this.sizeInfo.selectOffsetRight = $window.width() - this.sizeInfo.selectOffsetLeft - this.sizeInfo['selectWidth'] - containerPos.left - winPad[1];
  1320. this.sizeInfo.selectOffsetTop -= winPad[0];
  1321. this.sizeInfo.selectOffsetLeft -= winPad[3];
  1322. },
  1323. setMenuSize: function (isAuto) {
  1324. this.getSelectPosition();
  1325. var selectWidth = this.sizeInfo['selectWidth'],
  1326. liHeight = this.sizeInfo['liHeight'],
  1327. headerHeight = this.sizeInfo['headerHeight'],
  1328. searchHeight = this.sizeInfo['searchHeight'],
  1329. actionsHeight = this.sizeInfo['actionsHeight'],
  1330. doneButtonHeight = this.sizeInfo['doneButtonHeight'],
  1331. divHeight = this.sizeInfo['dividerHeight'],
  1332. menuPadding = this.sizeInfo['menuPadding'],
  1333. menuInnerHeight,
  1334. menuHeight,
  1335. divLength = 0,
  1336. minHeight,
  1337. _minHeight,
  1338. maxHeight,
  1339. menuInnerMinHeight,
  1340. estimate;
  1341. if (this.options.dropupAuto) {
  1342. // Get the estimated height of the menu without scrollbars.
  1343. // This is useful for smaller menus, where there might be plenty of room
  1344. // below the button without setting dropup, but we can't know
  1345. // the exact height of the menu until createView is called later
  1346. estimate = liHeight * this.selectpicker.current.elements.length + menuPadding.vert;
  1347. this.$newElement.toggleClass(classNames.DROPUP, this.sizeInfo.selectOffsetTop - this.sizeInfo.selectOffsetBot > this.sizeInfo.menuExtras.vert && estimate + this.sizeInfo.menuExtras.vert + 50 > this.sizeInfo.selectOffsetBot);
  1348. }
  1349. if (this.options.size === 'auto') {
  1350. _minHeight = this.selectpicker.current.elements.length > 3 ? this.sizeInfo.liHeight * 3 + this.sizeInfo.menuExtras.vert - 2 : 0;
  1351. menuHeight = this.sizeInfo.selectOffsetBot - this.sizeInfo.menuExtras.vert;
  1352. minHeight = _minHeight + headerHeight + searchHeight + actionsHeight + doneButtonHeight;
  1353. menuInnerMinHeight = Math.max(_minHeight - menuPadding.vert, 0);
  1354. if (this.$newElement.hasClass(classNames.DROPUP)) {
  1355. menuHeight = this.sizeInfo.selectOffsetTop - this.sizeInfo.menuExtras.vert;
  1356. }
  1357. maxHeight = menuHeight;
  1358. menuInnerHeight = menuHeight - headerHeight - searchHeight - actionsHeight - doneButtonHeight - menuPadding.vert;
  1359. } else if (this.options.size && this.options.size != 'auto' && this.selectpicker.current.elements.length > this.options.size) {
  1360. for (var i = 0; i < this.options.size; i++) {
  1361. if (this.selectpicker.current.data[i].type === 'divider') divLength++;
  1362. }
  1363. menuHeight = liHeight * this.options.size + divLength * divHeight + menuPadding.vert;
  1364. menuInnerHeight = menuHeight - menuPadding.vert;
  1365. maxHeight = menuHeight + headerHeight + searchHeight + actionsHeight + doneButtonHeight;
  1366. minHeight = menuInnerMinHeight = '';
  1367. }
  1368. if (this.options.dropdownAlignRight === 'auto') {
  1369. this.$menu.toggleClass(classNames.MENURIGHT, this.sizeInfo.selectOffsetLeft > this.sizeInfo.selectOffsetRight && this.sizeInfo.selectOffsetRight < (this.$menu[0].offsetWidth - selectWidth));
  1370. }
  1371. this.$menu.css({
  1372. 'max-height': maxHeight + 'px',
  1373. 'overflow': 'hidden',
  1374. 'min-height': minHeight + 'px'
  1375. });
  1376. this.$menuInner.css({
  1377. 'max-height': menuInnerHeight + 'px',
  1378. 'overflow-y': 'auto',
  1379. 'min-height': menuInnerMinHeight + 'px'
  1380. });
  1381. this.sizeInfo['menuInnerHeight'] = menuInnerHeight;
  1382. if (this.selectpicker.current.data.length && this.selectpicker.current.data[this.selectpicker.current.data.length - 1].position > this.sizeInfo.menuInnerHeight) {
  1383. this.sizeInfo.hasScrollBar = true;
  1384. this.sizeInfo.totalMenuWidth = this.sizeInfo.menuWidth + this.sizeInfo.scrollBarWidth;
  1385. this.$menu.css('min-width', this.sizeInfo.totalMenuWidth);
  1386. }
  1387. if (this.dropdown && this.dropdown._popper) this.dropdown._popper.update();
  1388. },
  1389. setSize: function (refresh) {
  1390. this.liHeight(refresh);
  1391. if (this.options.header) this.$menu.css('padding-top', 0);
  1392. if (this.options.size === false) return;
  1393. var that = this,
  1394. $window = $(window),
  1395. selectedIndex,
  1396. offset = 0;
  1397. this.setMenuSize();
  1398. if (this.options.size === 'auto') {
  1399. this.$searchbox.off('input.setMenuSize propertychange.setMenuSize').on('input.setMenuSize propertychange.setMenuSize', function() {
  1400. return that.setMenuSize();
  1401. });
  1402. $window.off('resize.setMenuSize scroll.setMenuSize').on('resize.setMenuSize scroll.setMenuSize', function() {
  1403. return that.setMenuSize();
  1404. });
  1405. } else if (this.options.size && this.options.size != 'auto' && this.selectpicker.current.elements.length > this.options.size) {
  1406. this.$searchbox.off('input.setMenuSize propertychange.setMenuSize');
  1407. $window.off('resize.setMenuSize scroll.setMenuSize');
  1408. }
  1409. if (refresh) {
  1410. offset = this.$menuInner[0].scrollTop;
  1411. } else if (!that.multiple) {
  1412. selectedIndex = that.selectpicker.main.map.newIndex[that.$element[0].selectedIndex];
  1413. if (typeof selectedIndex === 'number' && that.options.size !== false) {
  1414. offset = that.sizeInfo.liHeight * selectedIndex;
  1415. offset = offset - (that.sizeInfo.menuInnerHeight / 2) + (that.sizeInfo.liHeight / 2);
  1416. }
  1417. }
  1418. that.createView(false, offset);
  1419. },
  1420. setWidth: function () {
  1421. var that = this;
  1422. if (this.options.width === 'auto') {
  1423. requestAnimationFrame(function() {
  1424. that.$menu.css('min-width', '0');
  1425. that.liHeight();
  1426. that.setMenuSize();
  1427. // Get correct width if element is hidden
  1428. var $selectClone = that.$newElement.clone().appendTo('body'),
  1429. btnWidth = $selectClone.css('width', 'auto').children('button').outerWidth();
  1430. $selectClone.remove();
  1431. // Set width to whatever's larger, button title or longest option
  1432. that.sizeInfo.selectWidth = Math.max(that.sizeInfo.totalMenuWidth, btnWidth);
  1433. that.$newElement.css('width', that.sizeInfo.selectWidth + 'px');
  1434. });
  1435. } else if (this.options.width === 'fit') {
  1436. // Remove inline min-width so width can be changed from 'auto'
  1437. this.$menu.css('min-width', '');
  1438. this.$newElement.css('width', '').addClass('fit-width');
  1439. } else if (this.options.width) {
  1440. // Remove inline min-width so width can be changed from 'auto'
  1441. this.$menu.css('min-width', '');
  1442. this.$newElement.css('width', this.options.width);
  1443. } else {
  1444. // Remove inline min-width/width so width can be changed
  1445. this.$menu.css('min-width', '');
  1446. this.$newElement.css('width', '');
  1447. }
  1448. // Remove fit-width class if width is changed programmatically
  1449. if (this.$newElement.hasClass('fit-width') && this.options.width !== 'fit') {
  1450. this.$newElement.removeClass('fit-width');
  1451. }
  1452. },
  1453. selectPosition: function () {
  1454. this.$bsContainer = $('<div class="bs-container" />');
  1455. var that = this,
  1456. $container = $(this.options.container),
  1457. pos,
  1458. containerPos,
  1459. actualHeight,
  1460. getPlacement = function ($element) {
  1461. var containerPosition = {},
  1462. // fall back to dropdown's default display setting if display is not manually set
  1463. display = that.options.display || $.fn.dropdown.Constructor.Default.display;
  1464. that.$bsContainer.addClass($element.attr('class').replace(/form-control|fit-width/gi, '')).toggleClass(classNames.DROPUP, $element.hasClass(classNames.DROPUP));
  1465. pos = $element.offset();
  1466. if (!$container.is('body')) {
  1467. containerPos = $container.offset();
  1468. containerPos.top += parseInt($container.css('borderTopWidth')) - $container.scrollTop();
  1469. containerPos.left += parseInt($container.css('borderLeftWidth')) - $container.scrollLeft();
  1470. } else {
  1471. containerPos = { top: 0, left: 0 };
  1472. }
  1473. actualHeight = $element.hasClass(classNames.DROPUP) ? 0 : $element[0].offsetHeight;
  1474. // Bootstrap 4+ uses Popper for menu positioning
  1475. if (version.major < 4 || display === 'static') {
  1476. containerPosition['top'] = pos.top - containerPos.top + actualHeight;
  1477. containerPosition['left'] = pos.left - containerPos.left;
  1478. }
  1479. containerPosition['width'] = $element[0].offsetWidth;
  1480. that.$bsContainer.css(containerPosition);
  1481. };
  1482. this.$button.on('click.bs.dropdown.data-api', function () {
  1483. if (that.isDisabled()) {
  1484. return;
  1485. }
  1486. getPlacement(that.$newElement);
  1487. that.$bsContainer
  1488. .appendTo(that.options.container)
  1489. .toggleClass(classNames.SHOW, !that.$button.hasClass(classNames.SHOW))
  1490. .append(that.$menu);
  1491. });
  1492. $(window).on('resize scroll', function () {
  1493. getPlacement(that.$newElement);
  1494. });
  1495. this.$element.on('hide.bs.select', function () {
  1496. that.$menu.data('height', that.$menu.height());
  1497. that.$bsContainer.detach();
  1498. });
  1499. },
  1500. setOptionStatus: function () {
  1501. var that = this,
  1502. $selectOptions = this.$element.find('option');
  1503. that.noScroll = false;
  1504. if (that.selectpicker.view.visibleElements && that.selectpicker.view.visibleElements.length) {
  1505. for (var i = 0; i < that.selectpicker.view.visibleElements.length; i++) {
  1506. var index = that.selectpicker.current.map.originalIndex[i + that.selectpicker.view.position0], // faster than $(li).data('originalIndex')
  1507. option = $selectOptions[index];
  1508. if (option) {
  1509. var liIndex = this.selectpicker.main.map.newIndex[index],
  1510. li = this.selectpicker.main.elements[liIndex];
  1511. that.setDisabled(
  1512. index,
  1513. option.disabled || option.parentNode.tagName === 'OPTGROUP' && option.parentNode.disabled,
  1514. liIndex,
  1515. li
  1516. );
  1517. that.setSelected(
  1518. index,
  1519. option.selected,
  1520. liIndex,
  1521. li
  1522. );
  1523. }
  1524. }
  1525. }
  1526. },
  1527. /**
  1528. * @param {number} index - the index of the option that is being changed
  1529. * @param {boolean} selected - true if the option is being selected, false if being deselected
  1530. */
  1531. setSelected: function (index, selected, liIndex, li) {
  1532. var activeIndexIsSet = this.activeIndex !== undefined,
  1533. thisIsActive = this.activeIndex === index,
  1534. prevActiveIndex,
  1535. prevActive,
  1536. a,
  1537. // if current option is already active
  1538. // OR
  1539. // if the current option is being selected, it's NOT multiple, and
  1540. // activeIndex is undefined:
  1541. // - when the menu is first being opened, OR
  1542. // - after a search has been performed, OR
  1543. // - when retainActive is false when selecting a new option (i.e. index of the newly selected option is not the same as the current activeIndex)
  1544. keepActive = thisIsActive || selected && !this.multiple && !activeIndexIsSet;
  1545. if (!liIndex) liIndex = this.selectpicker.main.map.newIndex[index];
  1546. if (!li) li = this.selectpicker.main.elements[liIndex];
  1547. a = li.firstChild;
  1548. if (selected) {
  1549. this.selectedIndex = index;
  1550. }
  1551. li.classList.toggle('selected', selected);
  1552. li.classList.toggle('active', keepActive);
  1553. if (keepActive) {
  1554. this.selectpicker.view.currentActive = li;
  1555. this.activeIndex = index;
  1556. }
  1557. if (a) {
  1558. a.classList.toggle('selected', selected);
  1559. a.classList.toggle('active', keepActive);
  1560. a.setAttribute('aria-selected', selected);
  1561. }
  1562. if (!keepActive) {
  1563. if (!activeIndexIsSet && selected && this.prevActiveIndex !== undefined) {
  1564. prevActiveIndex = this.selectpicker.main.map.newIndex[this.prevActiveIndex];
  1565. prevActive = this.selectpicker.main.elements[prevActiveIndex];
  1566. prevActive.classList.toggle('selected', selected);
  1567. prevActive.classList.remove('active');
  1568. if (prevActive.firstChild) {
  1569. prevActive.firstChild.classList.toggle('selected', selected);
  1570. prevActive.firstChild.classList.remove('active');
  1571. }
  1572. }
  1573. }
  1574. },
  1575. /**
  1576. * @param {number} index - the index of the option that is being disabled
  1577. * @param {boolean} disabled - true if the option is being disabled, false if being enabled
  1578. */
  1579. setDisabled: function (index, disabled, liIndex, li) {
  1580. var a;
  1581. if (!liIndex) liIndex = this.selectpicker.main.map.newIndex[index];
  1582. if (!li) li = this.selectpicker.main.elements[liIndex];
  1583. a = li.firstChild;
  1584. li.classList.toggle(classNames.DISABLED, disabled);
  1585. if (a) {
  1586. if (version.major === '4') a.classList.toggle(classNames.DISABLED, disabled);
  1587. a.setAttribute('aria-disabled', disabled);
  1588. if (disabled) {
  1589. a.setAttribute('tabindex', -1);
  1590. } else {
  1591. a.setAttribute('tabindex', 0);
  1592. }
  1593. }
  1594. },
  1595. isDisabled: function () {
  1596. return this.$element[0].disabled;
  1597. },
  1598. checkDisabled: function () {
  1599. var that = this;
  1600. if (this.isDisabled()) {
  1601. this.$newElement.addClass(classNames.DISABLED);
  1602. this.$button.addClass(classNames.DISABLED).attr('tabindex', -1).attr('aria-disabled', true);
  1603. } else {
  1604. if (this.$button.hasClass(classNames.DISABLED)) {
  1605. this.$newElement.removeClass(classNames.DISABLED);
  1606. this.$button.removeClass(classNames.DISABLED).attr('aria-disabled', false);
  1607. }
  1608. if (this.$button.attr('tabindex') == -1 && !this.$element.data('tabindex')) {
  1609. this.$button.removeAttr('tabindex');
  1610. }
  1611. }
  1612. this.$button.click(function () {
  1613. return !that.isDisabled();
  1614. });
  1615. },
  1616. togglePlaceholder: function () {
  1617. // much faster than calling $.val()
  1618. var element = this.$element[0],
  1619. selectedIndex = element.selectedIndex,
  1620. nothingSelected = selectedIndex === -1;
  1621. if (!nothingSelected && !element.options[selectedIndex].value) nothingSelected = true;
  1622. this.$button.toggleClass('bs-placeholder', nothingSelected);
  1623. },
  1624. tabIndex: function () {
  1625. if (this.$element.data('tabindex') !== this.$element.attr('tabindex') &&
  1626. (this.$element.attr('tabindex') !== -98 && this.$element.attr('tabindex') !== '-98')) {
  1627. this.$element.data('tabindex', this.$element.attr('tabindex'));
  1628. this.$button.attr('tabindex', this.$element.data('tabindex'));
  1629. }
  1630. this.$element.attr('tabindex', -98);
  1631. },
  1632. clickListener: function () {
  1633. var that = this,
  1634. $document = $(document);
  1635. $document.data('spaceSelect', false);
  1636. this.$button.on('keyup', function (e) {
  1637. if (/(32)/.test(e.keyCode.toString(10)) && $document.data('spaceSelect')) {
  1638. e.preventDefault();
  1639. $document.data('spaceSelect', false);
  1640. }
  1641. });
  1642. this.$newElement.on('show.bs.dropdown', function() {
  1643. if (version.major > 3 && !that.dropdown) {
  1644. that.dropdown = that.$button.data('bs.dropdown');
  1645. that.dropdown._menu = that.$menu[0];
  1646. }
  1647. });
  1648. this.$button.on('click.bs.dropdown.data-api', function () {
  1649. if (!that.$newElement.hasClass(classNames.SHOW)) {
  1650. that.setSize();
  1651. }
  1652. });
  1653. function setFocus () {
  1654. if (that.options.liveSearch) {
  1655. that.$searchbox.focus();
  1656. } else {
  1657. that.$menuInner.focus();
  1658. }
  1659. }
  1660. function checkPopperExists () {
  1661. if (that.dropdown && that.dropdown._popper && that.dropdown._popper.state.isCreated) {
  1662. setFocus();
  1663. } else {
  1664. requestAnimationFrame(checkPopperExists);
  1665. }
  1666. }
  1667. this.$element.on('shown.bs.select', function () {
  1668. if (that.$menuInner[0].scrollTop !== that.selectpicker.view.scrollTop) {
  1669. that.$menuInner[0].scrollTop = that.selectpicker.view.scrollTop;
  1670. }
  1671. if (version.major > 3) {
  1672. requestAnimationFrame(checkPopperExists);
  1673. } else {
  1674. setFocus();
  1675. }
  1676. });
  1677. this.$menuInner.on('click', 'li a', function (e, retainActive) {
  1678. var $this = $(this),
  1679. position0 = that.isVirtual() ? that.selectpicker.view.position0 : 0,
  1680. clickedIndex = that.selectpicker.current.map.originalIndex[$this.parent().index() + position0],
  1681. prevValue = getSelectValues(that.$element[0]),
  1682. prevIndex = that.$element.prop('selectedIndex'),
  1683. triggerChange = true;
  1684. // Don't close on multi choice menu
  1685. if (that.multiple && that.options.maxOptions !== 1) {
  1686. e.stopPropagation();
  1687. }
  1688. e.preventDefault();
  1689. //Don't run if we have been disabled
  1690. if (!that.isDisabled() && !$this.parent().hasClass(classNames.DISABLED)) {
  1691. var $options = that.$element.find('option'),
  1692. $option = $options.eq(clickedIndex),
  1693. state = $option.prop('selected'),
  1694. $optgroup = $option.parent('optgroup'),
  1695. $optgroupOptions = $optgroup.find('option'),
  1696. maxOptions = that.options.maxOptions,
  1697. maxOptionsGrp = $optgroup.data('maxOptions') || false;
  1698. if (clickedIndex === that.activeIndex) retainActive = true;
  1699. if (!retainActive) {
  1700. that.prevActiveIndex = that.activeIndex;
  1701. that.activeIndex = undefined;
  1702. }
  1703. if (!that.multiple) { // Deselect all others if not multi select box
  1704. $options.prop('selected', false);
  1705. $option.prop('selected', true);
  1706. that.setSelected(clickedIndex, true);
  1707. } else { // Toggle the one we have chosen if we are multi select.
  1708. $option.prop('selected', !state);
  1709. that.setSelected(clickedIndex, !state);
  1710. $this.blur();
  1711. if (maxOptions !== false || maxOptionsGrp !== false) {
  1712. var maxReached = maxOptions < $options.filter(':selected').length,
  1713. maxReachedGrp = maxOptionsGrp < $optgroup.find('option:selected').length;
  1714. if ((maxOptions && maxReached) || (maxOptionsGrp && maxReachedGrp)) {
  1715. if (maxOptions && maxOptions == 1) {
  1716. $options.prop('selected', false);
  1717. $option.prop('selected', true);
  1718. for (var i = 0; i < $options.length; i++) {
  1719. that.setSelected(i, false);
  1720. }
  1721. that.setSelected(clickedIndex, true);
  1722. } else if (maxOptionsGrp && maxOptionsGrp == 1) {
  1723. $optgroup.find('option:selected').prop('selected', false);
  1724. $option.prop('selected', true);
  1725. for (var i = 0; i < $optgroupOptions.length; i++) {
  1726. var option = $optgroupOptions[i];
  1727. that.setSelected($options.index(option), false);
  1728. }
  1729. that.setSelected(clickedIndex, true);
  1730. } else {
  1731. var maxOptionsText = typeof that.options.maxOptionsText === 'string' ? [that.options.maxOptionsText, that.options.maxOptionsText] : that.options.maxOptionsText,
  1732. maxOptionsArr = typeof maxOptionsText === 'function' ? maxOptionsText(maxOptions, maxOptionsGrp) : maxOptionsText,
  1733. maxTxt = maxOptionsArr[0].replace('{n}', maxOptions),
  1734. maxTxtGrp = maxOptionsArr[1].replace('{n}', maxOptionsGrp),
  1735. $notify = $('<div class="notify"></div>');
  1736. // If {var} is set in array, replace it
  1737. /** @deprecated */
  1738. if (maxOptionsArr[2]) {
  1739. maxTxt = maxTxt.replace('{var}', maxOptionsArr[2][maxOptions > 1 ? 0 : 1]);
  1740. maxTxtGrp = maxTxtGrp.replace('{var}', maxOptionsArr[2][maxOptionsGrp > 1 ? 0 : 1]);
  1741. }
  1742. $option.prop('selected', false);
  1743. that.$menu.append($notify);
  1744. if (maxOptions && maxReached) {
  1745. $notify.append($('<div>' + maxTxt + '</div>'));
  1746. triggerChange = false;
  1747. that.$element.trigger('maxReached.bs.select');
  1748. }
  1749. if (maxOptionsGrp && maxReachedGrp) {
  1750. $notify.append($('<div>' + maxTxtGrp + '</div>'));
  1751. triggerChange = false;
  1752. that.$element.trigger('maxReachedGrp.bs.select');
  1753. }
  1754. setTimeout(function () {
  1755. that.setSelected(clickedIndex, false);
  1756. }, 10);
  1757. $notify.delay(750).fadeOut(300, function () {
  1758. $(this).remove();
  1759. });
  1760. }
  1761. }
  1762. }
  1763. }
  1764. if (!that.multiple || (that.multiple && that.options.maxOptions === 1)) {
  1765. that.$button.focus();
  1766. } else if (that.options.liveSearch) {
  1767. that.$searchbox.focus();
  1768. }
  1769. // Trigger select 'change'
  1770. if (triggerChange) {
  1771. if ((prevValue != getSelectValues(that.$element[0]) && that.multiple) || (prevIndex != that.$element.prop('selectedIndex') && !that.multiple)) {
  1772. // $option.prop('selected') is current option state (selected/unselected). prevValue is the value of the select prior to being changed.
  1773. changed_arguments = [clickedIndex, $option.prop('selected'), prevValue];
  1774. that.$element
  1775. .triggerNative('change');
  1776. }
  1777. }
  1778. }
  1779. });
  1780. this.$menu.on('click', 'li.' + classNames.DISABLED + ' a, .' + classNames.POPOVERHEADER + ', .' + classNames.POPOVERHEADER + ' :not(.close)', function (e) {
  1781. if (e.currentTarget == this) {
  1782. e.preventDefault();
  1783. e.stopPropagation();
  1784. if (that.options.liveSearch && !$(e.target).hasClass('close')) {
  1785. that.$searchbox.focus();
  1786. } else {
  1787. that.$button.focus();
  1788. }
  1789. }
  1790. });
  1791. this.$menuInner.on('click', '.divider, .dropdown-header', function (e) {
  1792. e.preventDefault();
  1793. e.stopPropagation();
  1794. if (that.options.liveSearch) {
  1795. that.$searchbox.focus();
  1796. } else {
  1797. that.$button.focus();
  1798. }
  1799. });
  1800. this.$menu.on('click', '.' + classNames.POPOVERHEADER + ' .close', function () {
  1801. that.$button.click();
  1802. });
  1803. this.$searchbox.on('click', function (e) {
  1804. e.stopPropagation();
  1805. });
  1806. this.$menu.on('click', '.actions-btn', function (e) {
  1807. if (that.options.liveSearch) {
  1808. that.$searchbox.focus();
  1809. } else {
  1810. that.$button.focus();
  1811. }
  1812. e.preventDefault();
  1813. e.stopPropagation();
  1814. if ($(this).hasClass('bs-select-all')) {
  1815. that.selectAll();
  1816. } else {
  1817. that.deselectAll();
  1818. }
  1819. });
  1820. this.$element.on({
  1821. 'change': function () {
  1822. that.render();
  1823. that.$element.trigger('changed.bs.select', changed_arguments);
  1824. changed_arguments = null;
  1825. },
  1826. 'focus': function () {
  1827. that.$button.focus();
  1828. }
  1829. });
  1830. },
  1831. liveSearchListener: function () {
  1832. var that = this,
  1833. no_results = document.createElement('li');
  1834. this.$button.on('click.bs.dropdown.data-api', function () {
  1835. if (!!that.$searchbox.val()) {
  1836. that.$searchbox.val('');
  1837. }
  1838. });
  1839. this.$searchbox.on('click.bs.dropdown.data-api focus.bs.dropdown.data-api touchend.bs.dropdown.data-api', function (e) {
  1840. e.stopPropagation();
  1841. });
  1842. this.$searchbox.on('input propertychange', function () {
  1843. var searchValue = that.$searchbox.val();
  1844. that.selectpicker.search.map.newIndex = {};
  1845. that.selectpicker.search.map.originalIndex = {};
  1846. that.selectpicker.search.elements = [];
  1847. that.selectpicker.search.data = [];
  1848. if (searchValue) {
  1849. var i,
  1850. searchMatch = [],
  1851. q = searchValue.toUpperCase(),
  1852. cache = {},
  1853. cacheArr = [],
  1854. searchStyle = that._searchStyle(),
  1855. normalizeSearch = that.options.liveSearchNormalize;
  1856. that._$lisSelected = that.$menuInner.find('.selected');
  1857. for (var i = 0; i < that.selectpicker.main.data.length; i++) {
  1858. var li = that.selectpicker.main.data[i];
  1859. if (!cache[i]) {
  1860. cache[i] = stringSearch(li, q, searchStyle, normalizeSearch);
  1861. }
  1862. if (cache[i] && li.headerIndex !== undefined && cacheArr.indexOf(li.headerIndex) === -1) {
  1863. if (li.headerIndex > 0) {
  1864. cache[li.headerIndex - 1] = true;
  1865. cacheArr.push(li.headerIndex - 1);
  1866. }
  1867. cache[li.headerIndex] = true;
  1868. cacheArr.push(li.headerIndex);
  1869. cache[li.lastIndex + 1] = true;
  1870. }
  1871. if (cache[i] && li.type !== 'optgroup-label') cacheArr.push(i);
  1872. }
  1873. for (var i = 0, cacheLen = cacheArr.length; i < cacheLen; i++) {
  1874. var index = cacheArr[i],
  1875. prevIndex = cacheArr[i - 1],
  1876. li = that.selectpicker.main.data[index],
  1877. liPrev = that.selectpicker.main.data[prevIndex];
  1878. if ( li.type !== 'divider' || ( li.type === 'divider' && liPrev && liPrev.type !== 'divider' && cacheLen - 1 !== i ) ) {
  1879. that.selectpicker.search.data.push(li);
  1880. searchMatch.push(that.selectpicker.main.elements[index]);
  1881. if (li.hasOwnProperty('originalIndex')) {
  1882. that.selectpicker.search.map.newIndex[li.originalIndex] = searchMatch.length - 1;
  1883. that.selectpicker.search.map.originalIndex[searchMatch.length - 1] = li.originalIndex;
  1884. }
  1885. }
  1886. }
  1887. that.activeIndex = undefined;
  1888. that.noScroll = true;
  1889. that.$menuInner.scrollTop(0);
  1890. that.selectpicker.search.elements = searchMatch;
  1891. that.createView(true);
  1892. if (!searchMatch.length) {
  1893. no_results.className = 'no-results';
  1894. no_results.innerHTML = that.options.noneResultsText.replace('{0}', '"' + htmlEscape(searchValue) + '"');
  1895. that.$menuInner[0].firstChild.appendChild(no_results);
  1896. }
  1897. } else {
  1898. that.$menuInner.scrollTop(0);
  1899. that.createView(false);
  1900. }
  1901. });
  1902. },
  1903. _searchStyle: function () {
  1904. return this.options.liveSearchStyle || 'contains';
  1905. },
  1906. val: function (value) {
  1907. if (typeof value !== 'undefined') {
  1908. this.$element
  1909. .val(value)
  1910. .triggerNative('change');
  1911. return this.$element;
  1912. } else {
  1913. return this.$element.val();
  1914. }
  1915. },
  1916. changeAll: function (status) {
  1917. if (!this.multiple) return;
  1918. if (typeof status === 'undefined') status = true;
  1919. var $selectOptions = this.$element.find('option'),
  1920. previousSelected = 0,
  1921. currentSelected = 0,
  1922. prevValue = getSelectValues(this.$element[0]);
  1923. this.$element.addClass('bs-select-hidden');
  1924. for (var i = 0; i < this.selectpicker.current.elements.length; i++) {
  1925. var liData = this.selectpicker.current.data[i],
  1926. index = this.selectpicker.current.map.originalIndex[i], // faster than $(li).data('originalIndex')
  1927. option = $selectOptions[index];
  1928. if (option && !option.disabled && liData.type !== 'divider') {
  1929. if (option.selected) previousSelected++;
  1930. option.selected = status;
  1931. if (option.selected) currentSelected++;
  1932. }
  1933. }
  1934. this.$element.removeClass('bs-select-hidden');
  1935. if (previousSelected === currentSelected) return;
  1936. this.setOptionStatus();
  1937. this.togglePlaceholder();
  1938. changed_arguments = [null, null, prevValue];
  1939. this.$element
  1940. .triggerNative('change');
  1941. },
  1942. selectAll: function () {
  1943. return this.changeAll(true);
  1944. },
  1945. deselectAll: function () {
  1946. return this.changeAll(false);
  1947. },
  1948. toggle: function (e) {
  1949. e = e || window.event;
  1950. if (e) e.stopPropagation();
  1951. this.$button.trigger('click.bs.dropdown.data-api');
  1952. },
  1953. keydown: function (e) {
  1954. var $this = $(this),
  1955. isToggle = $this.hasClass('dropdown-toggle'),
  1956. $parent = isToggle ? $this.closest('.dropdown') : $this.closest(Selector.MENU),
  1957. that = $parent.data('this'),
  1958. $items = that.findLis(),
  1959. index,
  1960. isActive,
  1961. liActive,
  1962. activeLi,
  1963. offset,
  1964. updateScroll = false,
  1965. downOnTab = e.which === keyCodes.TAB && !isToggle && !that.options.selectOnTab,
  1966. isArrowKey = REGEXP_ARROW.test(e.which) || downOnTab,
  1967. scrollTop = that.$menuInner[0].scrollTop,
  1968. isVirtual = that.isVirtual(),
  1969. position0 = isVirtual === true ? that.selectpicker.view.position0 : 0;
  1970. isActive = that.$newElement.hasClass(classNames.SHOW);
  1971. if (
  1972. !isActive &&
  1973. (
  1974. isArrowKey ||
  1975. e.which >= 48 && e.which <= 57 ||
  1976. e.which >= 96 && e.which <= 105 ||
  1977. e.which >= 65 && e.which <= 90
  1978. )
  1979. ) {
  1980. that.$button.trigger('click.bs.dropdown.data-api');
  1981. }
  1982. if (e.which === keyCodes.ESCAPE && isActive) {
  1983. e.preventDefault();
  1984. that.$button.trigger('click.bs.dropdown.data-api').focus();
  1985. }
  1986. if (isArrowKey) { // if up or down
  1987. if (!$items.length) return;
  1988. // $items.index/.filter is too slow with a large list and no virtual scroll
  1989. index = isVirtual === true ? $items.index($items.filter('.active')) : that.selectpicker.current.map.newIndex[that.activeIndex];
  1990. if (index === undefined) index = -1;
  1991. if (index !== -1) {
  1992. liActive = that.selectpicker.current.elements[index + position0];
  1993. liActive.classList.remove('active');
  1994. if (liActive.firstChild) liActive.firstChild.classList.remove('active');
  1995. }
  1996. if (e.which === keyCodes.ARROW_UP) { // up
  1997. if (index !== -1) index--;
  1998. if (index + position0 < 0) index += $items.length;
  1999. if (!that.selectpicker.view.canHighlight[index + position0]) {
  2000. index = that.selectpicker.view.canHighlight.slice(0, index + position0).lastIndexOf(true) - position0;
  2001. if (index === -1) index = $items.length - 1;
  2002. }
  2003. } else if (e.which === keyCodes.ARROW_DOWN || downOnTab) { // down
  2004. index++;
  2005. if (index + position0 >= that.selectpicker.view.canHighlight.length) index = 0;
  2006. if (!that.selectpicker.view.canHighlight[index + position0]) {
  2007. index = index + 1 + that.selectpicker.view.canHighlight.slice(index + position0 + 1).indexOf(true);
  2008. }
  2009. }
  2010. e.preventDefault();
  2011. var liActiveIndex = position0 + index;
  2012. if (e.which === keyCodes.ARROW_UP) { // up
  2013. // scroll to bottom and highlight last option
  2014. if (position0 === 0 && index === $items.length - 1) {
  2015. that.$menuInner[0].scrollTop = that.$menuInner[0].scrollHeight;
  2016. liActiveIndex = that.selectpicker.current.elements.length - 1;
  2017. } else {
  2018. activeLi = that.selectpicker.current.data[liActiveIndex];
  2019. offset = activeLi.position - activeLi.height;
  2020. updateScroll = offset < scrollTop;
  2021. }
  2022. } else if (e.which === keyCodes.ARROW_DOWN || downOnTab) { // down
  2023. // scroll to top and highlight first option
  2024. if (index === 0) {
  2025. that.$menuInner[0].scrollTop = 0;
  2026. liActiveIndex = 0;
  2027. } else {
  2028. activeLi = that.selectpicker.current.data[liActiveIndex];
  2029. offset = activeLi.position - that.sizeInfo.menuInnerHeight;
  2030. updateScroll = offset > scrollTop;
  2031. }
  2032. }
  2033. liActive = that.selectpicker.current.elements[liActiveIndex];
  2034. if (liActive) {
  2035. liActive.classList.add('active');
  2036. if (liActive.firstChild) liActive.firstChild.classList.add('active');
  2037. }
  2038. that.activeIndex = that.selectpicker.current.map.originalIndex[liActiveIndex];
  2039. that.selectpicker.view.currentActive = liActive;
  2040. if (updateScroll) that.$menuInner[0].scrollTop = offset;
  2041. if (that.options.liveSearch) {
  2042. that.$searchbox.focus();
  2043. } else {
  2044. $this.focus();
  2045. }
  2046. } else if (
  2047. !$this.is('input') &&
  2048. !REGEXP_TAB_OR_ESCAPE.test(e.which) ||
  2049. (e.which === keyCodes.SPACE && that.selectpicker.keydown.keyHistory)
  2050. ) {
  2051. var searchMatch,
  2052. matches = [],
  2053. keyHistory;
  2054. e.preventDefault();
  2055. that.selectpicker.keydown.keyHistory += keyCodeMap[e.which];
  2056. if (that.selectpicker.keydown.resetKeyHistory.cancel) clearTimeout(that.selectpicker.keydown.resetKeyHistory.cancel);
  2057. that.selectpicker.keydown.resetKeyHistory.cancel = that.selectpicker.keydown.resetKeyHistory.start();
  2058. keyHistory = that.selectpicker.keydown.keyHistory;
  2059. // if all letters are the same, set keyHistory to just the first character when searching
  2060. if (/^(.)\1+$/.test(keyHistory)) {
  2061. keyHistory = keyHistory.charAt(0);
  2062. }
  2063. // find matches
  2064. for (var i = 0; i < that.selectpicker.current.data.length; i++) {
  2065. var li = that.selectpicker.current.data[i],
  2066. hasMatch;
  2067. hasMatch = stringSearch(li, keyHistory, 'startsWith', true);
  2068. if (hasMatch && that.selectpicker.view.canHighlight[i]) {
  2069. li.index = i;
  2070. matches.push(li.originalIndex);
  2071. }
  2072. }
  2073. if (matches.length) {
  2074. var matchIndex = 0;
  2075. $items.removeClass('active').find('a').removeClass('active');
  2076. // either only one key has been pressed or they are all the same key
  2077. if (keyHistory.length === 1) {
  2078. matchIndex = matches.indexOf(that.activeIndex);
  2079. if (matchIndex === -1 || matchIndex === matches.length - 1) {
  2080. matchIndex = 0;
  2081. } else {
  2082. matchIndex++;
  2083. }
  2084. }
  2085. searchMatch = that.selectpicker.current.map.newIndex[matches[matchIndex]];
  2086. activeLi = that.selectpicker.current.data[searchMatch];
  2087. if (scrollTop - activeLi.position > 0) {
  2088. offset = activeLi.position - activeLi.height;
  2089. updateScroll = true;
  2090. } else {
  2091. offset = activeLi.position - that.sizeInfo.menuInnerHeight;
  2092. // if the option is already visible at the current scroll position, just keep it the same
  2093. updateScroll = activeLi.position > scrollTop + that.sizeInfo.menuInnerHeight;
  2094. }
  2095. liActive = that.selectpicker.current.elements[searchMatch];
  2096. liActive.classList.add('active');
  2097. if (liActive.firstChild) liActive.firstChild.classList.add('active');
  2098. that.activeIndex = matches[matchIndex];
  2099. liActive.firstChild.focus();
  2100. if (updateScroll) that.$menuInner[0].scrollTop = offset;
  2101. $this.focus();
  2102. }
  2103. }
  2104. // Select focused option if "Enter", "Spacebar" or "Tab" (when selectOnTab is true) are pressed inside the menu.
  2105. if (
  2106. isActive &&
  2107. (
  2108. (e.which === keyCodes.SPACE && !that.selectpicker.keydown.keyHistory) ||
  2109. e.which === keyCodes.ENTER ||
  2110. (e.which === keyCodes.TAB && that.options.selectOnTab)
  2111. )
  2112. ) {
  2113. if (e.which !== keyCodes.SPACE) e.preventDefault();
  2114. if (!that.options.liveSearch || e.which !== keyCodes.SPACE) {
  2115. that.$menuInner.find('.active a').trigger('click', true); // retain active class
  2116. $this.focus();
  2117. if (!that.options.liveSearch) {
  2118. // Prevent screen from scrolling if the user hits the spacebar
  2119. e.preventDefault();
  2120. // Fixes spacebar selection of dropdown items in FF & IE
  2121. $(document).data('spaceSelect', true);
  2122. }
  2123. }
  2124. }
  2125. },
  2126. mobile: function () {
  2127. this.$element.addClass('mobile-device');
  2128. },
  2129. refresh: function () {
  2130. // update options if data attributes have been changed
  2131. var config = $.extend({}, this.options, this.$element.data());
  2132. this.options = config;
  2133. this.selectpicker.main.map.newIndex = {};
  2134. this.selectpicker.main.map.originalIndex = {};
  2135. this.createLi();
  2136. this.checkDisabled();
  2137. this.render();
  2138. this.setStyle();
  2139. this.setWidth();
  2140. this.setSize(true);
  2141. this.$element.trigger('refreshed.bs.select');
  2142. },
  2143. hide: function () {
  2144. this.$newElement.hide();
  2145. },
  2146. show: function () {
  2147. this.$newElement.show();
  2148. },
  2149. remove: function () {
  2150. this.$newElement.remove();
  2151. this.$element.remove();
  2152. },
  2153. destroy: function () {
  2154. this.$newElement.before(this.$element).remove();
  2155. if (this.$bsContainer) {
  2156. this.$bsContainer.remove();
  2157. } else {
  2158. this.$menu.remove();
  2159. }
  2160. this.$element
  2161. .off('.bs.select')
  2162. .removeData('selectpicker')
  2163. .removeClass('bs-select-hidden selectpicker');
  2164. }
  2165. };
  2166. // SELECTPICKER PLUGIN DEFINITION
  2167. // ==============================
  2168. function Plugin(option) {
  2169. // get the args of the outer function..
  2170. var args = arguments;
  2171. // The arguments of the function are explicitly re-defined from the argument list, because the shift causes them
  2172. // to get lost/corrupted in android 2.3 and IE9 #715 #775
  2173. var _option = option;
  2174. [].shift.apply(args);
  2175. // if the version was not set successfully
  2176. if (!version.success) {
  2177. // try to retreive it again
  2178. try {
  2179. version.full = ($.fn.dropdown.Constructor.VERSION || '').split(' ')[0].split('.');
  2180. }
  2181. // fall back to use BootstrapVersion
  2182. catch(err) {
  2183. version.full = Selectpicker.BootstrapVersion.split(' ')[0].split('.');
  2184. }
  2185. version.major = version.full[0];
  2186. version.success = true;
  2187. if (version.major === '4') {
  2188. classNames.DIVIDER = 'dropdown-divider';
  2189. classNames.SHOW = 'show';
  2190. classNames.BUTTONCLASS = 'btn-light';
  2191. Selectpicker.DEFAULTS.style = classNames.BUTTONCLASS = 'btn-light';
  2192. classNames.POPOVERHEADER = 'popover-header';
  2193. }
  2194. }
  2195. var value;
  2196. var chain = this.each(function () {
  2197. var $this = $(this);
  2198. if ($this.is('select')) {
  2199. var data = $this.data('selectpicker'),
  2200. options = typeof _option == 'object' && _option;
  2201. if (!data) {
  2202. var config = $.extend({}, Selectpicker.DEFAULTS, $.fn.selectpicker.defaults || {}, $this.data(), options);
  2203. config.template = $.extend({}, Selectpicker.DEFAULTS.template, ($.fn.selectpicker.defaults ? $.fn.selectpicker.defaults.template : {}), $this.data().template, options.template);
  2204. $this.data('selectpicker', (data = new Selectpicker(this, config)));
  2205. } else if (options) {
  2206. for (var i in options) {
  2207. if (options.hasOwnProperty(i)) {
  2208. data.options[i] = options[i];
  2209. }
  2210. }
  2211. }
  2212. if (typeof _option == 'string') {
  2213. if (data[_option] instanceof Function) {
  2214. value = data[_option].apply(data, args);
  2215. } else {
  2216. value = data.options[_option];
  2217. }
  2218. }
  2219. }
  2220. });
  2221. if (typeof value !== 'undefined') {
  2222. //noinspection JSUnusedAssignment
  2223. return value;
  2224. } else {
  2225. return chain;
  2226. }
  2227. }
  2228. var old = $.fn.selectpicker;
  2229. $.fn.selectpicker = Plugin;
  2230. $.fn.selectpicker.Constructor = Selectpicker;
  2231. // SELECTPICKER NO CONFLICT
  2232. // ========================
  2233. $.fn.selectpicker.noConflict = function () {
  2234. $.fn.selectpicker = old;
  2235. return this;
  2236. };
  2237. $(document)
  2238. .off('keydown.bs.dropdown.data-api')
  2239. .on('keydown.bs.select', '.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bs-searchbox input', Selectpicker.prototype.keydown)
  2240. .on('focusin.modal', '.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bs-searchbox input', function (e) {
  2241. e.stopPropagation();
  2242. });
  2243. // SELECTPICKER DATA-API
  2244. // =====================
  2245. $(window).on('load.bs.select.data-api', function () {
  2246. $('.selectpicker').each(function () {
  2247. var $selectpicker = $(this);
  2248. Plugin.call($selectpicker, $selectpicker.data());
  2249. })
  2250. });
  2251. })(jQuery);