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.

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