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.

3327 lines
114 KiB

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