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

3442 lines
119 KiB

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