gitea源码

dropdown.js 154KB

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