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

semanticDropdown.js 141KB

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