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

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 );