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.

2829 lines
96 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
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
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
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
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
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
10 years ago
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
9 years ago
10 years ago
10 years ago
9 years ago
9 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
9 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
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. transition: function($subMenu) {
  1179. return (settings.transition == 'auto')
  1180. ? module.is.upward($subMenu)
  1181. ? 'slide up'
  1182. : 'slide down'
  1183. : settings.transition
  1184. ;
  1185. },
  1186. userValues: function() {
  1187. var
  1188. values = module.get.values()
  1189. ;
  1190. if(!values) {
  1191. return false;
  1192. }
  1193. return $.grep(values, function(value) {
  1194. return (module.get.item(value) === false);
  1195. });
  1196. },
  1197. uniqueArray: function(array) {
  1198. return $.grep(array, function (value, index) {
  1199. return $.inArray(value, array) === index;
  1200. });
  1201. },
  1202. caretPosition: function() {
  1203. var
  1204. input = $search.get(0),
  1205. range,
  1206. rangeLength
  1207. ;
  1208. if('selectionStart' in input) {
  1209. return input.selectionStart;
  1210. }
  1211. else if (document.selection) {
  1212. input.focus();
  1213. range = document.selection.createRange();
  1214. rangeLength = range.text.length;
  1215. range.moveStart('character', -input.value.length);
  1216. return range.text.length - rangeLength;
  1217. }
  1218. },
  1219. shortcutKeys: function() {
  1220. return {
  1221. backspace : 8,
  1222. delimiter : 188, // comma
  1223. deleteKey : 46,
  1224. enter : 13,
  1225. escape : 27,
  1226. pageUp : 33,
  1227. pageDown : 34,
  1228. leftArrow : 37,
  1229. upArrow : 38,
  1230. rightArrow : 39,
  1231. downArrow : 40
  1232. };
  1233. },
  1234. value: function() {
  1235. return ($input.length > 0)
  1236. ? $input.val()
  1237. : $module.data(metadata.value)
  1238. ;
  1239. },
  1240. values: function() {
  1241. var
  1242. value = module.get.value()
  1243. ;
  1244. if(value === '') {
  1245. return '';
  1246. }
  1247. return (!$input.is('select') && module.is.multiple())
  1248. ? typeof value == 'string'
  1249. ? value.split(settings.delimiter)
  1250. : ''
  1251. : value
  1252. ;
  1253. },
  1254. choiceText: function($choice, preserveHTML) {
  1255. preserveHTML = (preserveHTML !== undefined)
  1256. ? preserveHTML
  1257. : settings.preserveHTML
  1258. ;
  1259. if($choice) {
  1260. if($choice.find(selector.menu).length > 0) {
  1261. module.verbose('Retreiving text of element with sub-menu');
  1262. $choice = $choice.clone();
  1263. $choice.find(selector.menu).remove();
  1264. $choice.find(selector.menuIcon).remove();
  1265. }
  1266. return ($choice.data(metadata.text) !== undefined)
  1267. ? $choice.data(metadata.text)
  1268. : (preserveHTML)
  1269. ? $choice.html().trim()
  1270. : $choice.text().trim()
  1271. ;
  1272. }
  1273. },
  1274. choiceValue: function($choice, choiceText) {
  1275. choiceText = choiceText || module.get.choiceText($choice);
  1276. if(!$choice) {
  1277. return false;
  1278. }
  1279. return ($choice.data(metadata.value) !== undefined)
  1280. ? $choice.data(metadata.value)
  1281. : (typeof choiceText === 'string')
  1282. ? choiceText.toLowerCase().trim()
  1283. : choiceText
  1284. ;
  1285. },
  1286. inputEvent: function() {
  1287. var
  1288. input = $search[0]
  1289. ;
  1290. if(input) {
  1291. return (input.oninput !== undefined)
  1292. ? 'input'
  1293. : (input.onpropertychange !== undefined)
  1294. ? 'propertychange'
  1295. : 'keyup'
  1296. ;
  1297. }
  1298. return false;
  1299. },
  1300. selectValues: function() {
  1301. var
  1302. select = {}
  1303. ;
  1304. select.values = [];
  1305. $module
  1306. .find('option')
  1307. .each(function() {
  1308. var
  1309. name = $(this).html(),
  1310. value = ( $(this).attr('value') !== undefined )
  1311. ? $(this).attr('value')
  1312. : name
  1313. ;
  1314. if(settings.placeholder === 'auto' && value === '') {
  1315. select.placeholder = name;
  1316. }
  1317. else {
  1318. select.values.push({
  1319. name: name,
  1320. value: value
  1321. });
  1322. }
  1323. })
  1324. ;
  1325. if(settings.placeholder && settings.placeholder !== 'auto') {
  1326. module.debug('Setting placeholder value to', settings.placeholder);
  1327. select.placeholder = settings.placeholder;
  1328. }
  1329. if(settings.sortSelect) {
  1330. select.values.sort(function(a, b) {
  1331. return (a.name > b.name)
  1332. ? 1
  1333. : -1
  1334. ;
  1335. });
  1336. module.debug('Retrieved and sorted values from select', select);
  1337. }
  1338. else {
  1339. module.debug('Retreived values from select', select);
  1340. }
  1341. return select;
  1342. },
  1343. activeItem: function() {
  1344. return $item.filter('.' + className.active);
  1345. },
  1346. selectedItem: function() {
  1347. var
  1348. $selectedItem = $item.not('.' + className.filtered).filter('.' + className.selected)
  1349. ;
  1350. return ($selectedItem.length > 0)
  1351. ? $selectedItem
  1352. : $item.eq(0)
  1353. ;
  1354. },
  1355. item: function(value, strict) {
  1356. var
  1357. $selectedItem = false,
  1358. isMultiple
  1359. ;
  1360. value = (value !== undefined)
  1361. ? value
  1362. : ( module.get.values() !== undefined)
  1363. ? module.get.values()
  1364. : module.get.text()
  1365. ;
  1366. isMultiple = (module.is.multiple() && $.isArray(value));
  1367. strict = (value === '' || value === 0)
  1368. ? true
  1369. : strict || false
  1370. ;
  1371. if(value !== undefined) {
  1372. $item
  1373. .each(function() {
  1374. var
  1375. $choice = $(this),
  1376. optionText = module.get.choiceText($choice),
  1377. optionValue = module.get.choiceValue($choice, optionText)
  1378. ;
  1379. if(isMultiple) {
  1380. if($.inArray(optionValue.toString(), value) !== -1 || $.inArray(optionText, value) !== -1) {
  1381. $selectedItem = ($selectedItem)
  1382. ? $selectedItem.add($choice)
  1383. : $choice
  1384. ;
  1385. }
  1386. }
  1387. else if(strict) {
  1388. module.verbose('Ambiguous dropdown value using strict type check', $choice, value);
  1389. if( optionValue === value || optionText === value) {
  1390. $selectedItem = $choice;
  1391. return true;
  1392. }
  1393. }
  1394. else {
  1395. if( optionValue == value || optionText == value) {
  1396. module.verbose('Found select item by value', optionValue, value);
  1397. $selectedItem = $choice;
  1398. return true;
  1399. }
  1400. }
  1401. })
  1402. ;
  1403. }
  1404. return $selectedItem;
  1405. }
  1406. },
  1407. check: {
  1408. maxSelections: function(selectionCount) {
  1409. if(settings.maxSelections) {
  1410. selectionCount = (selectionCount !== undefined)
  1411. ? selectionCount
  1412. : module.get.selectionCount()
  1413. ;
  1414. if(selectionCount >= settings.maxSelections) {
  1415. module.debug('Maximum selection count reached');
  1416. $item.addClass(className.filtered);
  1417. module.add.message(message.maxSelections);
  1418. return true;
  1419. }
  1420. else {
  1421. module.verbose('No longer maximum selection count, removing message');
  1422. module.remove.message();
  1423. module.remove.filteredItem();
  1424. if(module.is.searchSelection()) {
  1425. module.filter();
  1426. }
  1427. return false;
  1428. }
  1429. }
  1430. return true;
  1431. }
  1432. },
  1433. restore: {
  1434. defaults: function() {
  1435. module.restore.defaultText();
  1436. module.restore.defaultValue();
  1437. },
  1438. defaultText: function() {
  1439. var
  1440. defaultText = $module.data(metadata.defaultText)
  1441. ;
  1442. module.debug('Restoring default text', defaultText);
  1443. module.set.text(defaultText);
  1444. $text.addClass(className.placeholder);
  1445. },
  1446. defaultValue: function() {
  1447. var
  1448. defaultValue = $module.data(metadata.defaultValue)
  1449. ;
  1450. if(defaultValue !== undefined) {
  1451. module.debug('Restoring default value', defaultValue);
  1452. if(defaultValue.length) {
  1453. module.set.selected(defaultValue);
  1454. }
  1455. else {
  1456. module.remove.activeItem();
  1457. module.remove.selectedItem();
  1458. }
  1459. }
  1460. }
  1461. },
  1462. save: {
  1463. defaults: function() {
  1464. module.save.defaultText();
  1465. module.save.placeholderText();
  1466. module.save.defaultValue();
  1467. },
  1468. defaultValue: function() {
  1469. $module.data(metadata.defaultValue, module.get.value());
  1470. },
  1471. defaultText: function() {
  1472. $module.data(metadata.defaultText, $text.text() );
  1473. },
  1474. placeholderText: function() {
  1475. if($text.hasClass(className.placeholder)) {
  1476. $module.data(metadata.placeholderText, $text.text());
  1477. }
  1478. }
  1479. },
  1480. clear: function() {
  1481. module.set.placeholderText();
  1482. module.clearValue();
  1483. module.remove.activeItem();
  1484. module.remove.selectedItem();
  1485. },
  1486. clearValue: function() {
  1487. module.set.value('');
  1488. },
  1489. scrollPage: function(direction, $selectedItem) {
  1490. var
  1491. menuHeight = $menu.outerHeight(),
  1492. currentScroll = $menu.scrollTop(),
  1493. itemHeight = $item.eq(0).outerHeight(),
  1494. itemsPerPage = Math.floor(menuHeight / itemHeight),
  1495. maxScroll = $menu.prop('scrollHeight'),
  1496. newScroll = (direction == 'up')
  1497. ? currentScroll - (itemHeight * itemsPerPage)
  1498. : currentScroll + (itemHeight * itemsPerPage),
  1499. $selectableItem = $item.not('.' + className.filtered),
  1500. isWithinRange,
  1501. $nextSelectedItem,
  1502. elementIndex
  1503. ;
  1504. $selectedItem = $selectedItem || module.get.selectedItem();
  1505. elementIndex = (direction == 'up')
  1506. ? $selectableItem.index($selectedItem) - itemsPerPage
  1507. : $selectableItem.index($selectedItem) + itemsPerPage
  1508. ;
  1509. isWithinRange = (direction == 'up')
  1510. ? (elementIndex >= 0)
  1511. : (elementIndex < $selectableItem.length)
  1512. ;
  1513. $nextSelectedItem = (isWithinRange)
  1514. ? $selectableItem.eq(elementIndex)
  1515. : (direction == 'up')
  1516. ? $selectableItem.first()
  1517. : $selectableItem.last()
  1518. ;
  1519. if($nextSelectedItem.length > 0) {
  1520. module.debug('Scrolling page', direction, $nextSelectedItem);
  1521. $selectedItem
  1522. .removeClass(className.selected)
  1523. ;
  1524. $nextSelectedItem
  1525. .addClass(className.selected)
  1526. ;
  1527. $menu
  1528. .scrollTop(newScroll)
  1529. ;
  1530. }
  1531. },
  1532. set: {
  1533. filtered: function() {
  1534. var
  1535. isMultiple = module.is.multiple(),
  1536. isSearch = module.is.searchSelection(),
  1537. isSearchMultiple = (isMultiple && isSearch),
  1538. searchValue = (isSearch)
  1539. ? module.get.query()
  1540. : '',
  1541. hasSearchValue = (typeof searchValue === 'string' && searchValue.length > 0),
  1542. searchWidth = module.get.searchWidth(searchValue.length),
  1543. valueIsSet = searchValue !== ''
  1544. ;
  1545. if(isMultiple && hasSearchValue) {
  1546. module.verbose('Adjusting input width', searchWidth, settings.glyphWidth);
  1547. $search.css('width', searchWidth);
  1548. }
  1549. if(hasSearchValue || (isSearchMultiple && valueIsSet)) {
  1550. module.verbose('Hiding placeholder text');
  1551. $text.addClass(className.filtered);
  1552. }
  1553. else if(!isMultiple || (isSearchMultiple && !valueIsSet)) {
  1554. module.verbose('Showing placeholder text');
  1555. $text.removeClass(className.filtered);
  1556. }
  1557. },
  1558. placeholderText: function(text) {
  1559. module.debug('Restoring placeholder text');
  1560. text = text || $module.data(metadata.placeholderText);
  1561. module.set.text(text);
  1562. $text.addClass(className.placeholder);
  1563. },
  1564. tabbable: function() {
  1565. if( module.has.search() ) {
  1566. module.debug('Added tabindex to searchable dropdown');
  1567. $search
  1568. .val('')
  1569. .attr('tabindex', 0)
  1570. ;
  1571. }
  1572. else {
  1573. module.debug('Added tabindex to dropdown');
  1574. if(!$module.attr('tabindex') ) {
  1575. $module
  1576. .attr('tabindex', 0)
  1577. ;
  1578. }
  1579. }
  1580. },
  1581. scrollPosition: function($item, forceScroll) {
  1582. var
  1583. edgeTolerance = 5,
  1584. $menu,
  1585. hasActive,
  1586. offset,
  1587. itemHeight,
  1588. itemOffset,
  1589. menuOffset,
  1590. menuScroll,
  1591. menuHeight,
  1592. abovePage,
  1593. belowPage
  1594. ;
  1595. $item = $item || module.get.selectedItem();
  1596. $menu = $item.closest(selector.menu);
  1597. hasActive = ($item && $item.length > 0);
  1598. forceScroll = (forceScroll !== undefined)
  1599. ? forceScroll
  1600. : false
  1601. ;
  1602. if($item && $menu.length > 0 && hasActive) {
  1603. itemOffset = $item.position().top;
  1604. $menu.addClass(className.loading);
  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. upward: function($menu) {
  1676. var $element = $menu || $module;
  1677. $element.addClass(className.upward);
  1678. },
  1679. value: function(value, text, $selected) {
  1680. var
  1681. hasInput = ($input.length > 0),
  1682. isAddition = !module.has.value(value),
  1683. currentValue = module.get.values()
  1684. ;
  1685. if($input.length > 0) {
  1686. if( module.is.multiple() ) {
  1687. if(value === '') {
  1688. module.debug('Cannot select blank values from multiselect');
  1689. return;
  1690. }
  1691. // extend currently selected values
  1692. value = [value];
  1693. if($.isArray(currentValue)) {
  1694. value = currentValue.concat(value);
  1695. value = module.get.uniqueArray(value);
  1696. }
  1697. // set values
  1698. if( $input.is('select') ) {
  1699. module.debug('Setting multiple <select> values', value, $input);
  1700. if(settings.allowAdditions) {
  1701. module.add.optionValue(value);
  1702. }
  1703. }
  1704. else {
  1705. value = value.join(settings.delimiter);
  1706. module.debug('Setting hidden input to delimited values', value, $input);
  1707. }
  1708. }
  1709. if(value == currentValue) {
  1710. module.verbose('Skipping value update already same value', value, currentValue);
  1711. return;
  1712. }
  1713. module.debug('Updating input value', value, currentValue);
  1714. $input
  1715. .val(value)
  1716. .trigger('change')
  1717. ;
  1718. settings.onChange.call(element, value, text, $selected);
  1719. }
  1720. else {
  1721. module.verbose('Storing value in metadata', value, $input);
  1722. if(value !== currentValue) {
  1723. $module.data(metadata.value, value);
  1724. settings.onChange.call(element, value, text, $selected);
  1725. }
  1726. }
  1727. module.check.maxSelections();
  1728. },
  1729. active: function() {
  1730. $module
  1731. .addClass(className.active)
  1732. ;
  1733. },
  1734. multiple: function() {
  1735. $module.addClass(className.multiple);
  1736. },
  1737. visible: function() {
  1738. $module.addClass(className.visible);
  1739. },
  1740. selected: function(value, $selectedItem) {
  1741. var
  1742. isMultiple = module.is.multiple()
  1743. ;
  1744. $selectedItem = $selectedItem || module.get.item(value);
  1745. if(!$selectedItem) {
  1746. return false;
  1747. }
  1748. module.debug('Setting selected menu item to', $selectedItem);
  1749. if(module.is.single()) {
  1750. module.remove.activeItem();
  1751. module.remove.selectedItem();
  1752. }
  1753. else if(settings.useLabels) {
  1754. module.remove.selectedItem();
  1755. }
  1756. $selectedItem
  1757. .each(function() {
  1758. var
  1759. $selected = $(this),
  1760. selectedText = module.get.choiceText($selected),
  1761. selectedValue = module.get.choiceValue($selected, selectedText),
  1762. isFiltered = $selected.hasClass(className.filtered),
  1763. isActive = $selected.hasClass(className.active),
  1764. isUserValue = $selected.hasClass(className.addition),
  1765. shouldAnimate = (isMultiple && $selectedItem.length == 1)
  1766. ;
  1767. if(isMultiple) {
  1768. if(!isActive || isUserValue) {
  1769. if(settings.useLabels) {
  1770. module.add.label(selectedValue, selectedText, shouldAnimate);
  1771. module.set.value(selectedValue, selectedText, $selected);
  1772. $selected.addClass(className.active);
  1773. module.filterActive();
  1774. module.select.nextAvailable($selectedItem);
  1775. }
  1776. else {
  1777. module.set.value(selectedValue, selectedText, $selected);
  1778. module.set.text(module.add.variables(message.count));
  1779. $selected.addClass(className.active);
  1780. }
  1781. }
  1782. else if(!isFiltered) {
  1783. module.debug('Selected active value, removing label');
  1784. module.remove.selected(selectedValue);
  1785. }
  1786. }
  1787. else {
  1788. module.set.value(selectedValue, selectedText, $selected);
  1789. module.set.text(selectedText);
  1790. $selected
  1791. .addClass(className.active)
  1792. .addClass(className.selected)
  1793. ;
  1794. }
  1795. })
  1796. ;
  1797. }
  1798. },
  1799. add: {
  1800. label: function(value, text, shouldAnimate) {
  1801. var
  1802. $next = module.is.searchSelection()
  1803. ? $search
  1804. : $text,
  1805. $label
  1806. ;
  1807. $label = $('<a />')
  1808. .addClass(className.label)
  1809. .attr('data-value', value)
  1810. .html(templates.label(value, text))
  1811. ;
  1812. $label = settings.onLabelCreate.call($label, value, text);
  1813. if(module.has.label(value)) {
  1814. module.debug('Label already exists, skipping', value);
  1815. return;
  1816. }
  1817. if(settings.label.variation) {
  1818. $label.addClass(settings.label.variation);
  1819. }
  1820. if(shouldAnimate === true) {
  1821. module.debug('Animating in label', $label);
  1822. $label
  1823. .addClass(className.hidden)
  1824. .insertBefore($next)
  1825. .transition(settings.label.transition, settings.label.duration)
  1826. ;
  1827. }
  1828. else {
  1829. module.debug('Adding selection label', $label);
  1830. $label
  1831. .insertBefore($next)
  1832. ;
  1833. }
  1834. },
  1835. message: function(message) {
  1836. var
  1837. $message = $menu.children(selector.message),
  1838. html = settings.templates.message(module.add.variables(message))
  1839. ;
  1840. if($message.length > 0) {
  1841. $message
  1842. .html(html)
  1843. ;
  1844. }
  1845. else {
  1846. $message = $('<div/>')
  1847. .html(html)
  1848. .addClass(className.message)
  1849. .appendTo($menu)
  1850. ;
  1851. }
  1852. },
  1853. optionValue: function(values) {
  1854. if(!$input.is('select')) {
  1855. return false;
  1856. }
  1857. if(selectObserver) {
  1858. selectObserver.disconnect();
  1859. }
  1860. $.each(values, function(index, value) {
  1861. var
  1862. $option = $input.find('option[value="' + value + '"]'),
  1863. hasOption = ($option.length > 0)
  1864. ;
  1865. if(!hasOption) {
  1866. $('<option/>')
  1867. .prop('value', value)
  1868. .html(value)
  1869. .appendTo($input)
  1870. ;
  1871. module.verbose('Adding user addition as an <option>', value);
  1872. }
  1873. });
  1874. if(selectObserver) {
  1875. selectObserver.observe($input[0], {
  1876. childList : true,
  1877. subtree : true
  1878. });
  1879. }
  1880. },
  1881. userChoice: function(value) {
  1882. var
  1883. alreadyHasValue = module.get.item(value),
  1884. $addition = $menu.children(selector.addition),
  1885. html
  1886. ;
  1887. if(module.has.maxSelections()) {
  1888. return;
  1889. }
  1890. if(value === '' || alreadyHasValue) {
  1891. $addition.remove();
  1892. return;
  1893. }
  1894. html = settings.templates.addition(value);
  1895. $item
  1896. .removeClass(className.selected)
  1897. ;
  1898. if($addition.length > 0) {
  1899. $addition
  1900. .html(html)
  1901. .data(metadata.value, value)
  1902. .removeClass(className.filtered)
  1903. .addClass(className.selected)
  1904. ;
  1905. }
  1906. else {
  1907. $addition = $('<div/>')
  1908. .html(html)
  1909. .data(metadata.value, value)
  1910. .addClass(className.addition)
  1911. .addClass(className.item)
  1912. .prependTo($menu)
  1913. .addClass(className.selected)
  1914. ;
  1915. }
  1916. },
  1917. variables: function(message) {
  1918. var
  1919. hasCount = (message.search('{count}') !== -1),
  1920. hasMaxCount = (message.search('{maxCount}') !== -1),
  1921. hasTerm = (message.search('{term}') !== -1),
  1922. values,
  1923. count,
  1924. query
  1925. ;
  1926. if(hasCount) {
  1927. count = module.get.selectionCount();
  1928. message = message.replace('{count}', count);
  1929. }
  1930. if(hasMaxCount) {
  1931. count = module.get.selectionCount();
  1932. message = message.replace('{maxCount}', settings.maxSelections);
  1933. }
  1934. if(hasTerm) {
  1935. query = module.get.query();
  1936. message = message.replace('{term}', query);
  1937. }
  1938. return message;
  1939. }
  1940. },
  1941. remove: {
  1942. active: function() {
  1943. $module.removeClass(className.active);
  1944. },
  1945. activeLabel: function() {
  1946. $module.find(selector.label).removeClass(className.active);
  1947. },
  1948. upward: function($menu) {
  1949. var $element = $menu || $module;
  1950. $element.removeClass(className.upward);
  1951. },
  1952. visible: function() {
  1953. $module.removeClass(className.visible);
  1954. },
  1955. activeItem: function() {
  1956. $item.removeClass(className.active);
  1957. },
  1958. filteredItem: function() {
  1959. if( module.has.maxSelections() ) {
  1960. return;
  1961. }
  1962. if(settings.useLabels) {
  1963. $item.not('.' + className.active).removeClass(className.filtered);
  1964. }
  1965. else {
  1966. $item.removeClass(className.filtered);
  1967. }
  1968. },
  1969. message: function() {
  1970. $menu.children(selector.message).remove();
  1971. },
  1972. searchTerm: function() {
  1973. module.verbose('Cleared search term');
  1974. $search.val('');
  1975. module.set.filtered();
  1976. },
  1977. selected: function(value) {
  1978. var
  1979. $selectedItem = module.get.item(value),
  1980. selectedValue = module.get.choiceValue($selectedItem)
  1981. ;
  1982. if(!$selectedItem) {
  1983. return false;
  1984. }
  1985. module.remove.value(selectedValue);
  1986. if(module.is.multiple()) {
  1987. if(settings.useLabels) {
  1988. module.remove.label(selectedValue);
  1989. }
  1990. else {
  1991. module.set.text(module.add.variables(message.count));
  1992. }
  1993. }
  1994. $selectedItem
  1995. .removeClass(className.filtered)
  1996. .removeClass(className.active)
  1997. ;
  1998. if(settings.useLabels) {
  1999. $selectedItem.removeClass(className.selected);
  2000. }
  2001. },
  2002. selectedItem: function() {
  2003. $item.removeClass(className.selected);
  2004. },
  2005. value: function(value) {
  2006. var
  2007. values = $input.val()
  2008. ;
  2009. if( $input.is('select') ) {
  2010. module.verbose('Input is <select> removing selected');
  2011. $input
  2012. .find('option[value="' + value + '"]')
  2013. .prop('selected', false)
  2014. ;
  2015. }
  2016. else {
  2017. module.verbose('Input is csv removing value');
  2018. values = module.remove.delimitedValue(value, values);
  2019. $input
  2020. .val(values)
  2021. .trigger('change')
  2022. ;
  2023. }
  2024. module.check.maxSelections();
  2025. },
  2026. delimitedValue: function(removedValue, values) {
  2027. if(typeof values != 'string') {
  2028. return false;
  2029. }
  2030. values = values.split(settings.delimiter);
  2031. values = $.grep(values, function(value){
  2032. return (removedValue != value);
  2033. });
  2034. values = values.join(settings.delimiter);
  2035. module.verbose('Removed value from delimited string', removedValue, values);
  2036. return values;
  2037. },
  2038. label: function(value) {
  2039. var
  2040. $labels = $module.find(selector.label),
  2041. $removedLabel = $labels.filter('[data-value="' + value +'"]'),
  2042. labelCount = $labels.length,
  2043. isLastLabel = ($labels.index($removedLabel) + 1 == labelCount),
  2044. shouldAnimate = ( (!module.is.searchSelection() || !module.is.focusedOnSearch()) && isLastLabel)
  2045. ;
  2046. if(shouldAnimate) {
  2047. module.verbose('Animating and removing label', $removedLabel);
  2048. $removedLabel
  2049. .transition(settings.label.transition, settings.label.duration, function() {
  2050. $removedLabel.remove();
  2051. })
  2052. ;
  2053. }
  2054. else {
  2055. module.verbose('Removing label', $removedLabel);
  2056. $removedLabel.remove();
  2057. }
  2058. },
  2059. labels: function($activeLabels) {
  2060. $activeLabels = $activeLabels || $module.find(selector.label).filter('.' + className.active);
  2061. module.verbose('Removing active label selections', $activeLabels);
  2062. $activeLabels
  2063. .each(function(){
  2064. var
  2065. value = $(this).data('value'),
  2066. isUserValue = module.is.userValue(value)
  2067. ;
  2068. if(isUserValue) {
  2069. module.remove.value(value);
  2070. module.remove.label(value);
  2071. }
  2072. else {
  2073. module.remove.selected(value);
  2074. }
  2075. })
  2076. ;
  2077. },
  2078. tabbable: function() {
  2079. if( module.has.search() ) {
  2080. module.debug('Searchable dropdown initialized');
  2081. $search
  2082. .attr('tabindex', '-1')
  2083. ;
  2084. $menu
  2085. .attr('tabindex', '-1')
  2086. ;
  2087. }
  2088. else {
  2089. module.debug('Simple selection dropdown initialized');
  2090. $module
  2091. .attr('tabindex', '-1')
  2092. ;
  2093. $menu
  2094. .attr('tabindex', '-1')
  2095. ;
  2096. }
  2097. }
  2098. },
  2099. has: {
  2100. search: function() {
  2101. return ($search.length > 0);
  2102. },
  2103. input: function() {
  2104. return ($input.length > 0);
  2105. },
  2106. menu: function() {
  2107. return ($menu.length > 0);
  2108. },
  2109. message: function() {
  2110. return ($menu.children(selector.message).length !== 0);
  2111. },
  2112. label: function(value) {
  2113. var
  2114. $labels = $module.find(selector.label)
  2115. ;
  2116. return ($labels.filter('[data-value="' + value +'"]').length > 0);
  2117. },
  2118. maxSelections: function() {
  2119. return (settings.maxSelections && module.get.selectionCount() >= settings.maxSelections);
  2120. },
  2121. allResultsFiltered: function() {
  2122. return ($item.filter('.' + className.filtered).length === $item.length);
  2123. },
  2124. value: function(value) {
  2125. var
  2126. values = module.get.values(),
  2127. hasValue = $.isArray(values)
  2128. ? values && ($.inArray(value, values) !== -1)
  2129. : (values == value)
  2130. ;
  2131. return (hasValue)
  2132. ? true
  2133. : false
  2134. ;
  2135. }
  2136. },
  2137. is: {
  2138. active: function() {
  2139. return $module.hasClass(className.active);
  2140. },
  2141. alreadySetup: function() {
  2142. return ($module.is('select') && $module.parent(selector.dropdown).length > 0 && $module.prev().length === 0);
  2143. },
  2144. animating: function($subMenu) {
  2145. return ($subMenu)
  2146. ? $subMenu.transition && $subMenu.transition('is animating')
  2147. : $menu.transition && $menu.transition('is animating')
  2148. ;
  2149. },
  2150. focused: function() {
  2151. return (document.activeElement === $module[0]);
  2152. },
  2153. focusedOnSearch: function() {
  2154. return (document.activeElement === $search[0]);
  2155. },
  2156. allFiltered: function() {
  2157. return( (module.is.multiple() || module.has.search()) && !module.has.message() && module.has.allResultsFiltered() );
  2158. },
  2159. hidden: function($subMenu) {
  2160. return !module.is.visible($subMenu);
  2161. },
  2162. onScreen: function($subMenu) {
  2163. var
  2164. $currentMenu = $subMenu || $menu,
  2165. onScreen
  2166. ;
  2167. $currentMenu.addClass(className.loading);
  2168. onScreen = (false && $.fn.visibility !== undefined)
  2169. ? $currentMenu.visibility('bottom visible')
  2170. : $('body').scrollTop() + $(window).height() >= $currentMenu.offset().top + $currentMenu.height()
  2171. ;
  2172. module.debug('Checking if menu can fit on screen', onScreen, $menu);
  2173. $currentMenu.removeClass(className.loading);
  2174. return onScreen;
  2175. },
  2176. inObject: function(needle, object) {
  2177. var
  2178. found = false
  2179. ;
  2180. $.each(object, function(index, property) {
  2181. if(property == needle) {
  2182. found = true;
  2183. return true;
  2184. }
  2185. });
  2186. return found;
  2187. },
  2188. multiple: function() {
  2189. return $module.hasClass(className.multiple);
  2190. },
  2191. single: function() {
  2192. return !module.is.multiple();
  2193. },
  2194. selectMutation: function(mutations) {
  2195. var
  2196. selectChanged = false
  2197. ;
  2198. $.each(mutations, function(index, mutation) {
  2199. if(mutation.target && $(mutation.target).is('select')) {
  2200. selectChanged = true;
  2201. return true;
  2202. }
  2203. });
  2204. return selectChanged;
  2205. },
  2206. search: function() {
  2207. return $module.hasClass(className.search);
  2208. },
  2209. searchSelection: function() {
  2210. return ( module.has.search() && $search.closest(selector.menu).length === 0 );
  2211. },
  2212. selection: function() {
  2213. return $module.hasClass(className.selection);
  2214. },
  2215. userValue: function(value) {
  2216. return ($.inArray(value, module.get.userValues()) !== -1);
  2217. },
  2218. upward: function($menu) {
  2219. var $element = $menu || $module;
  2220. return $element.hasClass(className.upward);
  2221. },
  2222. visible: function($subMenu) {
  2223. return ($subMenu)
  2224. ? $subMenu.hasClass(className.visible)
  2225. : $menu.hasClass(className.visible)
  2226. ;
  2227. }
  2228. },
  2229. can: {
  2230. click: function() {
  2231. return (hasTouch || settings.on == 'click');
  2232. },
  2233. show: function() {
  2234. return !$module.hasClass(className.disabled);
  2235. }
  2236. },
  2237. animate: {
  2238. show: function(callback, $subMenu) {
  2239. var
  2240. $currentMenu = $subMenu || $menu,
  2241. start = ($subMenu)
  2242. ? function() {}
  2243. : function() {
  2244. module.hideSubMenus();
  2245. module.hideOthers();
  2246. module.set.active();
  2247. },
  2248. transition
  2249. ;
  2250. callback = $.isFunction(callback)
  2251. ? callback
  2252. : function(){}
  2253. ;
  2254. module.verbose('Doing menu show animation', $currentMenu);
  2255. if(settings.keepOnScreen) {
  2256. if(module.is.onScreen($subMenu)) {
  2257. module.remove.upward($subMenu);
  2258. }
  2259. else {
  2260. module.set.upward($subMenu);
  2261. }
  2262. }
  2263. transition = module.get.transition($subMenu);
  2264. if( module.is.selection() ) {
  2265. module.set.scrollPosition(module.get.selectedItem(), true);
  2266. }
  2267. if( module.is.hidden($currentMenu) || module.is.animating($currentMenu) ) {
  2268. if(transition == 'none') {
  2269. start();
  2270. $currentMenu.transition('show');
  2271. callback.call(element);
  2272. }
  2273. else if($.fn.transition !== undefined && $module.transition('is supported')) {
  2274. $currentMenu
  2275. .transition({
  2276. animation : transition + ' in',
  2277. debug : settings.debug,
  2278. verbose : settings.verbose,
  2279. duration : settings.duration,
  2280. queue : true,
  2281. onStart : start,
  2282. onComplete : function() {
  2283. callback.call(element);
  2284. }
  2285. })
  2286. ;
  2287. }
  2288. else {
  2289. module.error(error.noTransition, transition);
  2290. }
  2291. }
  2292. },
  2293. hide: function(callback, $subMenu) {
  2294. var
  2295. $currentMenu = $subMenu || $menu,
  2296. duration = ($subMenu)
  2297. ? (settings.duration * 0.9)
  2298. : settings.duration,
  2299. start = ($subMenu)
  2300. ? function() {}
  2301. : function() {
  2302. if( module.can.click() ) {
  2303. module.unbind.intent();
  2304. }
  2305. module.focusSearch();
  2306. module.remove.active();
  2307. },
  2308. transition = module.get.transition($subMenu)
  2309. ;
  2310. callback = $.isFunction(callback)
  2311. ? callback
  2312. : function(){}
  2313. ;
  2314. if( module.is.visible($currentMenu) || module.is.animating($currentMenu) ) {
  2315. module.verbose('Doing menu hide animation', $currentMenu);
  2316. if(transition == 'none') {
  2317. start();
  2318. $currentMenu.transition('hide');
  2319. callback.call(element);
  2320. }
  2321. else if($.fn.transition !== undefined && $module.transition('is supported')) {
  2322. $currentMenu
  2323. .transition({
  2324. animation : transition + ' out',
  2325. duration : settings.duration,
  2326. debug : settings.debug,
  2327. verbose : settings.verbose,
  2328. queue : true,
  2329. onStart : start,
  2330. onComplete : function() {
  2331. module.remove.upward($subMenu);
  2332. callback.call(element);
  2333. }
  2334. })
  2335. ;
  2336. }
  2337. else {
  2338. module.error(error.transition);
  2339. }
  2340. }
  2341. }
  2342. },
  2343. hideAndClear: function() {
  2344. if(module.is.searchSelection()) {
  2345. module.remove.searchTerm();
  2346. module.hide(function() {
  2347. module.remove.filteredItem();
  2348. });
  2349. }
  2350. else {
  2351. module.hide();
  2352. }
  2353. },
  2354. delay: {
  2355. show: function() {
  2356. module.verbose('Delaying show event to ensure user intent');
  2357. clearTimeout(module.timer);
  2358. module.timer = setTimeout(module.show, settings.delay.show);
  2359. },
  2360. hide: function() {
  2361. module.verbose('Delaying hide event to ensure user intent');
  2362. clearTimeout(module.timer);
  2363. module.timer = setTimeout(module.hide, settings.delay.hide);
  2364. }
  2365. },
  2366. escape: {
  2367. regExp: function(text) {
  2368. text = String(text);
  2369. return text.replace(regExp.escape, '\\$&');
  2370. }
  2371. },
  2372. setting: function(name, value) {
  2373. module.debug('Changing setting', name, value);
  2374. if( $.isPlainObject(name) ) {
  2375. $.extend(true, settings, name);
  2376. }
  2377. else if(value !== undefined) {
  2378. settings[name] = value;
  2379. }
  2380. else {
  2381. return settings[name];
  2382. }
  2383. },
  2384. internal: function(name, value) {
  2385. if( $.isPlainObject(name) ) {
  2386. $.extend(true, module, name);
  2387. }
  2388. else if(value !== undefined) {
  2389. module[name] = value;
  2390. }
  2391. else {
  2392. return module[name];
  2393. }
  2394. },
  2395. debug: function() {
  2396. if(settings.debug) {
  2397. if(settings.performance) {
  2398. module.performance.log(arguments);
  2399. }
  2400. else {
  2401. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  2402. module.debug.apply(console, arguments);
  2403. }
  2404. }
  2405. },
  2406. verbose: function() {
  2407. if(settings.verbose && settings.debug) {
  2408. if(settings.performance) {
  2409. module.performance.log(arguments);
  2410. }
  2411. else {
  2412. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  2413. module.verbose.apply(console, arguments);
  2414. }
  2415. }
  2416. },
  2417. error: function() {
  2418. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  2419. module.error.apply(console, arguments);
  2420. },
  2421. performance: {
  2422. log: function(message) {
  2423. var
  2424. currentTime,
  2425. executionTime,
  2426. previousTime
  2427. ;
  2428. if(settings.performance) {
  2429. currentTime = new Date().getTime();
  2430. previousTime = time || currentTime;
  2431. executionTime = currentTime - previousTime;
  2432. time = currentTime;
  2433. performance.push({
  2434. 'Name' : message[0],
  2435. 'Arguments' : [].slice.call(message, 1) || '',
  2436. 'Element' : element,
  2437. 'Execution Time' : executionTime
  2438. });
  2439. }
  2440. clearTimeout(module.performance.timer);
  2441. module.performance.timer = setTimeout(module.performance.display, 500);
  2442. },
  2443. display: function() {
  2444. var
  2445. title = settings.name + ':',
  2446. totalTime = 0
  2447. ;
  2448. time = false;
  2449. clearTimeout(module.performance.timer);
  2450. $.each(performance, function(index, data) {
  2451. totalTime += data['Execution Time'];
  2452. });
  2453. title += ' ' + totalTime + 'ms';
  2454. if(moduleSelector) {
  2455. title += ' \'' + moduleSelector + '\'';
  2456. }
  2457. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  2458. console.groupCollapsed(title);
  2459. if(console.table) {
  2460. console.table(performance);
  2461. }
  2462. else {
  2463. $.each(performance, function(index, data) {
  2464. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  2465. });
  2466. }
  2467. console.groupEnd();
  2468. }
  2469. performance = [];
  2470. }
  2471. },
  2472. invoke: function(query, passedArguments, context) {
  2473. var
  2474. object = instance,
  2475. maxDepth,
  2476. found,
  2477. response
  2478. ;
  2479. passedArguments = passedArguments || queryArguments;
  2480. context = element || context;
  2481. if(typeof query == 'string' && object !== undefined) {
  2482. query = query.split(/[\. ]/);
  2483. maxDepth = query.length - 1;
  2484. $.each(query, function(depth, value) {
  2485. var camelCaseValue = (depth != maxDepth)
  2486. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  2487. : query
  2488. ;
  2489. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  2490. object = object[camelCaseValue];
  2491. }
  2492. else if( object[camelCaseValue] !== undefined ) {
  2493. found = object[camelCaseValue];
  2494. return false;
  2495. }
  2496. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  2497. object = object[value];
  2498. }
  2499. else if( object[value] !== undefined ) {
  2500. found = object[value];
  2501. return false;
  2502. }
  2503. else {
  2504. module.error(error.method, query);
  2505. return false;
  2506. }
  2507. });
  2508. }
  2509. if ( $.isFunction( found ) ) {
  2510. response = found.apply(context, passedArguments);
  2511. }
  2512. else if(found !== undefined) {
  2513. response = found;
  2514. }
  2515. if($.isArray(returnedValue)) {
  2516. returnedValue.push(response);
  2517. }
  2518. else if(returnedValue !== undefined) {
  2519. returnedValue = [returnedValue, response];
  2520. }
  2521. else if(response !== undefined) {
  2522. returnedValue = response;
  2523. }
  2524. return found;
  2525. }
  2526. };
  2527. if(methodInvoked) {
  2528. if(instance === undefined) {
  2529. module.initialize();
  2530. }
  2531. module.invoke(query);
  2532. }
  2533. else {
  2534. if(instance !== undefined) {
  2535. instance.invoke('destroy');
  2536. }
  2537. module.initialize();
  2538. }
  2539. })
  2540. ;
  2541. return (returnedValue !== undefined)
  2542. ? returnedValue
  2543. : $allModules
  2544. ;
  2545. };
  2546. $.fn.dropdown.settings = {
  2547. debug : false,
  2548. verbose : false,
  2549. performance : true,
  2550. on : 'click', // what event should show menu action on item selection
  2551. action : 'activate', // action on item selection (nothing, activate, select, combo, hide, function(){})
  2552. keepOnScreen : true, // Whether dropdown should check whether it is on screen before showing
  2553. match : 'both', // what to match against with search selection (both, text, or label)
  2554. fullTextSearch : false, // search anywhere in value
  2555. placeholder : 'auto', // whether to convert blank <select> values to placeholder text
  2556. preserveHTML : true, // preserve html when selecting value
  2557. sortSelect : false, // sort selection on init
  2558. forceSelection : true, // force a choice on blur with search selection
  2559. allowAdditions : false, // whether multiple select should allow user added values
  2560. maxSelections : false, // When set to a number limits the number of selections to this count
  2561. useLabels : true, // whether multiple select should filter currently active selections from choices
  2562. delimiter : ',', // when multiselect uses normal <input> the values will be delmited with this character
  2563. showOnFocus : true, // show menu on focus
  2564. allowTab : true, // add tabindex to element
  2565. allowCategorySelection : false, // allow elements with sub-menus to be selected
  2566. transition : 'auto', // auto transition will slide down or up based on direction
  2567. duration : 200, // duration of transition
  2568. glyphWidth : 1.0714, // widest glyph width in em (W is 1.0714 em) used to calculate multiselect input width
  2569. // label settings on multi-select
  2570. label: {
  2571. transition : 'scale',
  2572. duration : 200,
  2573. variation : false
  2574. },
  2575. // delay before event
  2576. delay : {
  2577. hide : 300,
  2578. show : 200,
  2579. search : 0,
  2580. touch : 50
  2581. },
  2582. /* Callbacks */
  2583. onChange : function(value, text, $selected){},
  2584. onLabelSelect : function($selectedLabels){},
  2585. onLabelCreate : function(value, text) { return $(this); },
  2586. onNoResults : function(searchTerm) { return true; },
  2587. onShow : function(){},
  2588. onHide : function(){},
  2589. /* Component */
  2590. name : 'Dropdown',
  2591. namespace : 'dropdown',
  2592. message: {
  2593. addResult : 'Add <b>{term}</b>',
  2594. count : '{count} selected',
  2595. maxSelections : 'Max {maxCount} selections',
  2596. noResults : 'No results found.'
  2597. },
  2598. error : {
  2599. action : 'You called a dropdown action that was not defined',
  2600. alreadySetup : 'Once a select has been initialized behaviors must be called on the created ui dropdown',
  2601. labels : 'Allowing user additions currently requires the use of labels.',
  2602. method : 'The method you called is not defined.',
  2603. noTransition : 'This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>'
  2604. },
  2605. regExp : {
  2606. escape : /[-[\]{}()*+?.,\\^$|#\s]/g,
  2607. },
  2608. metadata : {
  2609. defaultText : 'defaultText',
  2610. defaultValue : 'defaultValue',
  2611. placeholderText : 'placeholderText',
  2612. text : 'text',
  2613. value : 'value'
  2614. },
  2615. selector : {
  2616. addition : '.addition',
  2617. dropdown : '.ui.dropdown',
  2618. icon : '> .dropdown.icon',
  2619. input : '> input[type="hidden"], > select',
  2620. item : '.item',
  2621. label : '> .label',
  2622. remove : '> .label > .delete.icon',
  2623. siblingLabel : '.label',
  2624. menu : '.menu',
  2625. message : '.message',
  2626. menuIcon : '.dropdown.icon',
  2627. search : 'input.search, .menu > .search > input',
  2628. text : '> .text:not(.icon)'
  2629. },
  2630. className : {
  2631. active : 'active',
  2632. addition : 'addition',
  2633. animating : 'animating',
  2634. disabled : 'disabled',
  2635. dropdown : 'ui dropdown',
  2636. filtered : 'filtered',
  2637. hidden : 'hidden transition',
  2638. item : 'item',
  2639. label : 'ui label',
  2640. loading : 'loading',
  2641. menu : 'menu',
  2642. message : 'message',
  2643. multiple : 'multiple',
  2644. placeholder : 'default',
  2645. search : 'search',
  2646. selected : 'selected',
  2647. selection : 'selection',
  2648. upward : 'upward',
  2649. visible : 'visible'
  2650. }
  2651. };
  2652. /* Templates */
  2653. $.fn.dropdown.settings.templates = {
  2654. // generates dropdown from select values
  2655. dropdown: function(select) {
  2656. var
  2657. placeholder = select.placeholder || false,
  2658. values = select.values || {},
  2659. html = ''
  2660. ;
  2661. html += '<i class="dropdown icon"></i>';
  2662. if(select.placeholder) {
  2663. html += '<div class="default text">' + placeholder + '</div>';
  2664. }
  2665. else {
  2666. html += '<div class="text"></div>';
  2667. }
  2668. html += '<div class="menu">';
  2669. $.each(select.values, function(index, option) {
  2670. html += '<div class="item" data-value="' + option.value + '">' + option.name + '</div>';
  2671. });
  2672. html += '</div>';
  2673. return html;
  2674. },
  2675. // generates just menu from select
  2676. menu: function(select) {
  2677. var
  2678. placeholder = select.placeholder || false,
  2679. values = select.values || {},
  2680. html = ''
  2681. ;
  2682. $.each(select.values, function(index, option) {
  2683. html += '<div class="item" data-value="' + option.value + '">' + option.name + '</div>';
  2684. });
  2685. return html;
  2686. },
  2687. // generates label for multiselect
  2688. label: function(value, text) {
  2689. return text + '<i class="delete icon"></i>';
  2690. },
  2691. // generates messages like "No results"
  2692. message: function(message) {
  2693. return message;
  2694. },
  2695. // generates user addition to selection menu
  2696. addition: function(choice) {
  2697. return choice;
  2698. }
  2699. };
  2700. })( jQuery, window , document );