You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

semanticDropdown.js 155KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305
  1. /* This is a patched version of semantic.dropdown which includes a11y changes, see
  2. https://github.com/go-gitea/gitea/pull/8638#issuecomment-549175290 */
  3. /*!
  4. * # Fomantic-UI - Dropdown
  5. * http://github.com/fomantic/Fomantic-UI/
  6. *
  7. *
  8. * Released under the MIT license
  9. * http://opensource.org/licenses/MIT
  10. *
  11. */
  12. /*
  13. * Copyright 2019 The Gitea Authors
  14. * Released under the MIT license
  15. * http://opensource.org/licenses/MIT
  16. * This version has been modified by Gitea to improve accessibility.
  17. */
  18. ;(function ($, window, document, undefined) {
  19. 'use strict';
  20. $.isFunction = $.isFunction || function(obj) {
  21. return typeof obj === "function" && typeof obj.nodeType !== "number";
  22. };
  23. window = (typeof window != 'undefined' && window.Math == Math)
  24. ? window
  25. : (typeof self != 'undefined' && self.Math == Math)
  26. ? self
  27. : Function('return this')()
  28. ;
  29. $.fn.dropdown = function(parameters) {
  30. var
  31. $allModules = $(this),
  32. $document = $(document),
  33. moduleSelector = $allModules.selector || '',
  34. hasTouch = ('ontouchstart' in document.documentElement),
  35. time = new Date().getTime(),
  36. performance = [],
  37. query = arguments[0],
  38. methodInvoked = (typeof query == 'string'),
  39. queryArguments = [].slice.call(arguments, 1),
  40. lastAriaID = 1,
  41. returnedValue
  42. ;
  43. $allModules
  44. .each(function(elementIndex) {
  45. var
  46. settings = ( $.isPlainObject(parameters) )
  47. ? $.extend(true, {}, $.fn.dropdown.settings, parameters)
  48. : $.extend({}, $.fn.dropdown.settings),
  49. className = settings.className,
  50. message = settings.message,
  51. fields = settings.fields,
  52. keys = settings.keys,
  53. metadata = settings.metadata,
  54. namespace = settings.namespace,
  55. regExp = settings.regExp,
  56. selector = settings.selector,
  57. error = settings.error,
  58. templates = settings.templates,
  59. eventNamespace = '.' + namespace,
  60. moduleNamespace = 'module-' + namespace,
  61. $module = $(this),
  62. $context = $(settings.context),
  63. $text = $module.find(selector.text),
  64. $search = $module.find(selector.search),
  65. $sizer = $module.find(selector.sizer),
  66. $input = $module.find(selector.input),
  67. $icon = $module.find(selector.icon),
  68. $clear = $module.find(selector.clearIcon),
  69. $combo = ($module.prev().find(selector.text).length > 0)
  70. ? $module.prev().find(selector.text)
  71. : $module.prev(),
  72. $menu = $module.children(selector.menu),
  73. $item = $menu.find(selector.item),
  74. $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $(),
  75. activated = false,
  76. itemActivated = false,
  77. internalChange = false,
  78. iconClicked = false,
  79. element = this,
  80. instance = $module.data(moduleNamespace),
  81. selectActionActive,
  82. initialLoad,
  83. pageLostFocus,
  84. willRefocus,
  85. elementNamespace,
  86. id,
  87. selectObserver,
  88. menuObserver,
  89. module
  90. ;
  91. module = {
  92. initialize: function() {
  93. module.debug('Initializing dropdown', settings);
  94. if( module.is.alreadySetup() ) {
  95. module.setup.reference();
  96. }
  97. else {
  98. if (settings.ignoreDiacritics && !String.prototype.normalize) {
  99. settings.ignoreDiacritics = false;
  100. module.error(error.noNormalize, element);
  101. }
  102. module.setup.layout();
  103. if(settings.values) {
  104. module.change.values(settings.values);
  105. }
  106. module.refreshData();
  107. module.save.defaults();
  108. module.restore.selected();
  109. module.create.id();
  110. module.bind.events();
  111. module.observeChanges();
  112. module.instantiate();
  113. module.aria.setup();
  114. }
  115. },
  116. instantiate: function() {
  117. module.verbose('Storing instance of dropdown', module);
  118. instance = module;
  119. $module
  120. .data(moduleNamespace, module)
  121. ;
  122. },
  123. destroy: function() {
  124. module.verbose('Destroying previous dropdown', $module);
  125. module.remove.tabbable();
  126. module.remove.active();
  127. $menu.transition('stop all');
  128. $menu.removeClass(className.visible).addClass(className.hidden);
  129. $module
  130. .off(eventNamespace)
  131. .removeData(moduleNamespace)
  132. ;
  133. $menu
  134. .off(eventNamespace)
  135. ;
  136. $document
  137. .off(elementNamespace)
  138. ;
  139. module.disconnect.menuObserver();
  140. module.disconnect.selectObserver();
  141. },
  142. observeChanges: function() {
  143. if('MutationObserver' in window) {
  144. selectObserver = new MutationObserver(module.event.select.mutation);
  145. menuObserver = new MutationObserver(module.event.menu.mutation);
  146. module.debug('Setting up mutation observer', selectObserver, menuObserver);
  147. module.observe.select();
  148. module.observe.menu();
  149. }
  150. },
  151. disconnect: {
  152. menuObserver: function() {
  153. if(menuObserver) {
  154. menuObserver.disconnect();
  155. }
  156. },
  157. selectObserver: function() {
  158. if(selectObserver) {
  159. selectObserver.disconnect();
  160. }
  161. }
  162. },
  163. observe: {
  164. select: function() {
  165. if(module.has.input() && selectObserver) {
  166. selectObserver.observe($module[0], {
  167. childList : true,
  168. subtree : true
  169. });
  170. }
  171. },
  172. menu: function() {
  173. if(module.has.menu() && menuObserver) {
  174. menuObserver.observe($menu[0], {
  175. childList : true,
  176. subtree : true
  177. });
  178. }
  179. }
  180. },
  181. create: {
  182. id: function() {
  183. id = (Math.random().toString(16) + '000000000').substr(2, 8);
  184. elementNamespace = '.' + id;
  185. module.verbose('Creating unique id for element', id);
  186. },
  187. userChoice: function(values) {
  188. var
  189. $userChoices,
  190. $userChoice,
  191. isUserValue,
  192. html
  193. ;
  194. values = values || module.get.userValues();
  195. if(!values) {
  196. return false;
  197. }
  198. values = Array.isArray(values)
  199. ? values
  200. : [values]
  201. ;
  202. $.each(values, function(index, value) {
  203. if(module.get.item(value) === false) {
  204. html = settings.templates.addition( module.add.variables(message.addResult, value) );
  205. $userChoice = $('<div />')
  206. .html(html)
  207. .attr('data-' + metadata.value, value)
  208. .attr('data-' + metadata.text, value)
  209. .addClass(className.addition)
  210. .addClass(className.item)
  211. ;
  212. if(settings.hideAdditions) {
  213. $userChoice.addClass(className.hidden);
  214. }
  215. $userChoices = ($userChoices === undefined)
  216. ? $userChoice
  217. : $userChoices.add($userChoice)
  218. ;
  219. module.verbose('Creating user choices for value', value, $userChoice);
  220. }
  221. });
  222. return $userChoices;
  223. },
  224. userLabels: function(value) {
  225. var
  226. userValues = module.get.userValues()
  227. ;
  228. if(userValues) {
  229. module.debug('Adding user labels', userValues);
  230. $.each(userValues, function(index, value) {
  231. module.verbose('Adding custom user value');
  232. module.add.label(value, value);
  233. });
  234. }
  235. },
  236. menu: function() {
  237. $menu = $('<div />')
  238. .addClass(className.menu)
  239. .appendTo($module)
  240. ;
  241. },
  242. sizer: function() {
  243. $sizer = $('<span />')
  244. .addClass(className.sizer)
  245. .insertAfter($search)
  246. ;
  247. }
  248. },
  249. search: function(query) {
  250. query = (query !== undefined)
  251. ? query
  252. : module.get.query()
  253. ;
  254. module.verbose('Searching for query', query);
  255. if(module.has.minCharacters(query)) {
  256. module.filter(query);
  257. }
  258. else {
  259. module.hide(null,true);
  260. }
  261. },
  262. select: {
  263. firstUnfiltered: function() {
  264. module.verbose('Selecting first non-filtered element');
  265. module.remove.selectedItem();
  266. $item
  267. .not(selector.unselectable)
  268. .not(selector.addition + selector.hidden)
  269. .eq(0)
  270. .addClass(className.selected)
  271. ;
  272. },
  273. nextAvailable: function($selected) {
  274. $selected = $selected.eq(0);
  275. var
  276. $nextAvailable = $selected.nextAll(selector.item).not(selector.unselectable).eq(0),
  277. $prevAvailable = $selected.prevAll(selector.item).not(selector.unselectable).eq(0),
  278. hasNext = ($nextAvailable.length > 0)
  279. ;
  280. if(hasNext) {
  281. module.verbose('Moving selection to', $nextAvailable);
  282. $nextAvailable.addClass(className.selected);
  283. }
  284. else {
  285. module.verbose('Moving selection to', $prevAvailable);
  286. $prevAvailable.addClass(className.selected);
  287. }
  288. }
  289. },
  290. aria: {
  291. setup: function() {
  292. var role = module.aria.guessRole();
  293. if( role !== 'menu' ) {
  294. return;
  295. }
  296. $module.attr('aria-busy', 'true');
  297. $module.attr('role', 'menu');
  298. $module.attr('aria-haspopup', 'menu');
  299. $module.attr('aria-expanded', 'false');
  300. $menu.find('.divider').attr('role', 'separator');
  301. $item.attr('role', 'menuitem');
  302. $item.each(function (index, item) {
  303. if( !item.id ) {
  304. item.id = module.aria.nextID('menuitem');
  305. }
  306. });
  307. $text = $module
  308. .find('> .text')
  309. .eq(0)
  310. ;
  311. if( $module.data('content') ) {
  312. $text.attr('aria-hidden');
  313. $module.attr('aria-label', $module.data('content'));
  314. }
  315. else {
  316. $text.attr('id', module.aria.nextID('menutext'));
  317. $module.attr('aria-labelledby', $text.attr('id'));
  318. }
  319. $module.attr('aria-busy', 'false');
  320. },
  321. nextID: function(prefix) {
  322. var nextID;
  323. do {
  324. nextID = prefix + '_' + lastAriaID++;
  325. } while( document.getElementById(nextID) );
  326. return nextID;
  327. },
  328. setExpanded: function(expanded) {
  329. if( $module.attr('aria-haspopup') ) {
  330. $module.attr('aria-expanded', expanded);
  331. }
  332. },
  333. refreshDescendant: function() {
  334. if( $module.attr('aria-haspopup') !== 'menu' ) {
  335. return;
  336. }
  337. var
  338. $currentlySelected = $item.not(selector.unselectable).filter('.' + className.selected).eq(0),
  339. $activeItem = $menu.children('.' + className.active).eq(0),
  340. $selectedItem = ($currentlySelected.length > 0)
  341. ? $currentlySelected
  342. : $activeItem
  343. ;
  344. if( $selectedItem ) {
  345. $module.attr('aria-activedescendant', $selectedItem.attr('id'));
  346. }
  347. else {
  348. module.aria.removeDescendant();
  349. }
  350. },
  351. removeDescendant: function() {
  352. if( $module.attr('aria-haspopup') == 'menu' ) {
  353. $module.removeAttr('aria-activedescendant');
  354. }
  355. },
  356. guessRole: function() {
  357. var
  358. isIcon = $module.hasClass('icon'),
  359. hasSearch = module.has.search(),
  360. hasInput = ($input.length > 0),
  361. isMultiple = module.is.multiple()
  362. ;
  363. if ( !isIcon && !hasSearch && !hasInput && !isMultiple ) {
  364. return 'menu';
  365. }
  366. return 'unknown';
  367. }
  368. },
  369. setup: {
  370. api: function() {
  371. var
  372. apiSettings = {
  373. debug : settings.debug,
  374. urlData : {
  375. value : module.get.value(),
  376. query : module.get.query()
  377. },
  378. on : false
  379. }
  380. ;
  381. module.verbose('First request, initializing API');
  382. $module
  383. .api(apiSettings)
  384. ;
  385. },
  386. layout: function() {
  387. if( $module.is('select') ) {
  388. module.setup.select();
  389. module.setup.returnedObject();
  390. }
  391. if( !module.has.menu() ) {
  392. module.create.menu();
  393. }
  394. if ( module.is.selection() && module.is.clearable() && !module.has.clearItem() ) {
  395. module.verbose('Adding clear icon');
  396. $clear = $('<i />')
  397. .addClass('remove icon')
  398. .insertBefore($text)
  399. ;
  400. }
  401. if( module.is.search() && !module.has.search() ) {
  402. module.verbose('Adding search input');
  403. $search = $('<input />')
  404. .addClass(className.search)
  405. .prop('autocomplete', 'off')
  406. .insertBefore($text)
  407. ;
  408. }
  409. if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) {
  410. module.create.sizer();
  411. }
  412. if(settings.allowTab) {
  413. module.set.tabbable();
  414. }
  415. $item.attr('tabindex', '-1');
  416. },
  417. select: function() {
  418. var
  419. selectValues = module.get.selectValues()
  420. ;
  421. module.debug('Dropdown initialized on a select', selectValues);
  422. if( $module.is('select') ) {
  423. $input = $module;
  424. }
  425. // see if select is placed correctly already
  426. if($input.parent(selector.dropdown).length > 0) {
  427. module.debug('UI dropdown already exists. Creating dropdown menu only');
  428. $module = $input.closest(selector.dropdown);
  429. if( !module.has.menu() ) {
  430. module.create.menu();
  431. }
  432. $menu = $module.children(selector.menu);
  433. module.setup.menu(selectValues);
  434. }
  435. else {
  436. module.debug('Creating entire dropdown from select');
  437. $module = $('<div />')
  438. .attr('class', $input.attr('class') )
  439. .addClass(className.selection)
  440. .addClass(className.dropdown)
  441. .html( templates.dropdown(selectValues, fields, settings.preserveHTML, settings.className) )
  442. .insertBefore($input)
  443. ;
  444. if($input.hasClass(className.multiple) && $input.prop('multiple') === false) {
  445. module.error(error.missingMultiple);
  446. $input.prop('multiple', true);
  447. }
  448. if($input.is('[multiple]')) {
  449. module.set.multiple();
  450. }
  451. if ($input.prop('disabled')) {
  452. module.debug('Disabling dropdown');
  453. $module.addClass(className.disabled);
  454. }
  455. $input
  456. .removeAttr('required')
  457. .removeAttr('class')
  458. .detach()
  459. .prependTo($module)
  460. ;
  461. }
  462. module.refresh();
  463. },
  464. menu: function(values) {
  465. $menu.html( templates.menu(values, fields,settings.preserveHTML,settings.className));
  466. $item = $menu.find(selector.item);
  467. $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $();
  468. },
  469. reference: function() {
  470. module.debug('Dropdown behavior was called on select, replacing with closest dropdown');
  471. // replace module reference
  472. $module = $module.parent(selector.dropdown);
  473. instance = $module.data(moduleNamespace);
  474. element = $module.get(0);
  475. module.refresh();
  476. module.setup.returnedObject();
  477. },
  478. returnedObject: function() {
  479. var
  480. $firstModules = $allModules.slice(0, elementIndex),
  481. $lastModules = $allModules.slice(elementIndex + 1)
  482. ;
  483. // adjust all modules to use correct reference
  484. $allModules = $firstModules.add($module).add($lastModules);
  485. }
  486. },
  487. refresh: function() {
  488. module.refreshSelectors();
  489. module.refreshData();
  490. },
  491. refreshItems: function() {
  492. $item = $menu.find(selector.item);
  493. $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $();
  494. },
  495. refreshSelectors: function() {
  496. module.verbose('Refreshing selector cache');
  497. $text = $module.find(selector.text);
  498. $search = $module.find(selector.search);
  499. $input = $module.find(selector.input);
  500. $icon = $module.find(selector.icon);
  501. $combo = ($module.prev().find(selector.text).length > 0)
  502. ? $module.prev().find(selector.text)
  503. : $module.prev()
  504. ;
  505. $menu = $module.children(selector.menu);
  506. $item = $menu.find(selector.item);
  507. $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $();
  508. },
  509. refreshData: function() {
  510. module.verbose('Refreshing cached metadata');
  511. $item
  512. .removeData(metadata.text)
  513. .removeData(metadata.value)
  514. ;
  515. },
  516. clearData: function() {
  517. module.verbose('Clearing metadata');
  518. $item
  519. .removeData(metadata.text)
  520. .removeData(metadata.value)
  521. ;
  522. $module
  523. .removeData(metadata.defaultText)
  524. .removeData(metadata.defaultValue)
  525. .removeData(metadata.placeholderText)
  526. ;
  527. },
  528. toggle: function() {
  529. module.verbose('Toggling menu visibility');
  530. if( !module.is.active() ) {
  531. module.show();
  532. }
  533. else {
  534. module.hide();
  535. }
  536. },
  537. show: function(callback, preventFocus) {
  538. callback = $.isFunction(callback)
  539. ? callback
  540. : function(){}
  541. ;
  542. if(!module.can.show() && module.is.remote()) {
  543. module.debug('No API results retrieved, searching before show');
  544. module.queryRemote(module.get.query(), module.show);
  545. }
  546. if( module.can.show() && !module.is.active() ) {
  547. module.debug('Showing dropdown');
  548. if(module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered()) ) {
  549. module.remove.message();
  550. }
  551. if(module.is.allFiltered()) {
  552. return true;
  553. }
  554. if(settings.onShow.call(element) !== false) {
  555. module.aria.setExpanded(true);
  556. module.aria.refreshDescendant();
  557. module.animate.show(function() {
  558. if( module.can.click() ) {
  559. module.bind.intent();
  560. }
  561. if(module.has.search() && !preventFocus) {
  562. module.focusSearch();
  563. }
  564. module.set.visible();
  565. callback.call(element);
  566. });
  567. }
  568. }
  569. },
  570. hide: function(callback, preventBlur) {
  571. callback = $.isFunction(callback)
  572. ? callback
  573. : function(){}
  574. ;
  575. if( module.is.active() && !module.is.animatingOutward() ) {
  576. module.debug('Hiding dropdown');
  577. if(settings.onHide.call(element) !== false) {
  578. module.aria.setExpanded(false);
  579. module.aria.removeDescendant();
  580. module.animate.hide(function() {
  581. module.remove.visible();
  582. // hidding search focus
  583. if ( module.is.focusedOnSearch() && preventBlur !== true ) {
  584. $search.blur();
  585. }
  586. callback.call(element);
  587. });
  588. }
  589. } else if( module.can.click() ) {
  590. module.unbind.intent();
  591. }
  592. },
  593. hideOthers: function() {
  594. module.verbose('Finding other dropdowns to hide');
  595. $allModules
  596. .not($module)
  597. .has(selector.menu + '.' + className.visible)
  598. .dropdown('hide')
  599. ;
  600. },
  601. hideMenu: function() {
  602. module.verbose('Hiding menu instantaneously');
  603. module.remove.active();
  604. module.remove.visible();
  605. $menu.transition('hide');
  606. },
  607. hideSubMenus: function() {
  608. var
  609. $subMenus = $menu.children(selector.item).find(selector.menu)
  610. ;
  611. module.verbose('Hiding sub menus', $subMenus);
  612. $subMenus.transition('hide');
  613. },
  614. bind: {
  615. events: function() {
  616. if(hasTouch) {
  617. module.bind.touchEvents();
  618. }
  619. module.bind.keyboardEvents();
  620. module.bind.inputEvents();
  621. module.bind.mouseEvents();
  622. },
  623. touchEvents: function() {
  624. module.debug('Touch device detected binding additional touch events');
  625. if( module.is.searchSelection() ) {
  626. // do nothing special yet
  627. }
  628. else if( module.is.single() ) {
  629. $module
  630. .on('touchstart' + eventNamespace, module.event.test.toggle)
  631. ;
  632. }
  633. $menu
  634. .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter)
  635. ;
  636. },
  637. keyboardEvents: function() {
  638. module.verbose('Binding keyboard events');
  639. $module
  640. .on('keydown' + eventNamespace, module.event.keydown)
  641. ;
  642. if( module.has.search() ) {
  643. $module
  644. .on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input)
  645. ;
  646. }
  647. if( module.is.multiple() ) {
  648. $document
  649. .on('keydown' + elementNamespace, module.event.document.keydown)
  650. ;
  651. }
  652. },
  653. inputEvents: function() {
  654. module.verbose('Binding input change events');
  655. $module
  656. .on('change' + eventNamespace, selector.input, module.event.change)
  657. ;
  658. },
  659. mouseEvents: function() {
  660. module.verbose('Binding mouse events');
  661. if(module.is.multiple()) {
  662. $module
  663. .on('click' + eventNamespace, selector.label, module.event.label.click)
  664. .on('click' + eventNamespace, selector.remove, module.event.remove.click)
  665. ;
  666. }
  667. if( module.is.searchSelection() ) {
  668. $module
  669. .on('mousedown' + eventNamespace, module.event.mousedown)
  670. .on('mouseup' + eventNamespace, module.event.mouseup)
  671. .on('mousedown' + eventNamespace, selector.menu, module.event.menu.mousedown)
  672. .on('mouseup' + eventNamespace, selector.menu, module.event.menu.mouseup)
  673. .on('click' + eventNamespace, selector.icon, module.event.icon.click)
  674. .on('click' + eventNamespace, selector.clearIcon, module.event.clearIcon.click)
  675. .on('focus' + eventNamespace, selector.search, module.event.search.focus)
  676. .on('click' + eventNamespace, selector.search, module.event.search.focus)
  677. .on('blur' + eventNamespace, selector.search, module.event.search.blur)
  678. .on('click' + eventNamespace, selector.text, module.event.text.focus)
  679. ;
  680. if(module.is.multiple()) {
  681. $module
  682. .on('click' + eventNamespace, module.event.click)
  683. ;
  684. }
  685. }
  686. else {
  687. if(settings.on == 'click') {
  688. $module
  689. .on('click' + eventNamespace, selector.icon, module.event.icon.click)
  690. .on('click' + eventNamespace, module.event.test.toggle)
  691. ;
  692. }
  693. else if(settings.on == 'hover') {
  694. $module
  695. .on('mouseenter' + eventNamespace, module.delay.show)
  696. .on('mouseleave' + eventNamespace, module.delay.hide)
  697. ;
  698. }
  699. else {
  700. $module
  701. .on(settings.on + eventNamespace, module.toggle)
  702. ;
  703. }
  704. $module
  705. .on('mousedown' + eventNamespace, module.event.mousedown)
  706. .on('mouseup' + eventNamespace, module.event.mouseup)
  707. .on('focus' + eventNamespace, module.event.focus)
  708. .on('click' + eventNamespace, selector.clearIcon, module.event.clearIcon.click)
  709. ;
  710. if(module.has.menuSearch() ) {
  711. $module
  712. .on('blur' + eventNamespace, selector.search, module.event.search.blur)
  713. ;
  714. }
  715. else {
  716. $module
  717. .on('blur' + eventNamespace, module.event.blur)
  718. ;
  719. }
  720. }
  721. $menu
  722. .on('mouseenter' + eventNamespace, selector.item, module.event.item.mouseenter)
  723. .on('mouseleave' + eventNamespace, selector.item, module.event.item.mouseleave)
  724. .on('click' + eventNamespace, selector.item, module.event.item.click)
  725. ;
  726. },
  727. intent: function() {
  728. module.verbose('Binding hide intent event to document');
  729. if(hasTouch) {
  730. $document
  731. .on('touchstart' + elementNamespace, module.event.test.touch)
  732. .on('touchmove' + elementNamespace, module.event.test.touch)
  733. ;
  734. }
  735. $document
  736. .on('click' + elementNamespace, module.event.test.hide)
  737. ;
  738. }
  739. },
  740. unbind: {
  741. intent: function() {
  742. module.verbose('Removing hide intent event from document');
  743. if(hasTouch) {
  744. $document
  745. .off('touchstart' + elementNamespace)
  746. .off('touchmove' + elementNamespace)
  747. ;
  748. }
  749. $document
  750. .off('click' + elementNamespace)
  751. ;
  752. }
  753. },
  754. filter: function(query) {
  755. var
  756. searchTerm = (query !== undefined)
  757. ? query
  758. : module.get.query(),
  759. afterFiltered = function() {
  760. if(module.is.multiple()) {
  761. module.filterActive();
  762. }
  763. if(query || (!query && module.get.activeItem().length == 0)) {
  764. module.select.firstUnfiltered();
  765. }
  766. if( module.has.allResultsFiltered() ) {
  767. if( settings.onNoResults.call(element, searchTerm) ) {
  768. if(settings.allowAdditions) {
  769. if(settings.hideAdditions) {
  770. module.verbose('User addition with no menu, setting empty style');
  771. module.set.empty();
  772. module.hideMenu();
  773. }
  774. }
  775. else {
  776. module.verbose('All items filtered, showing message', searchTerm);
  777. module.add.message(message.noResults);
  778. }
  779. }
  780. else {
  781. module.verbose('All items filtered, hiding dropdown', searchTerm);
  782. module.hideMenu();
  783. }
  784. }
  785. else {
  786. module.remove.empty();
  787. module.remove.message();
  788. }
  789. if(settings.allowAdditions) {
  790. module.add.userSuggestion(module.escape.htmlEntities(query));
  791. }
  792. if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) {
  793. module.show();
  794. }
  795. }
  796. ;
  797. if(settings.useLabels && module.has.maxSelections()) {
  798. return;
  799. }
  800. if(settings.apiSettings) {
  801. if( module.can.useAPI() ) {
  802. module.queryRemote(searchTerm, function() {
  803. if(settings.filterRemoteData) {
  804. module.filterItems(searchTerm);
  805. }
  806. var preSelected = $input.val();
  807. if(!Array.isArray(preSelected)) {
  808. preSelected = preSelected && preSelected!=="" ? preSelected.split(settings.delimiter) : [];
  809. }
  810. $.each(preSelected,function(index,value){
  811. $item.filter('[data-value="'+value+'"]')
  812. .addClass(className.filtered)
  813. ;
  814. });
  815. afterFiltered();
  816. });
  817. }
  818. else {
  819. module.error(error.noAPI);
  820. }
  821. }
  822. else {
  823. module.filterItems(searchTerm);
  824. afterFiltered();
  825. }
  826. },
  827. queryRemote: function(query, callback) {
  828. var
  829. apiSettings = {
  830. errorDuration : false,
  831. cache : 'local',
  832. throttle : settings.throttle,
  833. urlData : {
  834. query: query
  835. },
  836. onError: function() {
  837. module.add.message(message.serverError);
  838. callback();
  839. },
  840. onFailure: function() {
  841. module.add.message(message.serverError);
  842. callback();
  843. },
  844. onSuccess : function(response) {
  845. var
  846. values = response[fields.remoteValues]
  847. ;
  848. if (!Array.isArray(values)){
  849. values = [];
  850. }
  851. module.remove.message();
  852. module.setup.menu({
  853. values: values
  854. });
  855. if(values.length===0 && !settings.allowAdditions) {
  856. module.add.message(message.noResults);
  857. }
  858. callback();
  859. }
  860. }
  861. ;
  862. if( !$module.api('get request') ) {
  863. module.setup.api();
  864. }
  865. apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings);
  866. $module
  867. .api('setting', apiSettings)
  868. .api('query')
  869. ;
  870. },
  871. filterItems: function(query) {
  872. var
  873. searchTerm = module.remove.diacritics(query !== undefined
  874. ? query
  875. : module.get.query()
  876. ),
  877. results = null,
  878. escapedTerm = module.escape.string(searchTerm),
  879. regExpFlags = (settings.ignoreSearchCase ? 'i' : '') + 'gm',
  880. beginsWithRegExp = new RegExp('^' + escapedTerm, regExpFlags)
  881. ;
  882. // avoid loop if we're matching nothing
  883. if( module.has.query() ) {
  884. results = [];
  885. module.verbose('Searching for matching values', searchTerm);
  886. $item
  887. .each(function(){
  888. var
  889. $choice = $(this),
  890. text,
  891. value
  892. ;
  893. if(settings.match === 'both' || settings.match === 'text') {
  894. text = module.remove.diacritics(String(module.get.choiceText($choice, false)));
  895. if(text.search(beginsWithRegExp) !== -1) {
  896. results.push(this);
  897. return true;
  898. }
  899. else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) {
  900. results.push(this);
  901. return true;
  902. }
  903. else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) {
  904. results.push(this);
  905. return true;
  906. }
  907. }
  908. if(settings.match === 'both' || settings.match === 'value') {
  909. value = module.remove.diacritics(String(module.get.choiceValue($choice, text)));
  910. if(value.search(beginsWithRegExp) !== -1) {
  911. results.push(this);
  912. return true;
  913. }
  914. else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, value)) {
  915. results.push(this);
  916. return true;
  917. }
  918. else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, value)) {
  919. results.push(this);
  920. return true;
  921. }
  922. }
  923. })
  924. ;
  925. }
  926. module.debug('Showing only matched items', searchTerm);
  927. module.remove.filteredItem();
  928. if(results) {
  929. $item
  930. .not(results)
  931. .addClass(className.filtered)
  932. ;
  933. }
  934. if(!module.has.query()) {
  935. $divider
  936. .removeClass(className.hidden);
  937. } else if(settings.hideDividers === true) {
  938. $divider
  939. .addClass(className.hidden);
  940. } else if(settings.hideDividers === 'empty') {
  941. $divider
  942. .removeClass(className.hidden)
  943. .filter(function() {
  944. // First find the last divider in this divider group
  945. // Dividers which are direct siblings are considered a group
  946. var lastDivider = $(this).nextUntil(selector.item);
  947. return (lastDivider.length ? lastDivider : $(this))
  948. // Count all non-filtered items until the next divider (or end of the dropdown)
  949. .nextUntil(selector.divider)
  950. .filter(selector.item + ":not(." + className.filtered + ")")
  951. // Hide divider if no items are found
  952. .length === 0;
  953. })
  954. .addClass(className.hidden);
  955. }
  956. },
  957. fuzzySearch: function(query, term) {
  958. var
  959. termLength = term.length,
  960. queryLength = query.length
  961. ;
  962. query = (settings.ignoreSearchCase ? query.toLowerCase() : query);
  963. term = (settings.ignoreSearchCase ? term.toLowerCase() : term);
  964. if(queryLength > termLength) {
  965. return false;
  966. }
  967. if(queryLength === termLength) {
  968. return (query === term);
  969. }
  970. search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) {
  971. var
  972. queryCharacter = query.charCodeAt(characterIndex)
  973. ;
  974. while(nextCharacterIndex < termLength) {
  975. if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
  976. continue search;
  977. }
  978. }
  979. return false;
  980. }
  981. return true;
  982. },
  983. exactSearch: function (query, term) {
  984. query = (settings.ignoreSearchCase ? query.toLowerCase() : query);
  985. term = (settings.ignoreSearchCase ? term.toLowerCase() : term);
  986. return term.indexOf(query) > -1;
  987. },
  988. filterActive: function() {
  989. if(settings.useLabels) {
  990. $item.filter('.' + className.active)
  991. .addClass(className.filtered)
  992. ;
  993. }
  994. },
  995. focusSearch: function(skipHandler) {
  996. if( module.has.search() && !module.is.focusedOnSearch() ) {
  997. if(skipHandler) {
  998. $module.off('focus' + eventNamespace, selector.search);
  999. $search.focus();
  1000. $module.on('focus' + eventNamespace, selector.search, module.event.search.focus);
  1001. }
  1002. else {
  1003. $search.focus();
  1004. }
  1005. }
  1006. },
  1007. blurSearch: function() {
  1008. if( module.has.search() ) {
  1009. $search.blur();
  1010. }
  1011. },
  1012. forceSelection: function() {
  1013. var
  1014. $currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0),
  1015. $activeItem = $item.not(className.filtered).filter('.' + className.active).eq(0),
  1016. $selectedItem = ($currentlySelected.length > 0)
  1017. ? $currentlySelected
  1018. : $activeItem,
  1019. hasSelected = ($selectedItem.length > 0)
  1020. ;
  1021. if(settings.allowAdditions || (hasSelected && !module.is.multiple())) {
  1022. module.debug('Forcing partial selection to selected item', $selectedItem);
  1023. $selectedItem[0].click();
  1024. }
  1025. else {
  1026. module.remove.searchTerm();
  1027. }
  1028. },
  1029. change: {
  1030. values: function(values) {
  1031. if(!settings.allowAdditions) {
  1032. module.clear();
  1033. }
  1034. module.debug('Creating dropdown with specified values', values);
  1035. module.setup.menu({values: values});
  1036. $.each(values, function(index, item) {
  1037. if(item.selected == true) {
  1038. module.debug('Setting initial selection to', item[fields.value]);
  1039. module.set.selected(item[fields.value]);
  1040. if(!module.is.multiple()) {
  1041. return false;
  1042. }
  1043. }
  1044. });
  1045. if(module.has.selectInput()) {
  1046. module.disconnect.selectObserver();
  1047. $input.html('');
  1048. $input.append('<option disabled selected value></option>');
  1049. $.each(values, function(index, item) {
  1050. var
  1051. value = settings.templates.deQuote(item[fields.value]),
  1052. name = settings.templates.escape(
  1053. item[fields.name] || item[fields.value],
  1054. settings.preserveHTML
  1055. )
  1056. ;
  1057. $input.append('<option value="' + value + '">' + name + '</option>');
  1058. });
  1059. module.observe.select();
  1060. }
  1061. }
  1062. },
  1063. event: {
  1064. change: function() {
  1065. if(!internalChange) {
  1066. module.debug('Input changed, updating selection');
  1067. module.set.selected();
  1068. }
  1069. },
  1070. focus: function() {
  1071. if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) {
  1072. module.show();
  1073. }
  1074. },
  1075. blur: function(event) {
  1076. pageLostFocus = (document.activeElement === this);
  1077. if(!activated && !pageLostFocus) {
  1078. module.remove.activeLabel();
  1079. module.hide();
  1080. }
  1081. },
  1082. mousedown: function() {
  1083. if(module.is.searchSelection()) {
  1084. // prevent menu hiding on immediate re-focus
  1085. willRefocus = true;
  1086. }
  1087. else {
  1088. // prevents focus callback from occurring on mousedown
  1089. activated = true;
  1090. }
  1091. },
  1092. mouseup: function() {
  1093. if(module.is.searchSelection()) {
  1094. // prevent menu hiding on immediate re-focus
  1095. willRefocus = false;
  1096. }
  1097. else {
  1098. activated = false;
  1099. }
  1100. },
  1101. click: function(event) {
  1102. var
  1103. $target = $(event.target)
  1104. ;
  1105. // focus search
  1106. if($target.is($module)) {
  1107. if(!module.is.focusedOnSearch()) {
  1108. module.focusSearch();
  1109. }
  1110. else {
  1111. module.show();
  1112. }
  1113. }
  1114. },
  1115. search: {
  1116. focus: function(event) {
  1117. activated = true;
  1118. if(module.is.multiple()) {
  1119. module.remove.activeLabel();
  1120. }
  1121. if(settings.showOnFocus || (event.type !== 'focus' && event.type !== 'focusin')) {
  1122. module.search();
  1123. }
  1124. },
  1125. blur: function(event) {
  1126. pageLostFocus = (document.activeElement === this);
  1127. if(module.is.searchSelection() && !willRefocus) {
  1128. if(!itemActivated && !pageLostFocus) {
  1129. if(settings.forceSelection) {
  1130. module.forceSelection();
  1131. } else if(!settings.allowAdditions){
  1132. module.remove.searchTerm();
  1133. }
  1134. module.hide();
  1135. }
  1136. }
  1137. willRefocus = false;
  1138. }
  1139. },
  1140. clearIcon: {
  1141. click: function(event) {
  1142. module.clear();
  1143. if(module.is.searchSelection()) {
  1144. module.remove.searchTerm();
  1145. }
  1146. module.hide();
  1147. event.stopPropagation();
  1148. }
  1149. },
  1150. icon: {
  1151. click: function(event) {
  1152. iconClicked=true;
  1153. if(module.has.search()) {
  1154. if(!module.is.active()) {
  1155. if(settings.showOnFocus){
  1156. module.focusSearch();
  1157. } else {
  1158. module.toggle();
  1159. }
  1160. } else {
  1161. module.blurSearch();
  1162. }
  1163. } else {
  1164. module.toggle();
  1165. }
  1166. }
  1167. },
  1168. text: {
  1169. focus: function(event) {
  1170. activated = true;
  1171. module.focusSearch();
  1172. }
  1173. },
  1174. input: function(event) {
  1175. if(module.is.multiple() || module.is.searchSelection()) {
  1176. module.set.filtered();
  1177. }
  1178. clearTimeout(module.timer);
  1179. module.timer = setTimeout(module.search, settings.delay.search);
  1180. },
  1181. label: {
  1182. click: function(event) {
  1183. var
  1184. $label = $(this),
  1185. $labels = $module.find(selector.label),
  1186. $activeLabels = $labels.filter('.' + className.active),
  1187. $nextActive = $label.nextAll('.' + className.active),
  1188. $prevActive = $label.prevAll('.' + className.active),
  1189. $range = ($nextActive.length > 0)
  1190. ? $label.nextUntil($nextActive).add($activeLabels).add($label)
  1191. : $label.prevUntil($prevActive).add($activeLabels).add($label)
  1192. ;
  1193. if(event.shiftKey) {
  1194. $activeLabels.removeClass(className.active);
  1195. $range.addClass(className.active);
  1196. }
  1197. else if(event.ctrlKey) {
  1198. $label.toggleClass(className.active);
  1199. }
  1200. else {
  1201. $activeLabels.removeClass(className.active);
  1202. $label.addClass(className.active);
  1203. }
  1204. settings.onLabelSelect.apply(this, $labels.filter('.' + className.active));
  1205. }
  1206. },
  1207. remove: {
  1208. click: function() {
  1209. var
  1210. $label = $(this).parent()
  1211. ;
  1212. if( $label.hasClass(className.active) ) {
  1213. // remove all selected labels
  1214. module.remove.activeLabels();
  1215. }
  1216. else {
  1217. // remove this label only
  1218. module.remove.activeLabels( $label );
  1219. }
  1220. }
  1221. },
  1222. test: {
  1223. toggle: function(event) {
  1224. var
  1225. toggleBehavior = (module.is.multiple())
  1226. ? module.show
  1227. : module.toggle
  1228. ;
  1229. if(module.is.bubbledLabelClick(event) || module.is.bubbledIconClick(event)) {
  1230. return;
  1231. }
  1232. if( module.determine.eventOnElement(event, toggleBehavior) ) {
  1233. event.preventDefault();
  1234. }
  1235. },
  1236. touch: function(event) {
  1237. module.determine.eventOnElement(event, function() {
  1238. if(event.type == 'touchstart') {
  1239. module.timer = setTimeout(function() {
  1240. module.hide();
  1241. }, settings.delay.touch);
  1242. }
  1243. else if(event.type == 'touchmove') {
  1244. clearTimeout(module.timer);
  1245. }
  1246. });
  1247. event.stopPropagation();
  1248. },
  1249. hide: function(event) {
  1250. if(module.determine.eventInModule(event, module.hide)){
  1251. if(element.id && $(event.target).attr('for') === element.id){
  1252. event.preventDefault();
  1253. }
  1254. }
  1255. }
  1256. },
  1257. select: {
  1258. mutation: function(mutations) {
  1259. module.debug('<select> modified, recreating menu');
  1260. if(module.is.selectMutation(mutations)) {
  1261. module.disconnect.selectObserver();
  1262. module.refresh();
  1263. module.setup.select();
  1264. module.set.selected();
  1265. module.observe.select();
  1266. }
  1267. }
  1268. },
  1269. menu: {
  1270. mutation: function(mutations) {
  1271. var
  1272. mutation = mutations[0],
  1273. $addedNode = mutation.addedNodes
  1274. ? $(mutation.addedNodes[0])
  1275. : $(false),
  1276. $removedNode = mutation.removedNodes
  1277. ? $(mutation.removedNodes[0])
  1278. : $(false),
  1279. $changedNodes = $addedNode.add($removedNode),
  1280. isUserAddition = $changedNodes.is(selector.addition) || $changedNodes.closest(selector.addition).length > 0,
  1281. isMessage = $changedNodes.is(selector.message) || $changedNodes.closest(selector.message).length > 0
  1282. ;
  1283. if(isUserAddition || isMessage) {
  1284. module.debug('Updating item selector cache');
  1285. module.refreshItems();
  1286. }
  1287. else {
  1288. module.debug('Menu modified, updating selector cache');
  1289. module.refresh();
  1290. }
  1291. },
  1292. mousedown: function() {
  1293. itemActivated = true;
  1294. },
  1295. mouseup: function() {
  1296. itemActivated = false;
  1297. }
  1298. },
  1299. item: {
  1300. mouseenter: function(event) {
  1301. var
  1302. $target = $(event.target),
  1303. $item = $(this),
  1304. $subMenu = $item.children(selector.menu),
  1305. $otherMenus = $item.siblings(selector.item).children(selector.menu),
  1306. hasSubMenu = ($subMenu.length > 0),
  1307. isBubbledEvent = ($subMenu.find($target).length > 0)
  1308. ;
  1309. if( !isBubbledEvent && hasSubMenu ) {
  1310. clearTimeout(module.itemTimer);
  1311. module.itemTimer = setTimeout(function() {
  1312. module.verbose('Showing sub-menu', $subMenu);
  1313. $.each($otherMenus, function() {
  1314. module.animate.hide(false, $(this));
  1315. });
  1316. module.animate.show(false, $subMenu);
  1317. }, settings.delay.show);
  1318. event.preventDefault();
  1319. }
  1320. },
  1321. mouseleave: function(event) {
  1322. var
  1323. $subMenu = $(this).children(selector.menu)
  1324. ;
  1325. if($subMenu.length > 0) {
  1326. clearTimeout(module.itemTimer);
  1327. module.itemTimer = setTimeout(function() {
  1328. module.verbose('Hiding sub-menu', $subMenu);
  1329. module.animate.hide(false, $subMenu);
  1330. }, settings.delay.hide);
  1331. }
  1332. },
  1333. click: function (event, skipRefocus) {
  1334. var
  1335. $choice = $(this),
  1336. $target = (event)
  1337. ? $(event.target)
  1338. : $(''),
  1339. $subMenu = $choice.find(selector.menu),
  1340. text = module.get.choiceText($choice),
  1341. value = module.get.choiceValue($choice, text),
  1342. hasSubMenu = ($subMenu.length > 0),
  1343. isBubbledEvent = ($subMenu.find($target).length > 0)
  1344. ;
  1345. // prevents IE11 bug where menu receives focus even though `tabindex=-1`
  1346. if (document.activeElement.tagName.toLowerCase() !== 'input') {
  1347. $(document.activeElement).blur();
  1348. }
  1349. if(!isBubbledEvent && (!hasSubMenu || settings.allowCategorySelection)) {
  1350. if(module.is.searchSelection()) {
  1351. if(settings.allowAdditions) {
  1352. module.remove.userAddition();
  1353. }
  1354. module.remove.searchTerm();
  1355. if(!module.is.focusedOnSearch() && !(skipRefocus == true)) {
  1356. module.focusSearch(true);
  1357. }
  1358. }
  1359. if(!settings.useLabels) {
  1360. module.remove.filteredItem();
  1361. module.set.scrollPosition($choice);
  1362. }
  1363. module.determine.selectAction.call(this, text, value);
  1364. }
  1365. }
  1366. },
  1367. document: {
  1368. // label selection should occur even when element has no focus
  1369. keydown: function(event) {
  1370. var
  1371. pressedKey = event.which,
  1372. isShortcutKey = module.is.inObject(pressedKey, keys)
  1373. ;
  1374. if(isShortcutKey) {
  1375. var
  1376. $label = $module.find(selector.label),
  1377. $activeLabel = $label.filter('.' + className.active),
  1378. activeValue = $activeLabel.data(metadata.value),
  1379. labelIndex = $label.index($activeLabel),
  1380. labelCount = $label.length,
  1381. hasActiveLabel = ($activeLabel.length > 0),
  1382. hasMultipleActive = ($activeLabel.length > 1),
  1383. isFirstLabel = (labelIndex === 0),
  1384. isLastLabel = (labelIndex + 1 == labelCount),
  1385. isSearch = module.is.searchSelection(),
  1386. isFocusedOnSearch = module.is.focusedOnSearch(),
  1387. isFocused = module.is.focused(),
  1388. caretAtStart = (isFocusedOnSearch && module.get.caretPosition(false) === 0),
  1389. isSelectedSearch = (caretAtStart && module.get.caretPosition(true) !== 0),
  1390. $nextLabel
  1391. ;
  1392. if(isSearch && !hasActiveLabel && !isFocusedOnSearch) {
  1393. return;
  1394. }
  1395. if(pressedKey == keys.leftArrow) {
  1396. // activate previous label
  1397. if((isFocused || caretAtStart) && !hasActiveLabel) {
  1398. module.verbose('Selecting previous label');
  1399. $label.last().addClass(className.active);
  1400. }
  1401. else if(hasActiveLabel) {
  1402. if(!event.shiftKey) {
  1403. module.verbose('Selecting previous label');
  1404. $label.removeClass(className.active);
  1405. }
  1406. else {
  1407. module.verbose('Adding previous label to selection');
  1408. }
  1409. if(isFirstLabel && !hasMultipleActive) {
  1410. $activeLabel.addClass(className.active);
  1411. }
  1412. else {
  1413. $activeLabel.prev(selector.siblingLabel)
  1414. .addClass(className.active)
  1415. .end()
  1416. ;
  1417. }
  1418. event.preventDefault();
  1419. }
  1420. }
  1421. else if(pressedKey == keys.rightArrow) {
  1422. // activate first label
  1423. if(isFocused && !hasActiveLabel) {
  1424. $label.first().addClass(className.active);
  1425. }
  1426. // activate next label
  1427. if(hasActiveLabel) {
  1428. if(!event.shiftKey) {
  1429. module.verbose('Selecting next label');
  1430. $label.removeClass(className.active);
  1431. }
  1432. else {
  1433. module.verbose('Adding next label to selection');
  1434. }
  1435. if(isLastLabel) {
  1436. if(isSearch) {
  1437. if(!isFocusedOnSearch) {
  1438. module.focusSearch();
  1439. }
  1440. else {
  1441. $label.removeClass(className.active);
  1442. }
  1443. }
  1444. else if(hasMultipleActive) {
  1445. $activeLabel.next(selector.siblingLabel).addClass(className.active);
  1446. }
  1447. else {
  1448. $activeLabel.addClass(className.active);
  1449. }
  1450. }
  1451. else {
  1452. $activeLabel.next(selector.siblingLabel).addClass(className.active);
  1453. }
  1454. event.preventDefault();
  1455. }
  1456. }
  1457. else if(pressedKey == keys.deleteKey || pressedKey == keys.backspace) {
  1458. if(hasActiveLabel) {
  1459. module.verbose('Removing active labels');
  1460. if(isLastLabel) {
  1461. if(isSearch && !isFocusedOnSearch) {
  1462. module.focusSearch();
  1463. }
  1464. }
  1465. $activeLabel.last().next(selector.siblingLabel).addClass(className.active);
  1466. module.remove.activeLabels($activeLabel);
  1467. event.preventDefault();
  1468. }
  1469. else if(caretAtStart && !isSelectedSearch && !hasActiveLabel && pressedKey == keys.backspace) {
  1470. module.verbose('Removing last label on input backspace');
  1471. $activeLabel = $label.last().addClass(className.active);
  1472. module.remove.activeLabels($activeLabel);
  1473. }
  1474. }
  1475. else {
  1476. $activeLabel.removeClass(className.active);
  1477. }
  1478. }
  1479. }
  1480. },
  1481. keydown: function(event) {
  1482. var
  1483. pressedKey = event.which,
  1484. isShortcutKey = module.is.inObject(pressedKey, keys)
  1485. ;
  1486. if(isShortcutKey) {
  1487. var
  1488. $currentlySelected = $item.not(selector.unselectable).filter('.' + className.selected).eq(0),
  1489. $activeItem = $menu.children('.' + className.active).eq(0),
  1490. $selectedItem = ($currentlySelected.length > 0)
  1491. ? $currentlySelected
  1492. : $activeItem,
  1493. $visibleItems = ($selectedItem.length > 0)
  1494. ? $selectedItem.siblings(':not(.' + className.filtered +')').addBack()
  1495. : $menu.children(':not(.' + className.filtered +')'),
  1496. $subMenu = $selectedItem.children(selector.menu),
  1497. $parentMenu = $selectedItem.closest(selector.menu),
  1498. inVisibleMenu = ($parentMenu.hasClass(className.visible) || $parentMenu.hasClass(className.animating) || $parentMenu.parent(selector.menu).length > 0),
  1499. hasSubMenu = ($subMenu.length> 0),
  1500. hasSelectedItem = ($selectedItem.length > 0),
  1501. selectedIsSelectable = ($selectedItem.not(selector.unselectable).length > 0),
  1502. delimiterPressed = (pressedKey == keys.delimiter && settings.allowAdditions && module.is.multiple()),
  1503. isAdditionWithoutMenu = (settings.allowAdditions && settings.hideAdditions && (pressedKey == keys.enter || delimiterPressed) && selectedIsSelectable),
  1504. $nextItem,
  1505. isSubMenuItem,
  1506. newIndex
  1507. ;
  1508. // allow selection with menu closed
  1509. if(isAdditionWithoutMenu) {
  1510. module.verbose('Selecting item from keyboard shortcut', $selectedItem);
  1511. $selectedItem[0].click();
  1512. if(module.is.searchSelection()) {
  1513. module.remove.searchTerm();
  1514. }
  1515. if(module.is.multiple()){
  1516. event.preventDefault();
  1517. }
  1518. }
  1519. // visible menu keyboard shortcuts
  1520. if( module.is.visible() ) {
  1521. // enter (select or open sub-menu)
  1522. if(pressedKey == keys.enter || delimiterPressed) {
  1523. if(pressedKey == keys.enter && hasSelectedItem && hasSubMenu && !settings.allowCategorySelection) {
  1524. module.verbose('Pressed enter on unselectable category, opening sub menu');
  1525. pressedKey = keys.rightArrow;
  1526. }
  1527. else if(selectedIsSelectable) {
  1528. module.verbose('Selecting item from keyboard shortcut', $selectedItem);
  1529. $selectedItem[0].click();
  1530. if(module.is.searchSelection()) {
  1531. module.remove.searchTerm();
  1532. if(module.is.multiple()) {
  1533. $search.focus();
  1534. }
  1535. }
  1536. }
  1537. event.preventDefault();
  1538. }
  1539. // sub-menu actions
  1540. if(hasSelectedItem) {
  1541. if(pressedKey == keys.leftArrow) {
  1542. isSubMenuItem = ($parentMenu[0] !== $menu[0]);
  1543. if(isSubMenuItem) {
  1544. module.verbose('Left key pressed, closing sub-menu');
  1545. module.animate.hide(false, $parentMenu);
  1546. $selectedItem
  1547. .removeClass(className.selected)
  1548. ;
  1549. $parentMenu
  1550. .closest(selector.item)
  1551. .addClass(className.selected)
  1552. ;
  1553. module.aria.refreshDescendant();
  1554. event.preventDefault();
  1555. }
  1556. }
  1557. // right arrow (show sub-menu)
  1558. if(pressedKey == keys.rightArrow) {
  1559. if(hasSubMenu) {
  1560. module.verbose('Right key pressed, opening sub-menu');
  1561. module.animate.show(false, $subMenu);
  1562. $selectedItem
  1563. .removeClass(className.selected)
  1564. ;
  1565. $subMenu
  1566. .find(selector.item).eq(0)
  1567. .addClass(className.selected)
  1568. ;
  1569. module.aria.refreshDescendant();
  1570. event.preventDefault();
  1571. }
  1572. }
  1573. }
  1574. // up arrow (traverse menu up)
  1575. if(pressedKey == keys.upArrow) {
  1576. $nextItem = (hasSelectedItem && inVisibleMenu)
  1577. ? $selectedItem.prevAll(selector.item + ':not(' + selector.unselectable + ')').eq(0)
  1578. : $item.eq(0)
  1579. ;
  1580. if($visibleItems.index( $nextItem ) < 0) {
  1581. module.verbose('Up key pressed but reached top of current menu');
  1582. event.preventDefault();
  1583. return;
  1584. }
  1585. else {
  1586. module.verbose('Up key pressed, changing active item');
  1587. $selectedItem
  1588. .removeClass(className.selected)
  1589. ;
  1590. $nextItem
  1591. .addClass(className.selected)
  1592. ;
  1593. module.aria.refreshDescendant();
  1594. module.set.scrollPosition($nextItem);
  1595. if(settings.selectOnKeydown && module.is.single()) {
  1596. module.set.selectedItem($nextItem);
  1597. }
  1598. }
  1599. event.preventDefault();
  1600. }
  1601. // down arrow (traverse menu down)
  1602. if(pressedKey == keys.downArrow) {
  1603. $nextItem = (hasSelectedItem && inVisibleMenu)
  1604. ? $nextItem = $selectedItem.nextAll(selector.item + ':not(' + selector.unselectable + ')').eq(0)
  1605. : $item.eq(0)
  1606. ;
  1607. if($nextItem.length === 0) {
  1608. module.verbose('Down key pressed but reached bottom of current menu');
  1609. event.preventDefault();
  1610. return;
  1611. }
  1612. else {
  1613. module.verbose('Down key pressed, changing active item');
  1614. $item
  1615. .removeClass(className.selected)
  1616. ;
  1617. $nextItem
  1618. .addClass(className.selected)
  1619. ;
  1620. module.aria.refreshDescendant();
  1621. module.set.scrollPosition($nextItem);
  1622. if(settings.selectOnKeydown && module.is.single()) {
  1623. module.set.selectedItem($nextItem);
  1624. }
  1625. }
  1626. event.preventDefault();
  1627. }
  1628. // page down (show next page)
  1629. if(pressedKey == keys.pageUp) {
  1630. module.scrollPage('up');
  1631. event.preventDefault();
  1632. }
  1633. if(pressedKey == keys.pageDown) {
  1634. module.scrollPage('down');
  1635. event.preventDefault();
  1636. }
  1637. // escape (close menu)
  1638. if(pressedKey == keys.escape) {
  1639. module.verbose('Escape key pressed, closing dropdown');
  1640. module.hide();
  1641. }
  1642. }
  1643. else {
  1644. // delimiter key
  1645. if(delimiterPressed) {
  1646. event.preventDefault();
  1647. }
  1648. // down arrow (open menu)
  1649. if(pressedKey == keys.downArrow && !module.is.visible()) {
  1650. module.verbose('Down key pressed, showing dropdown');
  1651. module.show();
  1652. event.preventDefault();
  1653. }
  1654. }
  1655. }
  1656. else {
  1657. if( !module.has.search() ) {
  1658. module.set.selectedLetter( String.fromCharCode(pressedKey) );
  1659. }
  1660. }
  1661. }
  1662. },
  1663. trigger: {
  1664. change: function() {
  1665. var
  1666. events = document.createEvent('HTMLEvents'),
  1667. inputElement = $input[0]
  1668. ;
  1669. if(inputElement) {
  1670. module.verbose('Triggering native change event');
  1671. events.initEvent('change', true, false);
  1672. inputElement.dispatchEvent(events);
  1673. }
  1674. }
  1675. },
  1676. determine: {
  1677. selectAction: function(text, value) {
  1678. selectActionActive = true;
  1679. module.verbose('Determining action', settings.action);
  1680. if( $.isFunction( module.action[settings.action] ) ) {
  1681. module.verbose('Triggering preset action', settings.action, text, value);
  1682. module.action[ settings.action ].call(element, text, value, this);
  1683. }
  1684. else if( $.isFunction(settings.action) ) {
  1685. module.verbose('Triggering user action', settings.action, text, value);
  1686. settings.action.call(element, text, value, this);
  1687. }
  1688. else {
  1689. module.error(error.action, settings.action);
  1690. }
  1691. selectActionActive = false;
  1692. },
  1693. eventInModule: function(event, callback) {
  1694. var
  1695. $target = $(event.target),
  1696. inDocument = ($target.closest(document.documentElement).length > 0),
  1697. inModule = ($target.closest($module).length > 0)
  1698. ;
  1699. callback = $.isFunction(callback)
  1700. ? callback
  1701. : function(){}
  1702. ;
  1703. if(inDocument && !inModule) {
  1704. module.verbose('Triggering event', callback);
  1705. callback();
  1706. return true;
  1707. }
  1708. else {
  1709. module.verbose('Event occurred in dropdown, canceling callback');
  1710. return false;
  1711. }
  1712. },
  1713. eventOnElement: function(event, callback) {
  1714. var
  1715. $target = $(event.target),
  1716. $label = $target.closest(selector.siblingLabel),
  1717. inVisibleDOM = document.body.contains(event.target),
  1718. notOnLabel = ($module.find($label).length === 0 || !(module.is.multiple() && settings.useLabels)),
  1719. notInMenu = ($target.closest($menu).length === 0)
  1720. ;
  1721. callback = $.isFunction(callback)
  1722. ? callback
  1723. : function(){}
  1724. ;
  1725. if(inVisibleDOM && notOnLabel && notInMenu) {
  1726. module.verbose('Triggering event', callback);
  1727. callback();
  1728. return true;
  1729. }
  1730. else {
  1731. module.verbose('Event occurred in dropdown menu, canceling callback');
  1732. return false;
  1733. }
  1734. }
  1735. },
  1736. action: {
  1737. nothing: function() {},
  1738. activate: function(text, value, element) {
  1739. value = (value !== undefined)
  1740. ? value
  1741. : text
  1742. ;
  1743. if( module.can.activate( $(element) ) ) {
  1744. module.set.selected(value, $(element));
  1745. if(!module.is.multiple()) {
  1746. module.hideAndClear();
  1747. }
  1748. }
  1749. },
  1750. select: function(text, value, element) {
  1751. value = (value !== undefined)
  1752. ? value
  1753. : text
  1754. ;
  1755. if( module.can.activate( $(element) ) ) {
  1756. module.set.value(value, text, $(element));
  1757. if(!module.is.multiple()) {
  1758. module.hideAndClear();
  1759. }
  1760. }
  1761. },
  1762. combo: function(text, value, element) {
  1763. value = (value !== undefined)
  1764. ? value
  1765. : text
  1766. ;
  1767. module.set.selected(value, $(element));
  1768. module.hideAndClear();
  1769. },
  1770. hide: function(text, value, element) {
  1771. module.set.value(value, text, $(element));
  1772. module.hideAndClear();
  1773. }
  1774. },
  1775. get: {
  1776. id: function() {
  1777. return id;
  1778. },
  1779. defaultText: function() {
  1780. return $module.data(metadata.defaultText);
  1781. },
  1782. defaultValue: function() {
  1783. return $module.data(metadata.defaultValue);
  1784. },
  1785. placeholderText: function() {
  1786. if(settings.placeholder != 'auto' && typeof settings.placeholder == 'string') {
  1787. return settings.placeholder;
  1788. }
  1789. return $module.data(metadata.placeholderText) || '';
  1790. },
  1791. text: function() {
  1792. return $text.text();
  1793. },
  1794. query: function() {
  1795. return $.trim($search.val());
  1796. },
  1797. searchWidth: function(value) {
  1798. value = (value !== undefined)
  1799. ? value
  1800. : $search.val()
  1801. ;
  1802. $sizer.text(value);
  1803. // prevent rounding issues
  1804. return Math.ceil( $sizer.width() + 1);
  1805. },
  1806. selectionCount: function() {
  1807. var
  1808. values = module.get.values(),
  1809. count
  1810. ;
  1811. count = ( module.is.multiple() )
  1812. ? Array.isArray(values)
  1813. ? values.length
  1814. : 0
  1815. : (module.get.value() !== '')
  1816. ? 1
  1817. : 0
  1818. ;
  1819. return count;
  1820. },
  1821. transition: function($subMenu) {
  1822. return (settings.transition == 'auto')
  1823. ? module.is.upward($subMenu)
  1824. ? 'slide up'
  1825. : 'slide down'
  1826. : settings.transition
  1827. ;
  1828. },
  1829. userValues: function() {
  1830. var
  1831. values = module.get.values()
  1832. ;
  1833. if(!values) {
  1834. return false;
  1835. }
  1836. values = Array.isArray(values)
  1837. ? values
  1838. : [values]
  1839. ;
  1840. return $.grep(values, function(value) {
  1841. return (module.get.item(value) === false);
  1842. });
  1843. },
  1844. uniqueArray: function(array) {
  1845. return $.grep(array, function (value, index) {
  1846. return $.inArray(value, array) === index;
  1847. });
  1848. },
  1849. caretPosition: function(returnEndPos) {
  1850. var
  1851. input = $search.get(0),
  1852. range,
  1853. rangeLength
  1854. ;
  1855. if(returnEndPos && 'selectionEnd' in input){
  1856. return input.selectionEnd;
  1857. }
  1858. else if(!returnEndPos && 'selectionStart' in input) {
  1859. return input.selectionStart;
  1860. }
  1861. if (document.selection) {
  1862. input.focus();
  1863. range = document.selection.createRange();
  1864. rangeLength = range.text.length;
  1865. if(returnEndPos) {
  1866. return rangeLength;
  1867. }
  1868. range.moveStart('character', -input.value.length);
  1869. return range.text.length - rangeLength;
  1870. }
  1871. },
  1872. value: function() {
  1873. var
  1874. value = ($input.length > 0)
  1875. ? $input.val()
  1876. : $module.data(metadata.value),
  1877. isEmptyMultiselect = (Array.isArray(value) && value.length === 1 && value[0] === '')
  1878. ;
  1879. // prevents placeholder element from being selected when multiple
  1880. return (value === undefined || isEmptyMultiselect)
  1881. ? ''
  1882. : value
  1883. ;
  1884. },
  1885. values: function() {
  1886. var
  1887. value = module.get.value()
  1888. ;
  1889. if(value === '') {
  1890. return '';
  1891. }
  1892. return ( !module.has.selectInput() && module.is.multiple() )
  1893. ? (typeof value == 'string') // delimited string
  1894. ? module.escape.htmlEntities(value).split(settings.delimiter)
  1895. : ''
  1896. : value
  1897. ;
  1898. },
  1899. remoteValues: function() {
  1900. var
  1901. values = module.get.values(),
  1902. remoteValues = false
  1903. ;
  1904. if(values) {
  1905. if(typeof values == 'string') {
  1906. values = [values];
  1907. }
  1908. $.each(values, function(index, value) {
  1909. var
  1910. name = module.read.remoteData(value)
  1911. ;
  1912. module.verbose('Restoring value from session data', name, value);
  1913. if(name) {
  1914. if(!remoteValues) {
  1915. remoteValues = {};
  1916. }
  1917. remoteValues[value] = name;
  1918. }
  1919. });
  1920. }
  1921. return remoteValues;
  1922. },
  1923. choiceText: function($choice, preserveHTML) {
  1924. preserveHTML = (preserveHTML !== undefined)
  1925. ? preserveHTML
  1926. : settings.preserveHTML
  1927. ;
  1928. if($choice) {
  1929. if($choice.find(selector.menu).length > 0) {
  1930. module.verbose('Retrieving text of element with sub-menu');
  1931. $choice = $choice.clone();
  1932. $choice.find(selector.menu).remove();
  1933. $choice.find(selector.menuIcon).remove();
  1934. }
  1935. return ($choice.data(metadata.text) !== undefined)
  1936. ? $choice.data(metadata.text)
  1937. : (preserveHTML)
  1938. ? $.trim($choice.html())
  1939. : $.trim($choice.text())
  1940. ;
  1941. }
  1942. },
  1943. choiceValue: function($choice, choiceText) {
  1944. choiceText = choiceText || module.get.choiceText($choice);
  1945. if(!$choice) {
  1946. return false;
  1947. }
  1948. return ($choice.data(metadata.value) !== undefined)
  1949. ? String( $choice.data(metadata.value) )
  1950. : (typeof choiceText === 'string')
  1951. ? $.trim(
  1952. settings.ignoreSearchCase
  1953. ? choiceText.toLowerCase()
  1954. : choiceText
  1955. )
  1956. : String(choiceText)
  1957. ;
  1958. },
  1959. inputEvent: function() {
  1960. var
  1961. input = $search[0]
  1962. ;
  1963. if(input) {
  1964. return (input.oninput !== undefined)
  1965. ? 'input'
  1966. : (input.onpropertychange !== undefined)
  1967. ? 'propertychange'
  1968. : 'keyup'
  1969. ;
  1970. }
  1971. return false;
  1972. },
  1973. selectValues: function() {
  1974. var
  1975. select = {},
  1976. oldGroup = []
  1977. ;
  1978. select.values = [];
  1979. $module
  1980. .find('option')
  1981. .each(function() {
  1982. var
  1983. $option = $(this),
  1984. name = $option.html(),
  1985. disabled = $option.attr('disabled'),
  1986. value = ( $option.attr('value') !== undefined )
  1987. ? $option.attr('value')
  1988. : name,
  1989. group = $option.parent('optgroup')
  1990. ;
  1991. if(settings.placeholder === 'auto' && value === '') {
  1992. select.placeholder = name;
  1993. }
  1994. else {
  1995. if(group.length !== oldGroup.length || group[0] !== oldGroup[0]) {
  1996. select.values.push({
  1997. type: 'header',
  1998. divider: settings.headerDivider,
  1999. name: group.attr('label') || ''
  2000. });
  2001. oldGroup = group;
  2002. }
  2003. select.values.push({
  2004. name : name,
  2005. value : value,
  2006. disabled : disabled
  2007. });
  2008. }
  2009. })
  2010. ;
  2011. if(settings.placeholder && settings.placeholder !== 'auto') {
  2012. module.debug('Setting placeholder value to', settings.placeholder);
  2013. select.placeholder = settings.placeholder;
  2014. }
  2015. if(settings.sortSelect) {
  2016. if(settings.sortSelect === true) {
  2017. select.values.sort(function(a, b) {
  2018. return a.name.localeCompare(b.name);
  2019. });
  2020. } else if(settings.sortSelect === 'natural') {
  2021. select.values.sort(function(a, b) {
  2022. return (a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
  2023. });
  2024. } else if($.isFunction(settings.sortSelect)) {
  2025. select.values.sort(settings.sortSelect);
  2026. }
  2027. module.debug('Retrieved and sorted values from select', select);
  2028. }
  2029. else {
  2030. module.debug('Retrieved values from select', select);
  2031. }
  2032. return select;
  2033. },
  2034. activeItem: function() {
  2035. return $item.filter('.' + className.active);
  2036. },
  2037. selectedItem: function() {
  2038. var
  2039. $selectedItem = $item.not(selector.unselectable).filter('.' + className.selected)
  2040. ;
  2041. return ($selectedItem.length > 0)
  2042. ? $selectedItem
  2043. : $item.eq(0)
  2044. ;
  2045. },
  2046. itemWithAdditions: function(value) {
  2047. var
  2048. $items = module.get.item(value),
  2049. $userItems = module.create.userChoice(value),
  2050. hasUserItems = ($userItems && $userItems.length > 0)
  2051. ;
  2052. if(hasUserItems) {
  2053. $items = ($items.length > 0)
  2054. ? $items.add($userItems)
  2055. : $userItems
  2056. ;
  2057. }
  2058. return $items;
  2059. },
  2060. item: function(value, strict) {
  2061. var
  2062. $selectedItem = false,
  2063. shouldSearch,
  2064. isMultiple
  2065. ;
  2066. value = (value !== undefined)
  2067. ? value
  2068. : ( module.get.values() !== undefined)
  2069. ? module.get.values()
  2070. : module.get.text()
  2071. ;
  2072. isMultiple = (module.is.multiple() && Array.isArray(value));
  2073. shouldSearch = (isMultiple)
  2074. ? (value.length > 0)
  2075. : (value !== undefined && value !== null)
  2076. ;
  2077. strict = (value === '' || value === false || value === true)
  2078. ? true
  2079. : strict || false
  2080. ;
  2081. if(shouldSearch) {
  2082. $item
  2083. .each(function() {
  2084. var
  2085. $choice = $(this),
  2086. optionText = module.get.choiceText($choice),
  2087. optionValue = module.get.choiceValue($choice, optionText)
  2088. ;
  2089. // safe early exit
  2090. if(optionValue === null || optionValue === undefined) {
  2091. return;
  2092. }
  2093. if(isMultiple) {
  2094. if($.inArray( String(optionValue), value) !== -1) {
  2095. $selectedItem = ($selectedItem)
  2096. ? $selectedItem.add($choice)
  2097. : $choice
  2098. ;
  2099. }
  2100. }
  2101. else if(strict) {
  2102. module.verbose('Ambiguous dropdown value using strict type check', $choice, value);
  2103. if( optionValue === value) {
  2104. $selectedItem = $choice;
  2105. return true;
  2106. }
  2107. }
  2108. else {
  2109. if(settings.ignoreCase) {
  2110. optionValue = optionValue.toLowerCase();
  2111. value = value.toLowerCase();
  2112. }
  2113. if( String(optionValue) == String(value)) {
  2114. module.verbose('Found select item by value', optionValue, value);
  2115. $selectedItem = $choice;
  2116. return true;
  2117. }
  2118. }
  2119. })
  2120. ;
  2121. }
  2122. return $selectedItem;
  2123. }
  2124. },
  2125. check: {
  2126. maxSelections: function(selectionCount) {
  2127. if(settings.maxSelections) {
  2128. selectionCount = (selectionCount !== undefined)
  2129. ? selectionCount
  2130. : module.get.selectionCount()
  2131. ;
  2132. if(selectionCount >= settings.maxSelections) {
  2133. module.debug('Maximum selection count reached');
  2134. if(settings.useLabels) {
  2135. $item.addClass(className.filtered);
  2136. module.add.message(message.maxSelections);
  2137. }
  2138. return true;
  2139. }
  2140. else {
  2141. module.verbose('No longer at maximum selection count');
  2142. module.remove.message();
  2143. module.remove.filteredItem();
  2144. if(module.is.searchSelection()) {
  2145. module.filterItems();
  2146. }
  2147. return false;
  2148. }
  2149. }
  2150. return true;
  2151. }
  2152. },
  2153. restore: {
  2154. defaults: function(preventChangeTrigger) {
  2155. module.clear(preventChangeTrigger);
  2156. module.restore.defaultText();
  2157. module.restore.defaultValue();
  2158. },
  2159. defaultText: function() {
  2160. var
  2161. defaultText = module.get.defaultText(),
  2162. placeholderText = module.get.placeholderText
  2163. ;
  2164. if(defaultText === placeholderText) {
  2165. module.debug('Restoring default placeholder text', defaultText);
  2166. module.set.placeholderText(defaultText);
  2167. }
  2168. else {
  2169. module.debug('Restoring default text', defaultText);
  2170. module.set.text(defaultText);
  2171. }
  2172. },
  2173. placeholderText: function() {
  2174. module.set.placeholderText();
  2175. },
  2176. defaultValue: function() {
  2177. var
  2178. defaultValue = module.get.defaultValue()
  2179. ;
  2180. if(defaultValue !== undefined) {
  2181. module.debug('Restoring default value', defaultValue);
  2182. if(defaultValue !== '') {
  2183. module.set.value(defaultValue);
  2184. module.set.selected();
  2185. }
  2186. else {
  2187. module.remove.activeItem();
  2188. module.remove.selectedItem();
  2189. }
  2190. }
  2191. },
  2192. labels: function() {
  2193. if(settings.allowAdditions) {
  2194. if(!settings.useLabels) {
  2195. module.error(error.labels);
  2196. settings.useLabels = true;
  2197. }
  2198. module.debug('Restoring selected values');
  2199. module.create.userLabels();
  2200. }
  2201. module.check.maxSelections();
  2202. },
  2203. selected: function() {
  2204. module.restore.values();
  2205. if(module.is.multiple()) {
  2206. module.debug('Restoring previously selected values and labels');
  2207. module.restore.labels();
  2208. }
  2209. else {
  2210. module.debug('Restoring previously selected values');
  2211. }
  2212. },
  2213. values: function() {
  2214. // prevents callbacks from occurring on initial load
  2215. module.set.initialLoad();
  2216. if(settings.apiSettings && settings.saveRemoteData && module.get.remoteValues()) {
  2217. module.restore.remoteValues();
  2218. }
  2219. else {
  2220. module.set.selected();
  2221. }
  2222. var value = module.get.value();
  2223. if(value && value !== '' && !(Array.isArray(value) && value.length === 0)) {
  2224. $input.removeClass(className.noselection);
  2225. } else {
  2226. $input.addClass(className.noselection);
  2227. }
  2228. module.remove.initialLoad();
  2229. },
  2230. remoteValues: function() {
  2231. var
  2232. values = module.get.remoteValues()
  2233. ;
  2234. module.debug('Recreating selected from session data', values);
  2235. if(values) {
  2236. if( module.is.single() ) {
  2237. $.each(values, function(value, name) {
  2238. module.set.text(name);
  2239. });
  2240. }
  2241. else {
  2242. $.each(values, function(value, name) {
  2243. module.add.label(value, name);
  2244. });
  2245. }
  2246. }
  2247. }
  2248. },
  2249. read: {
  2250. remoteData: function(value) {
  2251. var
  2252. name
  2253. ;
  2254. if(window.Storage === undefined) {
  2255. module.error(error.noStorage);
  2256. return;
  2257. }
  2258. name = sessionStorage.getItem(value);
  2259. return (name !== undefined)
  2260. ? name
  2261. : false
  2262. ;
  2263. }
  2264. },
  2265. save: {
  2266. defaults: function() {
  2267. module.save.defaultText();
  2268. module.save.placeholderText();
  2269. module.save.defaultValue();
  2270. },
  2271. defaultValue: function() {
  2272. var
  2273. value = module.get.value()
  2274. ;
  2275. module.verbose('Saving default value as', value);
  2276. $module.data(metadata.defaultValue, value);
  2277. },
  2278. defaultText: function() {
  2279. var
  2280. text = module.get.text()
  2281. ;
  2282. module.verbose('Saving default text as', text);
  2283. $module.data(metadata.defaultText, text);
  2284. },
  2285. placeholderText: function() {
  2286. var
  2287. text
  2288. ;
  2289. if(settings.placeholder !== false && $text.hasClass(className.placeholder)) {
  2290. text = module.get.text();
  2291. module.verbose('Saving placeholder text as', text);
  2292. $module.data(metadata.placeholderText, text);
  2293. }
  2294. },
  2295. remoteData: function(name, value) {
  2296. if(window.Storage === undefined) {
  2297. module.error(error.noStorage);
  2298. return;
  2299. }
  2300. module.verbose('Saving remote data to session storage', value, name);
  2301. sessionStorage.setItem(value, name);
  2302. }
  2303. },
  2304. clear: function(preventChangeTrigger) {
  2305. if(module.is.multiple() && settings.useLabels) {
  2306. module.remove.labels();
  2307. }
  2308. else {
  2309. module.remove.activeItem();
  2310. module.remove.selectedItem();
  2311. module.remove.filteredItem();
  2312. }
  2313. module.set.placeholderText();
  2314. module.clearValue(preventChangeTrigger);
  2315. },
  2316. clearValue: function(preventChangeTrigger) {
  2317. module.set.value('', null, null, preventChangeTrigger);
  2318. },
  2319. scrollPage: function(direction, $selectedItem) {
  2320. var
  2321. $currentItem = $selectedItem || module.get.selectedItem(),
  2322. $menu = $currentItem.closest(selector.menu),
  2323. menuHeight = $menu.outerHeight(),
  2324. currentScroll = $menu.scrollTop(),
  2325. itemHeight = $item.eq(0).outerHeight(),
  2326. itemsPerPage = Math.floor(menuHeight / itemHeight),
  2327. maxScroll = $menu.prop('scrollHeight'),
  2328. newScroll = (direction == 'up')
  2329. ? currentScroll - (itemHeight * itemsPerPage)
  2330. : currentScroll + (itemHeight * itemsPerPage),
  2331. $selectableItem = $item.not(selector.unselectable),
  2332. isWithinRange,
  2333. $nextSelectedItem,
  2334. elementIndex
  2335. ;
  2336. elementIndex = (direction == 'up')
  2337. ? $selectableItem.index($currentItem) - itemsPerPage
  2338. : $selectableItem.index($currentItem) + itemsPerPage
  2339. ;
  2340. isWithinRange = (direction == 'up')
  2341. ? (elementIndex >= 0)
  2342. : (elementIndex < $selectableItem.length)
  2343. ;
  2344. $nextSelectedItem = (isWithinRange)
  2345. ? $selectableItem.eq(elementIndex)
  2346. : (direction == 'up')
  2347. ? $selectableItem.first()
  2348. : $selectableItem.last()
  2349. ;
  2350. if($nextSelectedItem.length > 0) {
  2351. module.debug('Scrolling page', direction, $nextSelectedItem);
  2352. $currentItem
  2353. .removeClass(className.selected)
  2354. ;
  2355. $nextSelectedItem
  2356. .addClass(className.selected)
  2357. ;
  2358. if(settings.selectOnKeydown && module.is.single()) {
  2359. module.set.selectedItem($nextSelectedItem);
  2360. }
  2361. $menu
  2362. .scrollTop(newScroll)
  2363. ;
  2364. }
  2365. },
  2366. set: {
  2367. filtered: function() {
  2368. var
  2369. isMultiple = module.is.multiple(),
  2370. isSearch = module.is.searchSelection(),
  2371. isSearchMultiple = (isMultiple && isSearch),
  2372. searchValue = (isSearch)
  2373. ? module.get.query()
  2374. : '',
  2375. hasSearchValue = (typeof searchValue === 'string' && searchValue.length > 0),
  2376. searchWidth = module.get.searchWidth(),
  2377. valueIsSet = searchValue !== ''
  2378. ;
  2379. if(isMultiple && hasSearchValue) {
  2380. module.verbose('Adjusting input width', searchWidth, settings.glyphWidth);
  2381. $search.css('width', searchWidth);
  2382. }
  2383. if(hasSearchValue || (isSearchMultiple && valueIsSet)) {
  2384. module.verbose('Hiding placeholder text');
  2385. $text.addClass(className.filtered);
  2386. }
  2387. else if(!isMultiple || (isSearchMultiple && !valueIsSet)) {
  2388. module.verbose('Showing placeholder text');
  2389. $text.removeClass(className.filtered);
  2390. }
  2391. },
  2392. empty: function() {
  2393. $module.addClass(className.empty);
  2394. },
  2395. loading: function() {
  2396. $module.addClass(className.loading);
  2397. },
  2398. placeholderText: function(text) {
  2399. text = text || module.get.placeholderText();
  2400. module.debug('Setting placeholder text', text);
  2401. module.set.text(text);
  2402. $text.addClass(className.placeholder);
  2403. },
  2404. tabbable: function() {
  2405. if( module.is.searchSelection() ) {
  2406. module.debug('Added tabindex to searchable dropdown');
  2407. $search
  2408. .val('')
  2409. .attr('tabindex', 0)
  2410. ;
  2411. $menu
  2412. .attr('tabindex', -1)
  2413. ;
  2414. }
  2415. else {
  2416. module.debug('Added tabindex to dropdown');
  2417. if( $module.attr('tabindex') === undefined) {
  2418. $module
  2419. .attr('tabindex', 0)
  2420. ;
  2421. $menu
  2422. .attr('tabindex', -1)
  2423. ;
  2424. }
  2425. }
  2426. },
  2427. initialLoad: function() {
  2428. module.verbose('Setting initial load');
  2429. initialLoad = true;
  2430. },
  2431. activeItem: function($item) {
  2432. if( settings.allowAdditions && $item.filter(selector.addition).length > 0 ) {
  2433. $item.addClass(className.filtered);
  2434. }
  2435. else {
  2436. $item.addClass(className.active);
  2437. }
  2438. },
  2439. partialSearch: function(text) {
  2440. var
  2441. length = module.get.query().length
  2442. ;
  2443. $search.val( text.substr(0, length));
  2444. },
  2445. scrollPosition: function($item, forceScroll) {
  2446. var
  2447. edgeTolerance = 5,
  2448. $menu,
  2449. hasActive,
  2450. offset,
  2451. itemHeight,
  2452. itemOffset,
  2453. menuOffset,
  2454. menuScroll,
  2455. menuHeight,
  2456. abovePage,
  2457. belowPage
  2458. ;
  2459. $item = $item || module.get.selectedItem();
  2460. $menu = $item.closest(selector.menu);
  2461. hasActive = ($item && $item.length > 0);
  2462. forceScroll = (forceScroll !== undefined)
  2463. ? forceScroll
  2464. : false
  2465. ;
  2466. if(module.get.activeItem().length === 0){
  2467. forceScroll = false;
  2468. }
  2469. if($item && $menu.length > 0 && hasActive) {
  2470. itemOffset = $item.position().top;
  2471. $menu.addClass(className.loading);
  2472. menuScroll = $menu.scrollTop();
  2473. menuOffset = $menu.offset().top;
  2474. itemOffset = $item.offset().top;
  2475. offset = menuScroll - menuOffset + itemOffset;
  2476. if(!forceScroll) {
  2477. menuHeight = $menu.height();
  2478. belowPage = menuScroll + menuHeight < (offset + edgeTolerance);
  2479. abovePage = ((offset - edgeTolerance) < menuScroll);
  2480. }
  2481. module.debug('Scrolling to active item', offset);
  2482. if(forceScroll || abovePage || belowPage) {
  2483. $menu.scrollTop(offset);
  2484. }
  2485. $menu.removeClass(className.loading);
  2486. }
  2487. },
  2488. text: function(text) {
  2489. if(settings.action === 'combo') {
  2490. module.debug('Changing combo button text', text, $combo);
  2491. if(settings.preserveHTML) {
  2492. $combo.html(text);
  2493. }
  2494. else {
  2495. $combo.text(text);
  2496. }
  2497. }
  2498. else if(settings.action === 'activate') {
  2499. if(text !== module.get.placeholderText()) {
  2500. $text.removeClass(className.placeholder);
  2501. }
  2502. module.debug('Changing text', text, $text);
  2503. $text
  2504. .removeClass(className.filtered)
  2505. ;
  2506. if(settings.preserveHTML) {
  2507. $text.html(text);
  2508. }
  2509. else {
  2510. $text.text(text);
  2511. }
  2512. }
  2513. },
  2514. selectedItem: function($item) {
  2515. var
  2516. value = module.get.choiceValue($item),
  2517. searchText = module.get.choiceText($item, false),
  2518. text = module.get.choiceText($item, true)
  2519. ;
  2520. module.debug('Setting user selection to item', $item);
  2521. module.remove.activeItem();
  2522. module.set.partialSearch(searchText);
  2523. module.set.activeItem($item);
  2524. module.set.selected(value, $item);
  2525. module.set.text(text);
  2526. },
  2527. selectedLetter: function(letter) {
  2528. var
  2529. $selectedItem = $item.filter('.' + className.selected),
  2530. alreadySelectedLetter = $selectedItem.length > 0 && module.has.firstLetter($selectedItem, letter),
  2531. $nextValue = false,
  2532. $nextItem
  2533. ;
  2534. // check next of same letter
  2535. if(alreadySelectedLetter) {
  2536. $nextItem = $selectedItem.nextAll($item).eq(0);
  2537. if( module.has.firstLetter($nextItem, letter) ) {
  2538. $nextValue = $nextItem;
  2539. }
  2540. }
  2541. // check all values
  2542. if(!$nextValue) {
  2543. $item
  2544. .each(function(){
  2545. if(module.has.firstLetter($(this), letter)) {
  2546. $nextValue = $(this);
  2547. return false;
  2548. }
  2549. })
  2550. ;
  2551. }
  2552. // set next value
  2553. if($nextValue) {
  2554. module.verbose('Scrolling to next value with letter', letter);
  2555. module.set.scrollPosition($nextValue);
  2556. $selectedItem.removeClass(className.selected);
  2557. $nextValue.addClass(className.selected);
  2558. module.aria.refreshDescendant();
  2559. if(settings.selectOnKeydown && module.is.single()) {
  2560. module.set.selectedItem($nextValue);
  2561. }
  2562. }
  2563. },
  2564. direction: function($menu) {
  2565. if(settings.direction == 'auto') {
  2566. // reset position, remove upward if it's base menu
  2567. if (!$menu) {
  2568. module.remove.upward();
  2569. } else if (module.is.upward($menu)) {
  2570. //we need make sure when make assertion openDownward for $menu, $menu does not have upward class
  2571. module.remove.upward($menu);
  2572. }
  2573. if(module.can.openDownward($menu)) {
  2574. module.remove.upward($menu);
  2575. }
  2576. else {
  2577. module.set.upward($menu);
  2578. }
  2579. if(!module.is.leftward($menu) && !module.can.openRightward($menu)) {
  2580. module.set.leftward($menu);
  2581. }
  2582. }
  2583. else if(settings.direction == 'upward') {
  2584. module.set.upward($menu);
  2585. }
  2586. },
  2587. upward: function($currentMenu) {
  2588. var $element = $currentMenu || $module;
  2589. $element.addClass(className.upward);
  2590. },
  2591. leftward: function($currentMenu) {
  2592. var $element = $currentMenu || $menu;
  2593. $element.addClass(className.leftward);
  2594. },
  2595. value: function(value, text, $selected, preventChangeTrigger) {
  2596. if(value !== undefined && value !== '' && !(Array.isArray(value) && value.length === 0)) {
  2597. $input.removeClass(className.noselection);
  2598. } else {
  2599. $input.addClass(className.noselection);
  2600. }
  2601. var
  2602. escapedValue = module.escape.value(value),
  2603. hasInput = ($input.length > 0),
  2604. currentValue = module.get.values(),
  2605. stringValue = (value !== undefined)
  2606. ? String(value)
  2607. : value,
  2608. newValue
  2609. ;
  2610. if(hasInput) {
  2611. if(!settings.allowReselection && stringValue == currentValue) {
  2612. module.verbose('Skipping value update already same value', value, currentValue);
  2613. if(!module.is.initialLoad()) {
  2614. return;
  2615. }
  2616. }
  2617. if( module.is.single() && module.has.selectInput() && module.can.extendSelect() ) {
  2618. module.debug('Adding user option', value);
  2619. module.add.optionValue(value);
  2620. }
  2621. module.debug('Updating input value', escapedValue, currentValue);
  2622. internalChange = true;
  2623. $input
  2624. .val(escapedValue)
  2625. ;
  2626. if(settings.fireOnInit === false && module.is.initialLoad()) {
  2627. module.debug('Input native change event ignored on initial load');
  2628. }
  2629. else if(preventChangeTrigger !== true) {
  2630. module.trigger.change();
  2631. }
  2632. internalChange = false;
  2633. }
  2634. else {
  2635. module.verbose('Storing value in metadata', escapedValue, $input);
  2636. if(escapedValue !== currentValue) {
  2637. $module.data(metadata.value, stringValue);
  2638. }
  2639. }
  2640. if(settings.fireOnInit === false && module.is.initialLoad()) {
  2641. module.verbose('No callback on initial load', settings.onChange);
  2642. }
  2643. else if(preventChangeTrigger !== true) {
  2644. settings.onChange.call(element, value, text, $selected);
  2645. }
  2646. },
  2647. active: function() {
  2648. $module
  2649. .addClass(className.active)
  2650. ;
  2651. },
  2652. multiple: function() {
  2653. $module.addClass(className.multiple);
  2654. },
  2655. visible: function() {
  2656. $module.addClass(className.visible);
  2657. },
  2658. exactly: function(value, $selectedItem) {
  2659. module.debug('Setting selected to exact values');
  2660. module.clear();
  2661. module.set.selected(value, $selectedItem);
  2662. },
  2663. selected: function(value, $selectedItem) {
  2664. var
  2665. isMultiple = module.is.multiple()
  2666. ;
  2667. $selectedItem = (settings.allowAdditions)
  2668. ? $selectedItem || module.get.itemWithAdditions(value)
  2669. : $selectedItem || module.get.item(value)
  2670. ;
  2671. if(!$selectedItem) {
  2672. return;
  2673. }
  2674. module.debug('Setting selected menu item to', $selectedItem);
  2675. if(module.is.multiple()) {
  2676. module.remove.searchWidth();
  2677. }
  2678. if(module.is.single()) {
  2679. module.remove.activeItem();
  2680. module.remove.selectedItem();
  2681. }
  2682. else if(settings.useLabels) {
  2683. module.remove.selectedItem();
  2684. }
  2685. // select each item
  2686. $selectedItem
  2687. .each(function() {
  2688. var
  2689. $selected = $(this),
  2690. selectedText = module.get.choiceText($selected),
  2691. selectedValue = module.get.choiceValue($selected, selectedText),
  2692. isFiltered = $selected.hasClass(className.filtered),
  2693. isActive = $selected.hasClass(className.active),
  2694. isUserValue = $selected.hasClass(className.addition),
  2695. shouldAnimate = (isMultiple && $selectedItem.length == 1)
  2696. ;
  2697. if(isMultiple) {
  2698. if(!isActive || isUserValue) {
  2699. if(settings.apiSettings && settings.saveRemoteData) {
  2700. module.save.remoteData(selectedText, selectedValue);
  2701. }
  2702. if(settings.useLabels) {
  2703. module.add.label(selectedValue, selectedText, shouldAnimate);
  2704. module.add.value(selectedValue, selectedText, $selected);
  2705. module.set.activeItem($selected);
  2706. module.filterActive();
  2707. module.select.nextAvailable($selectedItem);
  2708. }
  2709. else {
  2710. module.add.value(selectedValue, selectedText, $selected);
  2711. module.set.text(module.add.variables(message.count));
  2712. module.set.activeItem($selected);
  2713. }
  2714. }
  2715. else if(!isFiltered && (settings.useLabels || selectActionActive)) {
  2716. module.debug('Selected active value, removing label');
  2717. module.remove.selected(selectedValue);
  2718. }
  2719. }
  2720. else {
  2721. if(settings.apiSettings && settings.saveRemoteData) {
  2722. module.save.remoteData(selectedText, selectedValue);
  2723. }
  2724. module.set.text(selectedText);
  2725. module.set.value(selectedValue, selectedText, $selected);
  2726. $selected
  2727. .addClass(className.active)
  2728. .addClass(className.selected)
  2729. ;
  2730. }
  2731. })
  2732. ;
  2733. module.remove.searchTerm();
  2734. }
  2735. },
  2736. add: {
  2737. label: function(value, text, shouldAnimate) {
  2738. var
  2739. $next = module.is.searchSelection()
  2740. ? $search
  2741. : $text,
  2742. escapedValue = module.escape.value(value),
  2743. $label
  2744. ;
  2745. if(settings.ignoreCase) {
  2746. escapedValue = escapedValue.toLowerCase();
  2747. }
  2748. $label = $('<a />')
  2749. .addClass(className.label)
  2750. .attr('data-' + metadata.value, escapedValue)
  2751. .html(templates.label(escapedValue, text, settings.preserveHTML, settings.className))
  2752. ;
  2753. $label = settings.onLabelCreate.call($label, escapedValue, text);
  2754. if(module.has.label(value)) {
  2755. module.debug('User selection already exists, skipping', escapedValue);
  2756. return;
  2757. }
  2758. if(settings.label.variation) {
  2759. $label.addClass(settings.label.variation);
  2760. }
  2761. if(shouldAnimate === true) {
  2762. module.debug('Animating in label', $label);
  2763. $label
  2764. .addClass(className.hidden)
  2765. .insertBefore($next)
  2766. .transition({
  2767. animation : settings.label.transition,
  2768. debug : settings.debug,
  2769. verbose : settings.verbose,
  2770. duration : settings.label.duration
  2771. })
  2772. ;
  2773. }
  2774. else {
  2775. module.debug('Adding selection label', $label);
  2776. $label
  2777. .insertBefore($next)
  2778. ;
  2779. }
  2780. },
  2781. message: function(message) {
  2782. var
  2783. $message = $menu.children(selector.message),
  2784. html = settings.templates.message(module.add.variables(message))
  2785. ;
  2786. if($message.length > 0) {
  2787. $message
  2788. .html(html)
  2789. ;
  2790. }
  2791. else {
  2792. $message = $('<div/>')
  2793. .html(html)
  2794. .addClass(className.message)
  2795. .appendTo($menu)
  2796. ;
  2797. }
  2798. },
  2799. optionValue: function(value) {
  2800. var
  2801. escapedValue = module.escape.value(value),
  2802. $option = $input.find('option[value="' + module.escape.string(escapedValue) + '"]'),
  2803. hasOption = ($option.length > 0)
  2804. ;
  2805. if(hasOption) {
  2806. return;
  2807. }
  2808. // temporarily disconnect observer
  2809. module.disconnect.selectObserver();
  2810. if( module.is.single() ) {
  2811. module.verbose('Removing previous user addition');
  2812. $input.find('option.' + className.addition).remove();
  2813. }
  2814. $('<option/>')
  2815. .prop('value', escapedValue)
  2816. .addClass(className.addition)
  2817. .html(value)
  2818. .appendTo($input)
  2819. ;
  2820. module.verbose('Adding user addition as an <option>', value);
  2821. module.observe.select();
  2822. },
  2823. userSuggestion: function(value) {
  2824. var
  2825. $addition = $menu.children(selector.addition),
  2826. $existingItem = module.get.item(value),
  2827. alreadyHasValue = $existingItem && $existingItem.not(selector.addition).length,
  2828. hasUserSuggestion = $addition.length > 0,
  2829. html
  2830. ;
  2831. if(settings.useLabels && module.has.maxSelections()) {
  2832. return;
  2833. }
  2834. if(value === '' || alreadyHasValue) {
  2835. $addition.remove();
  2836. return;
  2837. }
  2838. if(hasUserSuggestion) {
  2839. $addition
  2840. .data(metadata.value, value)
  2841. .data(metadata.text, value)
  2842. .attr('data-' + metadata.value, value)
  2843. .attr('data-' + metadata.text, value)
  2844. .removeClass(className.filtered)
  2845. ;
  2846. if(!settings.hideAdditions) {
  2847. html = settings.templates.addition( module.add.variables(message.addResult, value) );
  2848. $addition
  2849. .html(html)
  2850. ;
  2851. }
  2852. module.verbose('Replacing user suggestion with new value', $addition);
  2853. }
  2854. else {
  2855. $addition = module.create.userChoice(value);
  2856. $addition
  2857. .prependTo($menu)
  2858. ;
  2859. module.verbose('Adding item choice to menu corresponding with user choice addition', $addition);
  2860. }
  2861. if(!settings.hideAdditions || module.is.allFiltered()) {
  2862. $addition
  2863. .addClass(className.selected)
  2864. .siblings()
  2865. .removeClass(className.selected)
  2866. ;
  2867. }
  2868. module.refreshItems();
  2869. },
  2870. variables: function(message, term) {
  2871. var
  2872. hasCount = (message.search('{count}') !== -1),
  2873. hasMaxCount = (message.search('{maxCount}') !== -1),
  2874. hasTerm = (message.search('{term}') !== -1),
  2875. count,
  2876. query
  2877. ;
  2878. module.verbose('Adding templated variables to message', message);
  2879. if(hasCount) {
  2880. count = module.get.selectionCount();
  2881. message = message.replace('{count}', count);
  2882. }
  2883. if(hasMaxCount) {
  2884. count = module.get.selectionCount();
  2885. message = message.replace('{maxCount}', settings.maxSelections);
  2886. }
  2887. if(hasTerm) {
  2888. query = term || module.get.query();
  2889. message = message.replace('{term}', query);
  2890. }
  2891. return message;
  2892. },
  2893. value: function(addedValue, addedText, $selectedItem) {
  2894. var
  2895. currentValue = module.get.values(),
  2896. newValue
  2897. ;
  2898. if(module.has.value(addedValue)) {
  2899. module.debug('Value already selected');
  2900. return;
  2901. }
  2902. if(addedValue === '') {
  2903. module.debug('Cannot select blank values from multiselect');
  2904. return;
  2905. }
  2906. // extend current array
  2907. if(Array.isArray(currentValue)) {
  2908. newValue = currentValue.concat([addedValue]);
  2909. newValue = module.get.uniqueArray(newValue);
  2910. }
  2911. else {
  2912. newValue = [addedValue];
  2913. }
  2914. // add values
  2915. if( module.has.selectInput() ) {
  2916. if(module.can.extendSelect()) {
  2917. module.debug('Adding value to select', addedValue, newValue, $input);
  2918. module.add.optionValue(addedValue);
  2919. }
  2920. }
  2921. else {
  2922. newValue = newValue.join(settings.delimiter);
  2923. module.debug('Setting hidden input to delimited value', newValue, $input);
  2924. }
  2925. if(settings.fireOnInit === false && module.is.initialLoad()) {
  2926. module.verbose('Skipping onadd callback on initial load', settings.onAdd);
  2927. }
  2928. else {
  2929. settings.onAdd.call(element, addedValue, addedText, $selectedItem);
  2930. }
  2931. module.set.value(newValue, addedText, $selectedItem);
  2932. module.check.maxSelections();
  2933. },
  2934. },
  2935. remove: {
  2936. active: function() {
  2937. $module.removeClass(className.active);
  2938. },
  2939. activeLabel: function() {
  2940. $module.find(selector.label).removeClass(className.active);
  2941. },
  2942. empty: function() {
  2943. $module.removeClass(className.empty);
  2944. },
  2945. loading: function() {
  2946. $module.removeClass(className.loading);
  2947. },
  2948. initialLoad: function() {
  2949. initialLoad = false;
  2950. },
  2951. upward: function($currentMenu) {
  2952. var $element = $currentMenu || $module;
  2953. $element.removeClass(className.upward);
  2954. },
  2955. leftward: function($currentMenu) {
  2956. var $element = $currentMenu || $menu;
  2957. $element.removeClass(className.leftward);
  2958. },
  2959. visible: function() {
  2960. $module.removeClass(className.visible);
  2961. },
  2962. activeItem: function() {
  2963. $item.removeClass(className.active);
  2964. },
  2965. filteredItem: function() {
  2966. if(settings.useLabels && module.has.maxSelections() ) {
  2967. return;
  2968. }
  2969. if(settings.useLabels && module.is.multiple()) {
  2970. $item.not('.' + className.active).removeClass(className.filtered);
  2971. }
  2972. else {
  2973. $item.removeClass(className.filtered);
  2974. }
  2975. if(settings.hideDividers) {
  2976. $divider.removeClass(className.hidden);
  2977. }
  2978. module.remove.empty();
  2979. },
  2980. optionValue: function(value) {
  2981. var
  2982. escapedValue = module.escape.value(value),
  2983. $option = $input.find('option[value="' + module.escape.string(escapedValue) + '"]'),
  2984. hasOption = ($option.length > 0)
  2985. ;
  2986. if(!hasOption || !$option.hasClass(className.addition)) {
  2987. return;
  2988. }
  2989. // temporarily disconnect observer
  2990. if(selectObserver) {
  2991. selectObserver.disconnect();
  2992. module.verbose('Temporarily disconnecting mutation observer');
  2993. }
  2994. $option.remove();
  2995. module.verbose('Removing user addition as an <option>', escapedValue);
  2996. if(selectObserver) {
  2997. selectObserver.observe($input[0], {
  2998. childList : true,
  2999. subtree : true
  3000. });
  3001. }
  3002. },
  3003. message: function() {
  3004. $menu.children(selector.message).remove();
  3005. },
  3006. searchWidth: function() {
  3007. $search.css('width', '');
  3008. },
  3009. searchTerm: function() {
  3010. module.verbose('Cleared search term');
  3011. $search.val('');
  3012. module.set.filtered();
  3013. },
  3014. userAddition: function() {
  3015. $item.filter(selector.addition).remove();
  3016. },
  3017. selected: function(value, $selectedItem) {
  3018. $selectedItem = (settings.allowAdditions)
  3019. ? $selectedItem || module.get.itemWithAdditions(value)
  3020. : $selectedItem || module.get.item(value)
  3021. ;
  3022. if(!$selectedItem) {
  3023. return false;
  3024. }
  3025. $selectedItem
  3026. .each(function() {
  3027. var
  3028. $selected = $(this),
  3029. selectedText = module.get.choiceText($selected),
  3030. selectedValue = module.get.choiceValue($selected, selectedText)
  3031. ;
  3032. if(module.is.multiple()) {
  3033. if(settings.useLabels) {
  3034. module.remove.value(selectedValue, selectedText, $selected);
  3035. module.remove.label(selectedValue);
  3036. }
  3037. else {
  3038. module.remove.value(selectedValue, selectedText, $selected);
  3039. if(module.get.selectionCount() === 0) {
  3040. module.set.placeholderText();
  3041. }
  3042. else {
  3043. module.set.text(module.add.variables(message.count));
  3044. }
  3045. }
  3046. }
  3047. else {
  3048. module.remove.value(selectedValue, selectedText, $selected);
  3049. }
  3050. $selected
  3051. .removeClass(className.filtered)
  3052. .removeClass(className.active)
  3053. ;
  3054. if(settings.useLabels) {
  3055. $selected.removeClass(className.selected);
  3056. }
  3057. })
  3058. ;
  3059. },
  3060. selectedItem: function() {
  3061. $item.removeClass(className.selected);
  3062. },
  3063. value: function(removedValue, removedText, $removedItem) {
  3064. var
  3065. values = module.get.values(),
  3066. newValue
  3067. ;
  3068. if( module.has.selectInput() ) {
  3069. module.verbose('Input is <select> removing selected option', removedValue);
  3070. newValue = module.remove.arrayValue(removedValue, values);
  3071. module.remove.optionValue(removedValue);
  3072. }
  3073. else {
  3074. module.verbose('Removing from delimited values', removedValue);
  3075. newValue = module.remove.arrayValue(removedValue, values);
  3076. newValue = newValue.join(settings.delimiter);
  3077. }
  3078. if(settings.fireOnInit === false && module.is.initialLoad()) {
  3079. module.verbose('No callback on initial load', settings.onRemove);
  3080. }
  3081. else {
  3082. settings.onRemove.call(element, removedValue, removedText, $removedItem);
  3083. }
  3084. module.set.value(newValue, removedText, $removedItem);
  3085. module.check.maxSelections();
  3086. },
  3087. arrayValue: function(removedValue, values) {
  3088. if( !Array.isArray(values) ) {
  3089. values = [values];
  3090. }
  3091. values = $.grep(values, function(value){
  3092. return (removedValue != value);
  3093. });
  3094. module.verbose('Removed value from delimited string', removedValue, values);
  3095. return values;
  3096. },
  3097. label: function(value, shouldAnimate) {
  3098. var
  3099. $labels = $module.find(selector.label),
  3100. $removedLabel = $labels.filter('[data-' + metadata.value + '="' + module.escape.string(settings.ignoreCase ? value.toLowerCase() : value) +'"]')
  3101. ;
  3102. module.verbose('Removing label', $removedLabel);
  3103. $removedLabel.remove();
  3104. },
  3105. activeLabels: function($activeLabels) {
  3106. $activeLabels = $activeLabels || $module.find(selector.label).filter('.' + className.active);
  3107. module.verbose('Removing active label selections', $activeLabels);
  3108. module.remove.labels($activeLabels);
  3109. },
  3110. labels: function($labels) {
  3111. $labels = $labels || $module.find(selector.label);
  3112. module.verbose('Removing labels', $labels);
  3113. $labels
  3114. .each(function(){
  3115. var
  3116. $label = $(this),
  3117. value = $label.data(metadata.value),
  3118. stringValue = (value !== undefined)
  3119. ? String(value)
  3120. : value,
  3121. isUserValue = module.is.userValue(stringValue)
  3122. ;
  3123. if(settings.onLabelRemove.call($label, value) === false) {
  3124. module.debug('Label remove callback cancelled removal');
  3125. return;
  3126. }
  3127. module.remove.message();
  3128. if(isUserValue) {
  3129. module.remove.value(stringValue);
  3130. module.remove.label(stringValue);
  3131. }
  3132. else {
  3133. // selected will also remove label
  3134. module.remove.selected(stringValue);
  3135. }
  3136. })
  3137. ;
  3138. },
  3139. tabbable: function() {
  3140. if( module.is.searchSelection() ) {
  3141. module.debug('Searchable dropdown initialized');
  3142. $search
  3143. .removeAttr('tabindex')
  3144. ;
  3145. $menu
  3146. .removeAttr('tabindex')
  3147. ;
  3148. }
  3149. else {
  3150. module.debug('Simple selection dropdown initialized');
  3151. $module
  3152. .removeAttr('tabindex')
  3153. ;
  3154. $menu
  3155. .removeAttr('tabindex')
  3156. ;
  3157. }
  3158. },
  3159. diacritics: function(text) {
  3160. return settings.ignoreDiacritics ? text.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : text;
  3161. }
  3162. },
  3163. has: {
  3164. menuSearch: function() {
  3165. return (module.has.search() && $search.closest($menu).length > 0);
  3166. },
  3167. clearItem: function() {
  3168. return ($clear.length > 0);
  3169. },
  3170. search: function() {
  3171. return ($search.length > 0);
  3172. },
  3173. sizer: function() {
  3174. return ($sizer.length > 0);
  3175. },
  3176. selectInput: function() {
  3177. return ( $input.is('select') );
  3178. },
  3179. minCharacters: function(searchTerm) {
  3180. if(settings.minCharacters && !iconClicked) {
  3181. searchTerm = (searchTerm !== undefined)
  3182. ? String(searchTerm)
  3183. : String(module.get.query())
  3184. ;
  3185. return (searchTerm.length >= settings.minCharacters);
  3186. }
  3187. iconClicked=false;
  3188. return true;
  3189. },
  3190. firstLetter: function($item, letter) {
  3191. var
  3192. text,
  3193. firstLetter
  3194. ;
  3195. if(!$item || $item.length === 0 || typeof letter !== 'string') {
  3196. return false;
  3197. }
  3198. text = module.get.choiceText($item, false);
  3199. letter = letter.toLowerCase();
  3200. firstLetter = String(text).charAt(0).toLowerCase();
  3201. return (letter == firstLetter);
  3202. },
  3203. input: function() {
  3204. return ($input.length > 0);
  3205. },
  3206. items: function() {
  3207. return ($item.length > 0);
  3208. },
  3209. menu: function() {
  3210. return ($menu.length > 0);
  3211. },
  3212. message: function() {
  3213. return ($menu.children(selector.message).length !== 0);
  3214. },
  3215. label: function(value) {
  3216. var
  3217. escapedValue = module.escape.value(value),
  3218. $labels = $module.find(selector.label)
  3219. ;
  3220. if(settings.ignoreCase) {
  3221. escapedValue = escapedValue.toLowerCase();
  3222. }
  3223. return ($labels.filter('[data-' + metadata.value + '="' + module.escape.string(escapedValue) +'"]').length > 0);
  3224. },
  3225. maxSelections: function() {
  3226. return (settings.maxSelections && module.get.selectionCount() >= settings.maxSelections);
  3227. },
  3228. allResultsFiltered: function() {
  3229. var
  3230. $normalResults = $item.not(selector.addition)
  3231. ;
  3232. return ($normalResults.filter(selector.unselectable).length === $normalResults.length);
  3233. },
  3234. userSuggestion: function() {
  3235. return ($menu.children(selector.addition).length > 0);
  3236. },
  3237. query: function() {
  3238. return (module.get.query() !== '');
  3239. },
  3240. value: function(value) {
  3241. return (settings.ignoreCase)
  3242. ? module.has.valueIgnoringCase(value)
  3243. : module.has.valueMatchingCase(value)
  3244. ;
  3245. },
  3246. valueMatchingCase: function(value) {
  3247. var
  3248. values = module.get.values(),
  3249. hasValue = Array.isArray(values)
  3250. ? values && ($.inArray(value, values) !== -1)
  3251. : (values == value)
  3252. ;
  3253. return (hasValue)
  3254. ? true
  3255. : false
  3256. ;
  3257. },
  3258. valueIgnoringCase: function(value) {
  3259. var
  3260. values = module.get.values(),
  3261. hasValue = false
  3262. ;
  3263. if(!Array.isArray(values)) {
  3264. values = [values];
  3265. }
  3266. $.each(values, function(index, existingValue) {
  3267. if(String(value).toLowerCase() == String(existingValue).toLowerCase()) {
  3268. hasValue = true;
  3269. return false;
  3270. }
  3271. });
  3272. return hasValue;
  3273. }
  3274. },
  3275. is: {
  3276. active: function() {
  3277. return $module.hasClass(className.active);
  3278. },
  3279. animatingInward: function() {
  3280. return $menu.transition('is inward');
  3281. },
  3282. animatingOutward: function() {
  3283. return $menu.transition('is outward');
  3284. },
  3285. bubbledLabelClick: function(event) {
  3286. return $(event.target).is('select, input') && $module.closest('label').length > 0;
  3287. },
  3288. bubbledIconClick: function(event) {
  3289. return $(event.target).closest($icon).length > 0;
  3290. },
  3291. alreadySetup: function() {
  3292. return ($module.is('select') && $module.parent(selector.dropdown).data(moduleNamespace) !== undefined && $module.prev().length === 0);
  3293. },
  3294. animating: function($subMenu) {
  3295. return ($subMenu)
  3296. ? $subMenu.transition && $subMenu.transition('is animating')
  3297. : $menu.transition && $menu.transition('is animating')
  3298. ;
  3299. },
  3300. leftward: function($subMenu) {
  3301. var $selectedMenu = $subMenu || $menu;
  3302. return $selectedMenu.hasClass(className.leftward);
  3303. },
  3304. clearable: function() {
  3305. return ($module.hasClass(className.clearable) || settings.clearable);
  3306. },
  3307. disabled: function() {
  3308. return $module.hasClass(className.disabled);
  3309. },
  3310. focused: function() {
  3311. return (document.activeElement === $module[0]);
  3312. },
  3313. focusedOnSearch: function() {
  3314. return (document.activeElement === $search[0]);
  3315. },
  3316. allFiltered: function() {
  3317. return( (module.is.multiple() || module.has.search()) && !(settings.hideAdditions == false && module.has.userSuggestion()) && !module.has.message() && module.has.allResultsFiltered() );
  3318. },
  3319. hidden: function($subMenu) {
  3320. return !module.is.visible($subMenu);
  3321. },
  3322. initialLoad: function() {
  3323. return initialLoad;
  3324. },
  3325. inObject: function(needle, object) {
  3326. var
  3327. found = false
  3328. ;
  3329. $.each(object, function(index, property) {
  3330. if(property == needle) {
  3331. found = true;
  3332. return true;
  3333. }
  3334. });
  3335. return found;
  3336. },
  3337. multiple: function() {
  3338. return $module.hasClass(className.multiple);
  3339. },
  3340. remote: function() {
  3341. return settings.apiSettings && module.can.useAPI();
  3342. },
  3343. single: function() {
  3344. return !module.is.multiple();
  3345. },
  3346. selectMutation: function(mutations) {
  3347. var
  3348. selectChanged = false
  3349. ;
  3350. $.each(mutations, function(index, mutation) {
  3351. if($(mutation.target).is('select') || $(mutation.addedNodes).is('select')) {
  3352. selectChanged = true;
  3353. return false;
  3354. }
  3355. });
  3356. return selectChanged;
  3357. },
  3358. search: function() {
  3359. return $module.hasClass(className.search);
  3360. },
  3361. searchSelection: function() {
  3362. return ( module.has.search() && $search.parent(selector.dropdown).length === 1 );
  3363. },
  3364. selection: function() {
  3365. return $module.hasClass(className.selection);
  3366. },
  3367. userValue: function(value) {
  3368. return ($.inArray(value, module.get.userValues()) !== -1);
  3369. },
  3370. upward: function($menu) {
  3371. var $element = $menu || $module;
  3372. return $element.hasClass(className.upward);
  3373. },
  3374. visible: function($subMenu) {
  3375. return ($subMenu)
  3376. ? $subMenu.hasClass(className.visible)
  3377. : $menu.hasClass(className.visible)
  3378. ;
  3379. },
  3380. verticallyScrollableContext: function() {
  3381. var
  3382. overflowY = ($context.get(0) !== window)
  3383. ? $context.css('overflow-y')
  3384. : false
  3385. ;
  3386. return (overflowY == 'auto' || overflowY == 'scroll');
  3387. },
  3388. horizontallyScrollableContext: function() {
  3389. var
  3390. overflowX = ($context.get(0) !== window)
  3391. ? $context.css('overflow-X')
  3392. : false
  3393. ;
  3394. return (overflowX == 'auto' || overflowX == 'scroll');
  3395. }
  3396. },
  3397. can: {
  3398. activate: function($item) {
  3399. if(settings.useLabels) {
  3400. return true;
  3401. }
  3402. if(!module.has.maxSelections()) {
  3403. return true;
  3404. }
  3405. if(module.has.maxSelections() && $item.hasClass(className.active)) {
  3406. return true;
  3407. }
  3408. return false;
  3409. },
  3410. openDownward: function($subMenu) {
  3411. var
  3412. $currentMenu = $subMenu || $menu,
  3413. canOpenDownward = true,
  3414. onScreen = {},
  3415. calculations
  3416. ;
  3417. $currentMenu
  3418. .addClass(className.loading)
  3419. ;
  3420. calculations = {
  3421. context: {
  3422. offset : ($context.get(0) === window)
  3423. ? { top: 0, left: 0}
  3424. : $context.offset(),
  3425. scrollTop : $context.scrollTop(),
  3426. height : $context.outerHeight()
  3427. },
  3428. menu : {
  3429. offset: $currentMenu.offset(),
  3430. height: $currentMenu.outerHeight()
  3431. }
  3432. };
  3433. if(module.is.verticallyScrollableContext()) {
  3434. calculations.menu.offset.top += calculations.context.scrollTop;
  3435. }
  3436. onScreen = {
  3437. above : (calculations.context.scrollTop) <= calculations.menu.offset.top - calculations.context.offset.top - calculations.menu.height,
  3438. below : (calculations.context.scrollTop + calculations.context.height) >= calculations.menu.offset.top - calculations.context.offset.top + calculations.menu.height
  3439. };
  3440. if(onScreen.below) {
  3441. module.verbose('Dropdown can fit in context downward', onScreen);
  3442. canOpenDownward = true;
  3443. }
  3444. else if(!onScreen.below && !onScreen.above) {
  3445. module.verbose('Dropdown cannot fit in either direction, favoring downward', onScreen);
  3446. canOpenDownward = true;
  3447. }
  3448. else {
  3449. module.verbose('Dropdown cannot fit below, opening upward', onScreen);
  3450. canOpenDownward = false;
  3451. }
  3452. $currentMenu.removeClass(className.loading);
  3453. return canOpenDownward;
  3454. },
  3455. openRightward: function($subMenu) {
  3456. var
  3457. $currentMenu = $subMenu || $menu,
  3458. canOpenRightward = true,
  3459. isOffscreenRight = false,
  3460. calculations
  3461. ;
  3462. $currentMenu
  3463. .addClass(className.loading)
  3464. ;
  3465. calculations = {
  3466. context: {
  3467. offset : ($context.get(0) === window)
  3468. ? { top: 0, left: 0}
  3469. : $context.offset(),
  3470. scrollLeft : $context.scrollLeft(),
  3471. width : $context.outerWidth()
  3472. },
  3473. menu: {
  3474. offset : $currentMenu.offset(),
  3475. width : $currentMenu.outerWidth()
  3476. }
  3477. };
  3478. if(module.is.horizontallyScrollableContext()) {
  3479. calculations.menu.offset.left += calculations.context.scrollLeft;
  3480. }
  3481. isOffscreenRight = (calculations.menu.offset.left - calculations.context.offset.left + calculations.menu.width >= calculations.context.scrollLeft + calculations.context.width);
  3482. if(isOffscreenRight) {
  3483. module.verbose('Dropdown cannot fit in context rightward', isOffscreenRight);
  3484. canOpenRightward = false;
  3485. }
  3486. $currentMenu.removeClass(className.loading);
  3487. return canOpenRightward;
  3488. },
  3489. click: function() {
  3490. return (hasTouch || settings.on == 'click');
  3491. },
  3492. extendSelect: function() {
  3493. return settings.allowAdditions || settings.apiSettings;
  3494. },
  3495. show: function() {
  3496. return !module.is.disabled() && (module.has.items() || module.has.message());
  3497. },
  3498. useAPI: function() {
  3499. return $.fn.api !== undefined;
  3500. }
  3501. },
  3502. animate: {
  3503. show: function(callback, $subMenu) {
  3504. var
  3505. $currentMenu = $subMenu || $menu,
  3506. start = ($subMenu)
  3507. ? function() {}
  3508. : function() {
  3509. module.hideSubMenus();
  3510. module.hideOthers();
  3511. module.set.active();
  3512. },
  3513. transition
  3514. ;
  3515. callback = $.isFunction(callback)
  3516. ? callback
  3517. : function(){}
  3518. ;
  3519. module.verbose('Doing menu show animation', $currentMenu);
  3520. module.set.direction($subMenu);
  3521. transition = module.get.transition($subMenu);
  3522. if( module.is.selection() ) {
  3523. module.set.scrollPosition(module.get.selectedItem(), true);
  3524. }
  3525. if( module.is.hidden($currentMenu) || module.is.animating($currentMenu) ) {
  3526. if(transition == 'none') {
  3527. start();
  3528. $currentMenu.transition('show');
  3529. callback.call(element);
  3530. }
  3531. else if($.fn.transition !== undefined && $module.transition('is supported')) {
  3532. $currentMenu
  3533. .transition({
  3534. animation : transition + ' in',
  3535. debug : settings.debug,
  3536. verbose : settings.verbose,
  3537. duration : settings.duration,
  3538. queue : true,
  3539. onStart : start,
  3540. onComplete : function() {
  3541. callback.call(element);
  3542. }
  3543. })
  3544. ;
  3545. }
  3546. else {
  3547. module.error(error.noTransition, transition);
  3548. }
  3549. }
  3550. },
  3551. hide: function(callback, $subMenu) {
  3552. var
  3553. $currentMenu = $subMenu || $menu,
  3554. start = ($subMenu)
  3555. ? function() {}
  3556. : function() {
  3557. if( module.can.click() ) {
  3558. module.unbind.intent();
  3559. }
  3560. module.remove.active();
  3561. },
  3562. transition = module.get.transition($subMenu)
  3563. ;
  3564. callback = $.isFunction(callback)
  3565. ? callback
  3566. : function(){}
  3567. ;
  3568. if( module.is.visible($currentMenu) || module.is.animating($currentMenu) ) {
  3569. module.verbose('Doing menu hide animation', $currentMenu);
  3570. if(transition == 'none') {
  3571. start();
  3572. $currentMenu.transition('hide');
  3573. callback.call(element);
  3574. }
  3575. else if($.fn.transition !== undefined && $module.transition('is supported')) {
  3576. $currentMenu
  3577. .transition({
  3578. animation : transition + ' out',
  3579. duration : settings.duration,
  3580. debug : settings.debug,
  3581. verbose : settings.verbose,
  3582. queue : false,
  3583. onStart : start,
  3584. onComplete : function() {
  3585. callback.call(element);
  3586. }
  3587. })
  3588. ;
  3589. }
  3590. else {
  3591. module.error(error.transition);
  3592. }
  3593. }
  3594. }
  3595. },
  3596. hideAndClear: function() {
  3597. module.remove.searchTerm();
  3598. if( module.has.maxSelections() ) {
  3599. return;
  3600. }
  3601. if(module.has.search()) {
  3602. module.hide(function() {
  3603. module.remove.filteredItem();
  3604. });
  3605. }
  3606. else {
  3607. module.hide();
  3608. }
  3609. },
  3610. delay: {
  3611. show: function() {
  3612. module.verbose('Delaying show event to ensure user intent');
  3613. clearTimeout(module.timer);
  3614. module.timer = setTimeout(module.show, settings.delay.show);
  3615. },
  3616. hide: function() {
  3617. module.verbose('Delaying hide event to ensure user intent');
  3618. clearTimeout(module.timer);
  3619. module.timer = setTimeout(module.hide, settings.delay.hide);
  3620. }
  3621. },
  3622. escape: {
  3623. value: function(value) {
  3624. var
  3625. multipleValues = Array.isArray(value),
  3626. stringValue = (typeof value === 'string'),
  3627. isUnparsable = (!stringValue && !multipleValues),
  3628. hasQuotes = (stringValue && value.search(regExp.quote) !== -1),
  3629. values = []
  3630. ;
  3631. if(isUnparsable || !hasQuotes) {
  3632. return value;
  3633. }
  3634. module.debug('Encoding quote values for use in select', value);
  3635. if(multipleValues) {
  3636. $.each(value, function(index, value){
  3637. values.push(value.replace(regExp.quote, '&quot;'));
  3638. });
  3639. return values;
  3640. }
  3641. return value.replace(regExp.quote, '&quot;');
  3642. },
  3643. string: function(text) {
  3644. text = String(text);
  3645. return text.replace(regExp.escape, '\\$&');
  3646. },
  3647. htmlEntities: function(string) {
  3648. var
  3649. badChars = /[&<>"'`]/g,
  3650. shouldEscape = /[&<>"'`]/,
  3651. escape = {
  3652. "&": "&amp;",
  3653. "<": "&lt;",
  3654. ">": "&gt;",
  3655. '"': "&quot;",
  3656. "'": "&#x27;",
  3657. "`": "&#x60;"
  3658. },
  3659. escapedChar = function(chr) {
  3660. return escape[chr];
  3661. }
  3662. ;
  3663. if(shouldEscape.test(string)) {
  3664. return string.replace(badChars, escapedChar);
  3665. }
  3666. return string;
  3667. }
  3668. },
  3669. setting: function(name, value) {
  3670. module.debug('Changing setting', name, value);
  3671. if( $.isPlainObject(name) ) {
  3672. $.extend(true, settings, name);
  3673. }
  3674. else if(value !== undefined) {
  3675. if($.isPlainObject(settings[name])) {
  3676. $.extend(true, settings[name], value);
  3677. }
  3678. else {
  3679. settings[name] = value;
  3680. }
  3681. }
  3682. else {
  3683. return settings[name];
  3684. }
  3685. },
  3686. internal: function(name, value) {
  3687. if( $.isPlainObject(name) ) {
  3688. $.extend(true, module, name);
  3689. }
  3690. else if(value !== undefined) {
  3691. module[name] = value;
  3692. }
  3693. else {
  3694. return module[name];
  3695. }
  3696. },
  3697. debug: function() {
  3698. if(!settings.silent && settings.debug) {
  3699. if(settings.performance) {
  3700. module.performance.log(arguments);
  3701. }
  3702. else {
  3703. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  3704. module.debug.apply(console, arguments);
  3705. }
  3706. }
  3707. },
  3708. verbose: function() {
  3709. if(!settings.silent && settings.verbose && settings.debug) {
  3710. if(settings.performance) {
  3711. module.performance.log(arguments);
  3712. }
  3713. else {
  3714. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  3715. module.verbose.apply(console, arguments);
  3716. }
  3717. }
  3718. },
  3719. error: function() {
  3720. if(!settings.silent) {
  3721. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  3722. module.error.apply(console, arguments);
  3723. }
  3724. },
  3725. performance: {
  3726. log: function(message) {
  3727. var
  3728. currentTime,
  3729. executionTime,
  3730. previousTime
  3731. ;
  3732. if(settings.performance) {
  3733. currentTime = new Date().getTime();
  3734. previousTime = time || currentTime;
  3735. executionTime = currentTime - previousTime;
  3736. time = currentTime;
  3737. performance.push({
  3738. 'Name' : message[0],
  3739. 'Arguments' : [].slice.call(message, 1) || '',
  3740. 'Element' : element,
  3741. 'Execution Time' : executionTime
  3742. });
  3743. }
  3744. clearTimeout(module.performance.timer);
  3745. module.performance.timer = setTimeout(module.performance.display, 500);
  3746. },
  3747. display: function() {
  3748. var
  3749. title = settings.name + ':',
  3750. totalTime = 0
  3751. ;
  3752. time = false;
  3753. clearTimeout(module.performance.timer);
  3754. $.each(performance, function(index, data) {
  3755. totalTime += data['Execution Time'];
  3756. });
  3757. title += ' ' + totalTime + 'ms';
  3758. if(moduleSelector) {
  3759. title += ' \'' + moduleSelector + '\'';
  3760. }
  3761. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  3762. console.groupCollapsed(title);
  3763. if(console.table) {
  3764. console.table(performance);
  3765. }
  3766. else {
  3767. $.each(performance, function(index, data) {
  3768. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  3769. });
  3770. }
  3771. console.groupEnd();
  3772. }
  3773. performance = [];
  3774. }
  3775. },
  3776. invoke: function(query, passedArguments, context) {
  3777. var
  3778. object = instance,
  3779. maxDepth,
  3780. found,
  3781. response
  3782. ;
  3783. passedArguments = passedArguments || queryArguments;
  3784. context = element || context;
  3785. if(typeof query == 'string' && object !== undefined) {
  3786. query = query.split(/[\. ]/);
  3787. maxDepth = query.length - 1;
  3788. $.each(query, function(depth, value) {
  3789. var camelCaseValue = (depth != maxDepth)
  3790. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  3791. : query
  3792. ;
  3793. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  3794. object = object[camelCaseValue];
  3795. }
  3796. else if( object[camelCaseValue] !== undefined ) {
  3797. found = object[camelCaseValue];
  3798. return false;
  3799. }
  3800. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  3801. object = object[value];
  3802. }
  3803. else if( object[value] !== undefined ) {
  3804. found = object[value];
  3805. return false;
  3806. }
  3807. else {
  3808. module.error(error.method, query);
  3809. return false;
  3810. }
  3811. });
  3812. }
  3813. if ( $.isFunction( found ) ) {
  3814. response = found.apply(context, passedArguments);
  3815. }
  3816. else if(found !== undefined) {
  3817. response = found;
  3818. }
  3819. if(Array.isArray(returnedValue)) {
  3820. returnedValue.push(response);
  3821. }
  3822. else if(returnedValue !== undefined) {
  3823. returnedValue = [returnedValue, response];
  3824. }
  3825. else if(response !== undefined) {
  3826. returnedValue = response;
  3827. }
  3828. return found;
  3829. }
  3830. };
  3831. if(methodInvoked) {
  3832. if(instance === undefined) {
  3833. module.initialize();
  3834. }
  3835. module.invoke(query);
  3836. }
  3837. else {
  3838. if(instance !== undefined) {
  3839. instance.invoke('destroy');
  3840. }
  3841. module.initialize();
  3842. }
  3843. })
  3844. ;
  3845. return (returnedValue !== undefined)
  3846. ? returnedValue
  3847. : $allModules
  3848. ;
  3849. };
  3850. $.fn.dropdown.settings = {
  3851. silent : false,
  3852. debug : false,
  3853. verbose : false,
  3854. performance : true,
  3855. on : 'click', // what event should show menu action on item selection
  3856. action : 'activate', // action on item selection (nothing, activate, select, combo, hide, function(){})
  3857. values : false, // specify values to use for dropdown
  3858. clearable : false, // whether the value of the dropdown can be cleared
  3859. apiSettings : false,
  3860. selectOnKeydown : true, // Whether selection should occur automatically when keyboard shortcuts used
  3861. minCharacters : 0, // Minimum characters required to trigger API call
  3862. filterRemoteData : false, // Whether API results should be filtered after being returned for query term
  3863. saveRemoteData : true, // Whether remote name/value pairs should be stored in sessionStorage to allow remote data to be restored on page refresh
  3864. throttle : 200, // How long to wait after last user input to search remotely
  3865. context : window, // Context to use when determining if on screen
  3866. direction : 'auto', // Whether dropdown should always open in one direction
  3867. keepOnScreen : true, // Whether dropdown should check whether it is on screen before showing
  3868. match : 'both', // what to match against with search selection (both, text, or label)
  3869. fullTextSearch : false, // search anywhere in value (set to 'exact' to require exact matches)
  3870. ignoreDiacritics : false, // match results also if they contain diacritics of the same base character (for example searching for "a" will also match "á" or "â" or "à", etc...)
  3871. hideDividers : false, // Whether to hide any divider elements (specified in selector.divider) that are sibling to any items when searched (set to true will hide all dividers, set to 'empty' will hide them when they are not followed by a visible item)
  3872. placeholder : 'auto', // whether to convert blank <select> values to placeholder text
  3873. preserveHTML : true, // preserve html when selecting value
  3874. sortSelect : false, // sort selection on init
  3875. forceSelection : true, // force a choice on blur with search selection
  3876. allowAdditions : false, // whether multiple select should allow user added values
  3877. ignoreCase : false, // whether to consider case sensitivity when creating labels
  3878. ignoreSearchCase : true, // whether to consider case sensitivity when filtering items
  3879. hideAdditions : true, // whether or not to hide special message prompting a user they can enter a value
  3880. maxSelections : false, // When set to a number limits the number of selections to this count
  3881. useLabels : true, // whether multiple select should filter currently active selections from choices
  3882. delimiter : ',', // when multiselect uses normal <input> the values will be delimited with this character
  3883. showOnFocus : true, // show menu on focus
  3884. allowReselection : false, // whether current value should trigger callbacks when reselected
  3885. allowTab : true, // add tabindex to element
  3886. allowCategorySelection : false, // allow elements with sub-menus to be selected
  3887. fireOnInit : false, // Whether callbacks should fire when initializing dropdown values
  3888. transition : 'auto', // auto transition will slide down or up based on direction
  3889. duration : 200, // duration of transition
  3890. glyphWidth : 1.037, // widest glyph width in em (W is 1.037 em) used to calculate multiselect input width
  3891. headerDivider : true, // whether option headers should have an additional divider line underneath when converted from <select> <optgroup>
  3892. // label settings on multi-select
  3893. label: {
  3894. transition : 'scale',
  3895. duration : 200,
  3896. variation : false
  3897. },
  3898. // delay before event
  3899. delay : {
  3900. hide : 300,
  3901. show : 200,
  3902. search : 20,
  3903. touch : 50
  3904. },
  3905. /* Callbacks */
  3906. onChange : function(value, text, $selected){},
  3907. onAdd : function(value, text, $selected){},
  3908. onRemove : function(value, text, $selected){},
  3909. onLabelSelect : function($selectedLabels){},
  3910. onLabelCreate : function(value, text) { return $(this); },
  3911. onLabelRemove : function(value) { return true; },
  3912. onNoResults : function(searchTerm) { return true; },
  3913. onShow : function(){},
  3914. onHide : function(){},
  3915. /* Component */
  3916. name : 'Dropdown',
  3917. namespace : 'dropdown',
  3918. message: {
  3919. addResult : 'Add <b>{term}</b>',
  3920. count : '{count} selected',
  3921. maxSelections : 'Max {maxCount} selections',
  3922. noResults : 'No results found.',
  3923. serverError : 'There was an error contacting the server'
  3924. },
  3925. error : {
  3926. action : 'You called a dropdown action that was not defined',
  3927. alreadySetup : 'Once a select has been initialized behaviors must be called on the created ui dropdown',
  3928. labels : 'Allowing user additions currently requires the use of labels.',
  3929. missingMultiple : '<select> requires multiple property to be set to correctly preserve multiple values',
  3930. method : 'The method you called is not defined.',
  3931. noAPI : 'The API module is required to load resources remotely',
  3932. noStorage : 'Saving remote data requires session storage',
  3933. noTransition : 'This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>',
  3934. noNormalize : '"ignoreDiacritics" setting will be ignored. Browser does not support String().normalize(). You may consider including <https://cdn.jsdelivr.net/npm/unorm@1.4.1/lib/unorm.min.js> as a polyfill.'
  3935. },
  3936. regExp : {
  3937. escape : /[-[\]{}()*+?.,\\^$|#\s:=@]/g,
  3938. quote : /"/g
  3939. },
  3940. metadata : {
  3941. defaultText : 'defaultText',
  3942. defaultValue : 'defaultValue',
  3943. placeholderText : 'placeholder',
  3944. text : 'text',
  3945. value : 'value'
  3946. },
  3947. // property names for remote query
  3948. fields: {
  3949. remoteValues : 'results', // grouping for api results
  3950. values : 'values', // grouping for all dropdown values
  3951. disabled : 'disabled', // whether value should be disabled
  3952. name : 'name', // displayed dropdown text
  3953. value : 'value', // actual dropdown value
  3954. text : 'text', // displayed text when selected
  3955. type : 'type', // type of dropdown element
  3956. image : 'image', // optional image path
  3957. imageClass : 'imageClass', // optional individual class for image
  3958. icon : 'icon', // optional icon name
  3959. iconClass : 'iconClass', // optional individual class for icon (for example to use flag instead)
  3960. class : 'class', // optional individual class for item/header
  3961. divider : 'divider' // optional divider append for group headers
  3962. },
  3963. keys : {
  3964. backspace : 8,
  3965. delimiter : 188, // comma
  3966. deleteKey : 46,
  3967. enter : 13,
  3968. escape : 27,
  3969. pageUp : 33,
  3970. pageDown : 34,
  3971. leftArrow : 37,
  3972. upArrow : 38,
  3973. rightArrow : 39,
  3974. downArrow : 40
  3975. },
  3976. selector : {
  3977. addition : '.addition',
  3978. divider : '.divider, .header',
  3979. dropdown : '.ui.dropdown',
  3980. hidden : '.hidden',
  3981. icon : '> .dropdown.icon',
  3982. input : '> input[type="hidden"], > select',
  3983. item : '.item',
  3984. label : '> .label',
  3985. remove : '> .label > .delete.icon',
  3986. siblingLabel : '.label',
  3987. menu : '.menu',
  3988. message : '.message',
  3989. menuIcon : '.dropdown.icon',
  3990. search : 'input.search, .menu > .search > input, .menu input.search',
  3991. sizer : '> input.sizer',
  3992. text : '> .text:not(.icon)',
  3993. unselectable : '.disabled, .filtered',
  3994. clearIcon : '> .remove.icon'
  3995. },
  3996. className : {
  3997. active : 'active',
  3998. addition : 'addition',
  3999. animating : 'animating',
  4000. disabled : 'disabled',
  4001. empty : 'empty',
  4002. dropdown : 'ui dropdown',
  4003. filtered : 'filtered',
  4004. hidden : 'hidden transition',
  4005. icon : 'icon',
  4006. image : 'image',
  4007. item : 'item',
  4008. label : 'ui label',
  4009. loading : 'loading',
  4010. menu : 'menu',
  4011. message : 'message',
  4012. multiple : 'multiple',
  4013. placeholder : 'default',
  4014. sizer : 'sizer',
  4015. search : 'search',
  4016. selected : 'selected',
  4017. selection : 'selection',
  4018. upward : 'upward',
  4019. leftward : 'left',
  4020. visible : 'visible',
  4021. clearable : 'clearable',
  4022. noselection : 'noselection',
  4023. delete : 'delete',
  4024. header : 'header',
  4025. divider : 'divider',
  4026. groupIcon : ''
  4027. }
  4028. };
  4029. /* Templates */
  4030. $.fn.dropdown.settings.templates = {
  4031. deQuote: function(string) {
  4032. return String(string).replace(/"/g,"");
  4033. },
  4034. escape: function(string, preserveHTML) {
  4035. if (preserveHTML){
  4036. return string;
  4037. }
  4038. var
  4039. badChars = /[&<>"'`]/g,
  4040. shouldEscape = /[&<>"'`]/,
  4041. escape = {
  4042. "&": "&amp;",
  4043. "<": "&lt;",
  4044. ">": "&gt;",
  4045. '"': "&quot;",
  4046. "'": "&#x27;",
  4047. "`": "&#x60;"
  4048. },
  4049. escapedChar = function(chr) {
  4050. return escape[chr];
  4051. }
  4052. ;
  4053. if(shouldEscape.test(string)) {
  4054. return string.replace(badChars, escapedChar);
  4055. }
  4056. return string;
  4057. },
  4058. // generates dropdown from select values
  4059. dropdown: function(select, fields, preserveHTML, className) {
  4060. var
  4061. placeholder = select.placeholder || false,
  4062. html = '',
  4063. escape = $.fn.dropdown.settings.templates.escape
  4064. ;
  4065. html += '<i class="dropdown icon"></i>';
  4066. if(placeholder) {
  4067. html += '<div class="default text">' + escape(placeholder,preserveHTML) + '</div>';
  4068. }
  4069. else {
  4070. html += '<div class="text"></div>';
  4071. }
  4072. html += '<div class="'+className.menu+'">';
  4073. html += $.fn.dropdown.settings.templates.menu(select, fields, preserveHTML,className);
  4074. html += '</div>';
  4075. return html;
  4076. },
  4077. // generates just menu from select
  4078. menu: function(response, fields, preserveHTML, className) {
  4079. var
  4080. values = response[fields.values] || [],
  4081. html = '',
  4082. escape = $.fn.dropdown.settings.templates.escape,
  4083. deQuote = $.fn.dropdown.settings.templates.deQuote
  4084. ;
  4085. $.each(values, function(index, option) {
  4086. var
  4087. itemType = (option[fields.type])
  4088. ? option[fields.type]
  4089. : 'item'
  4090. ;
  4091. if( itemType === 'item' ) {
  4092. var
  4093. maybeText = (option[fields.text])
  4094. ? ' data-text="' + deQuote(option[fields.text]) + '"'
  4095. : '',
  4096. maybeDisabled = (option[fields.disabled])
  4097. ? className.disabled+' '
  4098. : ''
  4099. ;
  4100. html += '<div class="'+ maybeDisabled + (option[fields.class] ? deQuote(option[fields.class]) : className.item)+'" data-value="' + deQuote(option[fields.value]) + '"' + maybeText + '>';
  4101. if(option[fields.image]) {
  4102. html += '<img class="'+(option[fields.imageClass] ? deQuote(option[fields.imageClass]) : className.image)+'" src="' + deQuote(option[fields.image]) + '">';
  4103. }
  4104. if(option[fields.icon]) {
  4105. html += '<i class="'+deQuote(option[fields.icon])+' '+(option[fields.iconClass] ? deQuote(option[fields.iconClass]) : className.icon)+'"></i>';
  4106. }
  4107. html += escape(option[fields.name] || option[fields.value],preserveHTML);
  4108. html += '</div>';
  4109. } else if (itemType === 'header') {
  4110. var groupName = escape(option[fields.name],preserveHTML),
  4111. groupIcon = option[fields.icon] ? deQuote(option[fields.icon]) : className.groupIcon
  4112. ;
  4113. if(groupName !== '' || groupIcon !== '') {
  4114. html += '<div class="' + (option[fields.class] ? deQuote(option[fields.class]) : className.header) + '">';
  4115. if (groupIcon !== '') {
  4116. html += '<i class="' + groupIcon + ' ' + (option[fields.iconClass] ? deQuote(option[fields.iconClass]) : className.icon) + '"></i>';
  4117. }
  4118. html += groupName;
  4119. html += '</div>';
  4120. }
  4121. if(option[fields.divider]){
  4122. html += '<div class="'+className.divider+'"></div>';
  4123. }
  4124. }
  4125. });
  4126. return html;
  4127. },
  4128. // generates label for multiselect
  4129. label: function(value, text, preserveHTML, className) {
  4130. var
  4131. escape = $.fn.dropdown.settings.templates.escape;
  4132. return escape(text,preserveHTML) + '<i class="'+className.delete+' icon"></i>';
  4133. },
  4134. // generates messages like "No results"
  4135. message: function(message) {
  4136. return message;
  4137. },
  4138. // generates user addition to selection menu
  4139. addition: function(choice) {
  4140. return choice;
  4141. }
  4142. };
  4143. })( jQuery, window, document );