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

3442 lines
119 KiB

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