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.

2805 lines
95 KiB

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