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

semantic.dropdown.custom.js 144 kB

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