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.

695 lines
21 KiB

  1. /* ******************************
  2. Semantic dropdown: Dropdown
  3. Author: Jack Lukic
  4. Notes: First Commit May 25, 2013
  5. ****************************** */
  6. ;(function ( $, window, document, undefined ) {
  7. $.fn.dropdown = function(parameters) {
  8. var
  9. $allDropdowns = $(this),
  10. $document = $(document),
  11. settings = ( $.isPlainObject(parameters) )
  12. ? $.extend(true, {}, $.fn.dropdown.settings, parameters)
  13. : $.fn.dropdown.settings,
  14. className = settings.className,
  15. metadata = settings.metadata,
  16. namespace = settings.namespace,
  17. selector = settings.selector,
  18. error = settings.error,
  19. eventNamespace = '.' + namespace,
  20. dropdownNamespace = 'dropdown-' + namespace,
  21. dropdownSelector = $allDropdowns.selector || '',
  22. time = new Date().getTime(),
  23. performance = [],
  24. query = arguments[0],
  25. methodInvoked = (typeof query == 'string'),
  26. queryArguments = [].slice.call(arguments, 1),
  27. invokedResponse
  28. ;
  29. $allDropdowns
  30. .each(function() {
  31. var
  32. $dropdown = $(this),
  33. $item = $dropdown.find(selector.item),
  34. $text = $dropdown.find(selector.text),
  35. $input = $dropdown.find(selector.input),
  36. $menu = $dropdown.children(selector.menu),
  37. isTouchDevice = ('ontouchstart' in document.documentElement),
  38. element = this,
  39. instance = $dropdown.data(dropdownNamespace),
  40. dropdown
  41. ;
  42. dropdown = {
  43. initialize: function() {
  44. dropdown.debug('Initializing dropdown', settings);
  45. if(isTouchDevice) {
  46. $dropdown
  47. .on('touchstart' + eventNamespace, dropdown.event.test.toggle)
  48. ;
  49. }
  50. else if(settings.on == 'click') {
  51. $dropdown
  52. .on('click' + eventNamespace, dropdown.event.test.toggle)
  53. ;
  54. }
  55. else if(settings.on == 'hover') {
  56. $dropdown
  57. .on('mouseenter' + eventNamespace, dropdown.delay.show)
  58. .on('mouseleave' + eventNamespace, dropdown.delay.hide)
  59. ;
  60. }
  61. else {
  62. $dropdown
  63. .on(settings.on + eventNamespace, dropdown.toggle)
  64. ;
  65. }
  66. if(settings.action == 'form') {
  67. dropdown.set.selected();
  68. }
  69. $item
  70. .on('mouseenter' + eventNamespace, dropdown.event.item.mouseenter)
  71. .on('mouseleave' + eventNamespace, dropdown.event.item.mouseleave)
  72. .on(dropdown.get.selectEvent() + eventNamespace, dropdown.event.item.click)
  73. ;
  74. dropdown.instantiate();
  75. },
  76. instantiate: function() {
  77. dropdown.verbose('Storing instance of dropdown', dropdown);
  78. $dropdown
  79. .data(dropdownNamespace, dropdown)
  80. ;
  81. },
  82. destroy: function() {
  83. dropdown.verbose('Destroying previous dropdown for', $dropdown);
  84. $item
  85. .off(eventNamespace)
  86. ;
  87. $dropdown
  88. .off(eventNamespace)
  89. .removeData(dropdownNamespace)
  90. ;
  91. },
  92. event: {
  93. stopPropagation: function(event) {
  94. event.stopPropagation();
  95. },
  96. test: {
  97. toggle: function(event) {
  98. dropdown.determine.intent(event, dropdown.toggle);
  99. event.stopImmediatePropagation();
  100. },
  101. hide: function(event) {
  102. dropdown.determine.intent(event, dropdown.hide);
  103. event.stopPropagation();
  104. }
  105. },
  106. item: {
  107. mouseenter: function(event) {
  108. var
  109. $currentMenu = $(this).find(selector.menu),
  110. $otherMenus = $(this).siblings(selector.item).children(selector.menu)
  111. ;
  112. console.log($currentMenu);
  113. if( $currentMenu.size() > 0 ) {
  114. clearTimeout(dropdown.itemTimer);
  115. dropdown.itemTimer = setTimeout(function() {
  116. dropdown.animate.hide(false, $otherMenus);
  117. dropdown.verbose('Showing sub-menu', $currentMenu);
  118. dropdown.animate.show(false, $currentMenu);
  119. }, settings.delay.show * 2);
  120. }
  121. },
  122. mouseleave: function(event) {
  123. var
  124. $currentMenu = $(this).find(selector.menu)
  125. ;
  126. if($currentMenu.size() > 0) {
  127. clearTimeout(dropdown.itemTimer);
  128. dropdown.itemTimer = setTimeout(function() {
  129. dropdown.verbose('Hiding sub-menu', $currentMenu);
  130. dropdown.animate.hide(false, $currentMenu);
  131. }, settings.delay.hide);
  132. }
  133. },
  134. click: function (event) {
  135. var
  136. $choice = $(this),
  137. text = $choice.data(metadata.text) || $choice.text(),
  138. value = $choice.data(metadata.value) || text
  139. ;
  140. dropdown.verbose('Adding active state to selected item');
  141. $item
  142. .removeClass(className.active)
  143. ;
  144. $choice
  145. .addClass(className.active)
  146. ;
  147. dropdown.determine.selectAction(text, value);
  148. $.proxy(settings.onChange, element)(value, text);
  149. event.stopPropagation();
  150. }
  151. },
  152. resetStyle: function() {
  153. $(this).removeAttr('style');
  154. }
  155. },
  156. determine: {
  157. selectAction: function(text, value) {
  158. dropdown.verbose('Determining action', settings.action);
  159. if( $.isFunction( dropdown[settings.action] ) ) {
  160. dropdown.verbose('Triggering preset action', settings.action);
  161. dropdown[ settings.action ](text, value);
  162. }
  163. else if( $.isFunction(settings.action) ) {
  164. dropdown.verbose('Triggering user action', settings.action);
  165. settings.action(text, value);
  166. }
  167. else {
  168. dropdown.error(error.action);
  169. }
  170. },
  171. intent: function(event, callback) {
  172. dropdown.debug('Determining whether event occurred in dropdown', event.target);
  173. callback = callback || function(){};
  174. if( $(event.target).closest($menu).size() === 0 ) {
  175. dropdown.verbose('Triggering event', callback);
  176. callback();
  177. }
  178. else {
  179. dropdown.verbose('Event occurred in dropdown, canceling callback');
  180. }
  181. }
  182. },
  183. bind: {
  184. intent: function() {
  185. dropdown.verbose('Binding hide intent event to document');
  186. $document
  187. .on(dropdown.get.selectEvent(), dropdown.event.test.hide)
  188. ;
  189. }
  190. },
  191. unbind: {
  192. intent: function() {
  193. dropdown.verbose('Removing hide intent event from document');
  194. $document
  195. .off(dropdown.get.selectEvent())
  196. ;
  197. }
  198. },
  199. nothing: function() {},
  200. changeText: function(text, value) {
  201. dropdown.set.text(text);
  202. dropdown.hide();
  203. },
  204. updateForm: function(text, value) {
  205. dropdown.set.text(text);
  206. dropdown.set.value(value);
  207. dropdown.hide();
  208. },
  209. get: {
  210. selectEvent: function() {
  211. return (isTouchDevice)
  212. ? 'touchstart'
  213. : 'click'
  214. ;
  215. },
  216. text: function() {
  217. return $text.text();
  218. },
  219. value: function() {
  220. return $input.val();
  221. },
  222. item: function(value) {
  223. var
  224. $selectedItem
  225. ;
  226. value = value || $input.val();
  227. $item
  228. .each(function() {
  229. if( $(this).data(metadata.value) == value ) {
  230. $selectedItem = $(this);
  231. }
  232. })
  233. ;
  234. return $selectedItem || false;
  235. }
  236. },
  237. set: {
  238. text: function(text) {
  239. dropdown.debug('Changing text', text, $text);
  240. $text.removeClass(className.placeholder);
  241. $text.text(text);
  242. },
  243. value: function(value) {
  244. dropdown.debug('Adding selected value to hidden input', value, $input);
  245. $input.val(value);
  246. },
  247. active: function() {
  248. $dropdown.addClass(className.active);
  249. },
  250. visible: function() {
  251. $dropdown.addClass(className.visible);
  252. },
  253. selected: function(value) {
  254. var
  255. $selectedItem = dropdown.get.item(value),
  256. selectedText
  257. ;
  258. if($selectedItem) {
  259. dropdown.debug('Setting selected menu item to', $selectedItem);
  260. selectedText = $selectedItem.data(metadata.text) || $selectedItem.text();
  261. $item
  262. .removeClass(className.active)
  263. ;
  264. $selectedItem
  265. .addClass(className.active)
  266. ;
  267. dropdown.set.text(selectedText);
  268. }
  269. }
  270. },
  271. remove: {
  272. active: function() {
  273. $dropdown.removeClass(className.active);
  274. },
  275. visible: function() {
  276. $dropdown.removeClass(className.visible);
  277. }
  278. },
  279. is: {
  280. visible: function($subMenu) {
  281. return ($subMenu)
  282. ? $subMenu.is(':visible')
  283. : $menu.is(':visible')
  284. ;
  285. },
  286. hidden: function($subMenu) {
  287. return ($subMenu)
  288. ? $subMenu.is(':not(:visible)')
  289. : $menu.is(':not(:visible)')
  290. ;
  291. }
  292. },
  293. can: {
  294. click: function() {
  295. return (isTouchDevice || settings.on == 'click');
  296. },
  297. show: function() {
  298. return !$dropdown.hasClass(className.disabled);
  299. }
  300. },
  301. animate: {
  302. show: function(callback, $subMenu) {
  303. var
  304. $currentMenu = $subMenu || $menu
  305. ;
  306. callback = callback || function(){};
  307. if( dropdown.is.hidden($currentMenu) ) {
  308. dropdown.verbose('Doing menu show animation', $currentMenu);
  309. if(settings.transition == 'none') {
  310. callback();
  311. }
  312. else if($.fn.transition !== undefined) {
  313. $currentMenu.transition(settings.transition + ' in', settings.duration, callback);
  314. }
  315. else if(settings.transition == 'slide down') {
  316. $currentMenu
  317. .hide()
  318. .clearQueue()
  319. .children()
  320. .clearQueue()
  321. .css('opacity', 0)
  322. .delay(50)
  323. .animate({
  324. opacity : 1
  325. }, settings.duration, 'easeOutQuad', dropdown.event.resetStyle)
  326. .end()
  327. .slideDown(100, 'easeOutQuad', function() {
  328. $.proxy(dropdown.event.resetStyle, this)();
  329. callback();
  330. })
  331. ;
  332. }
  333. else if(settings.transition == 'fade') {
  334. $currentMenu
  335. .hide()
  336. .clearQueue()
  337. .fadeIn(settings.duration, function() {
  338. $.proxy(dropdown.event.resetStyle, this)();
  339. callback();
  340. })
  341. ;
  342. }
  343. else {
  344. dropdown.error(error.transition);
  345. }
  346. }
  347. },
  348. hide: function(callback, $subMenu) {
  349. var
  350. $currentMenu = $subMenu || $menu
  351. ;
  352. callback = callback || function(){};
  353. if(dropdown.is.visible($currentMenu) ) {
  354. dropdown.verbose('Doing menu hide animation', $currentMenu);
  355. if($.fn.transition !== undefined) {
  356. $currentMenu.transition(settings.transition + ' out', settings.duration, callback);
  357. }
  358. else if(settings.transition == 'none') {
  359. callback();
  360. }
  361. else if(settings.transition == 'slide down') {
  362. $currentMenu
  363. .show()
  364. .clearQueue()
  365. .children()
  366. .clearQueue()
  367. .css('opacity', 1)
  368. .animate({
  369. opacity : 0
  370. }, 100, 'easeOutQuad', dropdown.event.resetStyle)
  371. .end()
  372. .delay(50)
  373. .slideUp(100, 'easeOutQuad', function() {
  374. $.proxy(dropdown.event.resetStyle, this)();
  375. callback();
  376. })
  377. ;
  378. }
  379. else if(settings.transition == 'fade') {
  380. $currentMenu
  381. .show()
  382. .clearQueue()
  383. .fadeOut(150, function() {
  384. $.proxy(dropdown.event.resetStyle, this)();
  385. callback();
  386. })
  387. ;
  388. }
  389. else {
  390. dropdown.error(error.transition);
  391. }
  392. }
  393. }
  394. },
  395. show: function() {
  396. dropdown.debug('Checking if dropdown can show');
  397. if( !dropdown.is.visible() ) {
  398. dropdown.hideOthers();
  399. dropdown.set.active();
  400. dropdown.animate.show(dropdown.set.visible);
  401. if( dropdown.can.click() ) {
  402. dropdown.bind.intent();
  403. }
  404. $.proxy(settings.onShow, element)();
  405. }
  406. },
  407. hide: function() {
  408. if( !dropdown.is.hidden() ) {
  409. dropdown.debug('Hiding dropdown');
  410. if( dropdown.can.click() ) {
  411. dropdown.unbind.intent();
  412. }
  413. dropdown.remove.active();
  414. dropdown.animate.hide(dropdown.remove.visible);
  415. $.proxy(settings.onHide, element)();
  416. }
  417. },
  418. delay: {
  419. show: function() {
  420. dropdown.verbose('Delaying show event to ensure user intent');
  421. clearTimeout(dropdown.timer);
  422. dropdown.timer = setTimeout(dropdown.show, settings.delay.show);
  423. },
  424. hide: function() {
  425. dropdown.verbose('Delaying hide event to ensure user intent');
  426. clearTimeout(dropdown.timer);
  427. dropdown.timer = setTimeout(dropdown.hide, settings.delay.hide);
  428. }
  429. },
  430. hideOthers: function() {
  431. dropdown.verbose('Finding other dropdowns to hide');
  432. $allDropdowns
  433. .not($dropdown)
  434. .has(selector.menu + ':visible')
  435. .dropdown('hide')
  436. ;
  437. },
  438. toggle: function() {
  439. dropdown.verbose('Toggling menu visibility');
  440. if( dropdown.is.hidden() ) {
  441. dropdown.show();
  442. }
  443. else {
  444. dropdown.hide();
  445. }
  446. },
  447. setting: function(name, value) {
  448. if(value !== undefined) {
  449. if( $.isPlainObject(name) ) {
  450. $.extend(true, settings, name);
  451. }
  452. else {
  453. settings[name] = value;
  454. }
  455. }
  456. else {
  457. return settings[name];
  458. }
  459. },
  460. internal: function(name, value) {
  461. if(value !== undefined) {
  462. if( $.isPlainObject(name) ) {
  463. $.extend(true, dropdown, name);
  464. }
  465. else {
  466. dropdown[name] = value;
  467. }
  468. }
  469. else {
  470. return dropdown[name];
  471. }
  472. },
  473. debug: function() {
  474. if(settings.debug) {
  475. if(settings.performance) {
  476. dropdown.performance.log(arguments);
  477. }
  478. else {
  479. dropdown.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  480. dropdown.debug.apply(console, arguments);
  481. }
  482. }
  483. },
  484. verbose: function() {
  485. if(settings.verbose && settings.debug) {
  486. if(settings.performance) {
  487. dropdown.performance.log(arguments);
  488. }
  489. else {
  490. dropdown.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  491. dropdown.verbose.apply(console, arguments);
  492. }
  493. }
  494. },
  495. error: function() {
  496. dropdown.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  497. dropdown.error.apply(console, arguments);
  498. },
  499. performance: {
  500. log: function(message) {
  501. var
  502. currentTime,
  503. executionTime,
  504. previousTime
  505. ;
  506. if(settings.performance) {
  507. currentTime = new Date().getTime();
  508. previousTime = time || currentTime;
  509. executionTime = currentTime - previousTime;
  510. time = currentTime;
  511. performance.push({
  512. 'Element' : element,
  513. 'Name' : message[0],
  514. 'Arguments' : [].slice.call(message, 1) || '',
  515. 'Execution Time' : executionTime
  516. });
  517. }
  518. clearTimeout(dropdown.performance.timer);
  519. dropdown.performance.timer = setTimeout(dropdown.performance.display, 100);
  520. },
  521. display: function() {
  522. var
  523. title = settings.name + ':',
  524. totalTime = 0
  525. ;
  526. time = false;
  527. clearTimeout(dropdown.performance.timer);
  528. $.each(performance, function(index, data) {
  529. totalTime += data['Execution Time'];
  530. });
  531. title += ' ' + totalTime + 'ms';
  532. if(dropdownSelector) {
  533. title += ' \'' + dropdownSelector + '\'';
  534. }
  535. title += ' ' + '(' + $allDropdowns.size() + ')';
  536. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  537. console.groupCollapsed(title);
  538. if(console.table) {
  539. console.table(performance);
  540. }
  541. else {
  542. $.each(performance, function(index, data) {
  543. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  544. });
  545. }
  546. console.groupEnd();
  547. }
  548. performance = [];
  549. }
  550. },
  551. invoke: function(query, passedArguments, context) {
  552. var
  553. maxDepth,
  554. found
  555. ;
  556. passedArguments = passedArguments || queryArguments;
  557. context = element || context;
  558. if(typeof query == 'string' && instance !== undefined) {
  559. query = query.split(/[\. ]/);
  560. maxDepth = query.length - 1;
  561. $.each(query, function(depth, value) {
  562. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  563. instance = instance[value];
  564. }
  565. else if( instance[value] !== undefined ) {
  566. found = instance[value];
  567. }
  568. else {
  569. dropdown.error(error.method);
  570. }
  571. });
  572. }
  573. if ( $.isFunction( found ) ) {
  574. return found.apply(context, passedArguments);
  575. }
  576. return found || false;
  577. }
  578. };
  579. if(methodInvoked) {
  580. if(instance === undefined) {
  581. dropdown.initialize();
  582. }
  583. invokedResponse = dropdown.invoke(query);
  584. }
  585. else {
  586. if(instance !== undefined) {
  587. dropdown.destroy();
  588. }
  589. dropdown.initialize();
  590. }
  591. })
  592. ;
  593. return (invokedResponse)
  594. ? invokedResponse
  595. : this
  596. ;
  597. };
  598. $.fn.dropdown.settings = {
  599. name : 'Dropdown',
  600. namespace : 'dropdown',
  601. verbose : true,
  602. debug : true,
  603. performance : true,
  604. on : 'click',
  605. action : 'hide',
  606. delay: {
  607. show: 200,
  608. hide: 300
  609. },
  610. transition : 'slide down',
  611. duration : 250,
  612. onChange : function(){},
  613. onShow : function(){},
  614. onHide : function(){},
  615. error : {
  616. action : 'You called a dropdown action that was not defined',
  617. method : 'The method you called is not defined.',
  618. transition : 'The requested transition was not found'
  619. },
  620. metadata: {
  621. text : 'text',
  622. value : 'value'
  623. },
  624. selector : {
  625. menu : '.menu',
  626. item : '.menu > .item',
  627. text : '> .text',
  628. input : '> input[type="hidden"]'
  629. },
  630. className : {
  631. active : 'active',
  632. placeholder : 'default',
  633. disabled : 'disabled',
  634. visible : 'visible'
  635. }
  636. };
  637. })( jQuery, window , document );