dropkick.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. /**
  2. * DropKick
  3. *
  4. * Highly customizable <select> lists
  5. * https://github.com/JamieLottering/DropKick
  6. *
  7. * &copy; 2011 Jamie Lottering <http://github.com/JamieLottering>
  8. * <http://twitter.com/JamieLottering>
  9. *
  10. */
  11. (function ($, window, document) {
  12. var ie6 = false;
  13. document.documentElement.className = document.documentElement.className + ' dk_fouc';
  14. var
  15. // Public methods exposed to $.fn.dropkick()
  16. methods = {},
  17. // Cache every <select> element that gets dropkicked
  18. lists = [],
  19. // Convenience keys for keyboard navigation
  20. keyMap = {
  21. 'left' : 37,
  22. 'up' : 38,
  23. 'right' : 39,
  24. 'down' : 40,
  25. 'enter' : 13
  26. },
  27. // HTML template for the dropdowns
  28. dropdownTemplate = [
  29. '<div class="dk_container" id="dk_container_{{ id }}" tabindex="{{ tabindex }}">',
  30. '<a class="dk_toggle">',
  31. '<span class="dk_label">{{ label }}</span>',
  32. '</a>',
  33. '<div class="dk_options">',
  34. '<ul class="dk_options_inner">',
  35. '</ul>',
  36. '</div>',
  37. '</div>'
  38. ].join(''),
  39. // HTML template for dropdown options
  40. optionTemplate = '<li class="{{ current }}"><a data-dk-dropdown-value="{{ value }}">{{ text }}</a></li>',
  41. // Some nice default values
  42. defaults = {
  43. startSpeed : 1000, // I recommend a high value here, I feel it makes the changes less noticeable to the user
  44. theme : false,
  45. change : false
  46. },
  47. // Make sure we only bind keydown on the document once
  48. keysBound = false
  49. ;
  50. // Called by using $('foo').dropkick();
  51. methods.init = function (settings) {
  52. settings = $.extend({}, defaults, settings);
  53. return this.each(function () {
  54. var
  55. // The current <select> element
  56. $select = $(this),
  57. // Store a reference to the originally selected <option> element
  58. $original = $select.find(':selected').first(),
  59. // Save all of the <option> elements
  60. $options = $select.find('option'),
  61. // We store lots of great stuff using jQuery data
  62. data = $select.data('dropkick') || {},
  63. // This gets applied to the 'dk_container' element
  64. id = $select.attr('id') || $select.attr('name'),
  65. // This gets updated to be equal to the longest <option> element
  66. width = settings.width || $select.outerWidth(),
  67. // Check if we have a tabindex set or not
  68. tabindex = $select.attr('tabindex') ? $select.attr('tabindex') : '',
  69. // The completed dk_container element
  70. $dk = false,
  71. theme
  72. ;
  73. // Dont do anything if we've already setup dropkick on this element
  74. if (data.id) {
  75. return $select;
  76. } else {
  77. data.settings = settings;
  78. data.tabindex = tabindex;
  79. data.id = id;
  80. data.$original = $original;
  81. data.$select = $select;
  82. data.value = _notBlank($select.val()) || _notBlank($original.attr('value'));
  83. data.label = $original.text();
  84. data.options = $options;
  85. }
  86. // Build the dropdown HTML
  87. $dk = _build(dropdownTemplate, data);
  88. // Make the dropdown fixed width if desired
  89. $dk.find('.dk_toggle').css({
  90. 'width' : width + 'px'
  91. });
  92. // Hide the <select> list and place our new one in front of it
  93. $select.before($dk);
  94. // Update the reference to $dk
  95. $dk = $('#dk_container_' + id).fadeIn(settings.startSpeed);
  96. // Save the current theme
  97. theme = settings.theme ? settings.theme : 'default';
  98. $dk.addClass('dk_theme_' + theme);
  99. data.theme = theme;
  100. // Save the updated $dk reference into our data object
  101. data.$dk = $dk;
  102. // Save the dropkick data onto the <select> element
  103. $select.data('dropkick', data);
  104. // Do the same for the dropdown, but add a few helpers
  105. $dk.data('dropkick', data);
  106. lists[lists.length] = $select;
  107. // Focus events
  108. $dk.bind('focus.dropkick', function (e) {
  109. $dk.addClass('dk_focus');
  110. }).bind('blur.dropkick', function (e) {
  111. $dk.removeClass('dk_open dk_focus');
  112. });
  113. $select.bind('change', function (e) {
  114. text = $('option:selected', this).text();
  115. methods.set.call(this, text);
  116. });
  117. setTimeout(function () {
  118. $select.hide();
  119. }, 0);
  120. });
  121. };
  122. methods.set = function (text) {
  123. var
  124. $select = $(this),
  125. list = $select.data('dropkick'),
  126. $dk = list.$dk;
  127. $('.dk_label', $dk).text(text);
  128. $(".dk_options_inner li", $dk).each(function(){
  129. $(this).removeAttr('class');
  130. if ($(this).text() == text){
  131. $(this).attr('class', 'dk_option_current');
  132. }
  133. });
  134. }
  135. // Allows dynamic theme changes
  136. methods.theme = function (newTheme) {
  137. var
  138. $select = $(this),
  139. list = $select.data('dropkick'),
  140. $dk = list.$dk,
  141. oldtheme = 'dk_theme_' + list.theme
  142. ;
  143. $dk.removeClass(oldtheme).addClass('dk_theme_' + newTheme);
  144. list.theme = newTheme;
  145. };
  146. // Reset all <selects and dropdowns in our lists array
  147. methods.reset = function () {
  148. for (var i = 0, l = lists.length; i < l; i++) {
  149. var
  150. listData = lists[i].data('dropkick'),
  151. $dk = listData.$dk,
  152. $current = $dk.find('li').first()
  153. ;
  154. $dk.find('.dk_label').text(listData.label);
  155. $dk.find('.dk_options_inner').animate({ scrollTop: 0 }, 0);
  156. _setCurrent($current, $dk);
  157. _updateFields($current, $dk, true);
  158. }
  159. };
  160. // Expose the plugin
  161. $.fn.dropkick = function (method) {
  162. if (!ie6) {
  163. if (methods[method]) {
  164. return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
  165. } else if (typeof method === 'object' || ! method) {
  166. return methods.init.apply(this, arguments);
  167. }
  168. }
  169. };
  170. // private
  171. function _handleKeyBoardNav(e, $dk) {
  172. var
  173. code = e.keyCode,
  174. data = $dk.data('dropkick'),
  175. options = $dk.find('.dk_options'),
  176. open = $dk.hasClass('dk_open'),
  177. current = $dk.find('.dk_option_current'),
  178. first = options.find('li').first(),
  179. last = options.find('li').last(),
  180. next,
  181. prev
  182. ;
  183. switch (code) {
  184. case keyMap.enter:
  185. if (open) {
  186. _updateFields(current.find('a'), $dk);
  187. _closeDropdown($dk);
  188. } else {
  189. _openDropdown($dk);
  190. }
  191. e.preventDefault();
  192. break;
  193. case keyMap.up:
  194. prev = current.prev('li');
  195. if (open) {
  196. if (prev.length) {
  197. _setCurrent(prev, $dk);
  198. } else {
  199. _setCurrent(last, $dk);
  200. }
  201. } else {
  202. _openDropdown($dk);
  203. }
  204. e.preventDefault();
  205. break;
  206. case keyMap.down:
  207. if (open) {
  208. next = current.next('li').first();
  209. if (next.length) {
  210. _setCurrent(next, $dk);
  211. } else {
  212. _setCurrent(first, $dk);
  213. }
  214. } else {
  215. _openDropdown($dk);
  216. }
  217. e.preventDefault();
  218. break;
  219. default:
  220. break;
  221. }
  222. }
  223. // Update the <select> value, and the dropdown label
  224. function _updateFields(option, $dk, reset) {
  225. var value, label, data;
  226. value = option.attr('data-dk-dropdown-value');
  227. label = option.text();
  228. data = $dk.data('dropkick');
  229. $select = data.$select;
  230. $select.val(value);
  231. $dk.find('.dk_label').text(label);
  232. reset = reset || false;
  233. if (data.settings.change && !reset) {
  234. data.settings.change.call($select, value, label);
  235. }
  236. }
  237. // Set the currently selected option
  238. function _setCurrent($current, $dk) {
  239. $dk.find('.dk_option_current').removeClass('dk_option_current');
  240. $current.addClass('dk_option_current');
  241. _setScrollPos($dk, $current);
  242. }
  243. function _setScrollPos($dk, anchor) {
  244. var height = anchor.prevAll('li').outerHeight() * anchor.prevAll('li').length;
  245. $dk.find('.dk_options_inner').animate({ scrollTop: height + 'px' }, 0);
  246. }
  247. // Close a dropdown
  248. function _closeDropdown($dk) {
  249. $dk.removeClass('dk_open');
  250. }
  251. // Open a dropdown
  252. function _openDropdown($dk) {
  253. var data = $dk.data('dropkick');
  254. $dk.find('.dk_options').css({ top : $dk.find('.dk_toggle').outerHeight() - 1 });
  255. $dk.toggleClass('dk_open');
  256. }
  257. /**
  258. * Turn the dropdownTemplate into a jQuery object and fill in the variables.
  259. */
  260. function _build (tpl, view) {
  261. var
  262. // Template for the dropdown
  263. template = tpl,
  264. // Holder of the dropdowns options
  265. options = [],
  266. $dk
  267. ;
  268. template = template.replace('{{ id }}', view.id);
  269. template = template.replace('{{ label }}', view.label);
  270. template = template.replace('{{ tabindex }}', view.tabindex);
  271. if (view.options && view.options.length) {
  272. for (var i = 0, l = view.options.length; i < l; i++) {
  273. var
  274. $option = $(view.options[i]),
  275. current = 'dk_option_current',
  276. oTemplate = optionTemplate
  277. ;
  278. oTemplate = oTemplate.replace('{{ value }}', $option.val());
  279. oTemplate = oTemplate.replace('{{ current }}', (_notBlank($option.val()) === view.value) ? current : '');
  280. oTemplate = oTemplate.replace('{{ text }}', $option.text());
  281. options[options.length] = oTemplate;
  282. }
  283. }
  284. $dk = $(template);
  285. $dk.find('.dk_options_inner').html(options.join(''));
  286. return $dk;
  287. }
  288. function _notBlank(text) {
  289. return ($.trim(text).length > 0) ? text : false;
  290. }
  291. $(function () {
  292. // Handle click events on the dropdown toggler
  293. $(document).on('click', '.dk_toggle', function (e) {
  294. var $dk = $(this).parents('.dk_container').first();
  295. _openDropdown($dk);
  296. if ("ontouchstart" in window) {
  297. $dk.addClass('dk_touch');
  298. $dk.find('.dk_options_inner').addClass('scrollable vertical');
  299. }
  300. e.preventDefault();
  301. return false;
  302. });
  303. // Handle click events on individual dropdown options
  304. $(document).on('click', '.dk_options a', function (e) {
  305. var
  306. $option = $(this),
  307. $dk = $option.parents('.dk_container').first(),
  308. data = $dk.data('dropkick')
  309. ;
  310. _closeDropdown($dk);
  311. _updateFields($option, $dk);
  312. _setCurrent($option.parent(), $dk);
  313. e.preventDefault();
  314. return false;
  315. });
  316. // Setup keyboard nav
  317. $(document).bind('keydown.dk_nav', function (e) {
  318. var
  319. // Look for an open dropdown...
  320. $open = $('.dk_container.dk_open'),
  321. // Look for a focused dropdown
  322. $focused = $('.dk_container.dk_focus'),
  323. // Will be either $open, $focused, or null
  324. $dk = null
  325. ;
  326. // If we have an open dropdown, key events should get sent to that one
  327. if ($open.length) {
  328. $dk = $open;
  329. } else if ($focused.length && !$open.length) {
  330. // But if we have no open dropdowns, use the focused dropdown instead
  331. $dk = $focused;
  332. }
  333. if ($dk) {
  334. _handleKeyBoardNav(e, $dk);
  335. }
  336. });
  337. });
  338. })(jQuery, window, document);