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.

706 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(':animated, :visible')
  284. : $menu.is(':animated, :visible')
  285. ;
  286. },
  287. hidden: function($subMenu) {
  288. return ($subMenu)
  289. ? $subMenu.is(':not(:animated, :visible)')
  290. : $menu.is(':not(:animated, :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({
  315. animation : settings.transition + ' in',
  316. duration : settings.duration,
  317. complete : callback,
  318. queue : false
  319. })
  320. }
  321. else if(settings.transition == 'slide down') {
  322. $currentMenu
  323. .hide()
  324. .clearQueue()
  325. .children()
  326. .clearQueue()
  327. .css('opacity', 0)
  328. .delay(50)
  329. .animate({
  330. opacity : 1
  331. }, settings.duration, 'easeOutQuad', dropdown.event.resetStyle)
  332. .end()
  333. .slideDown(100, 'easeOutQuad', function() {
  334. $.proxy(dropdown.event.resetStyle, this)();
  335. callback();
  336. })
  337. ;
  338. }
  339. else if(settings.transition == 'fade') {
  340. $currentMenu
  341. .hide()
  342. .clearQueue()
  343. .fadeIn(settings.duration, function() {
  344. $.proxy(dropdown.event.resetStyle, this)();
  345. callback();
  346. })
  347. ;
  348. }
  349. else {
  350. dropdown.error(error.transition);
  351. }
  352. }
  353. },
  354. hide: function(callback, $subMenu) {
  355. var
  356. $currentMenu = $subMenu || $menu
  357. ;
  358. callback = callback || function(){};
  359. if(dropdown.is.visible($currentMenu) ) {
  360. dropdown.verbose('Doing menu hide animation', $currentMenu);
  361. if($.fn.transition !== undefined) {
  362. $currentMenu.transition({
  363. animation : settings.transition + ' out',
  364. duration : settings.duration,
  365. complete : callback,
  366. queue : false
  367. })
  368. }
  369. else if(settings.transition == 'none') {
  370. callback();
  371. }
  372. else if(settings.transition == 'slide down') {
  373. $currentMenu
  374. .show()
  375. .clearQueue()
  376. .children()
  377. .clearQueue()
  378. .css('opacity', 1)
  379. .animate({
  380. opacity : 0
  381. }, 100, 'easeOutQuad', dropdown.event.resetStyle)
  382. .end()
  383. .delay(50)
  384. .slideUp(100, 'easeOutQuad', function() {
  385. $.proxy(dropdown.event.resetStyle, this)();
  386. callback();
  387. })
  388. ;
  389. }
  390. else if(settings.transition == 'fade') {
  391. $currentMenu
  392. .show()
  393. .clearQueue()
  394. .fadeOut(150, function() {
  395. $.proxy(dropdown.event.resetStyle, this)();
  396. callback();
  397. })
  398. ;
  399. }
  400. else {
  401. dropdown.error(error.transition);
  402. }
  403. }
  404. }
  405. },
  406. show: function() {
  407. dropdown.debug('Checking if dropdown can show');
  408. if( dropdown.is.hidden() ) {
  409. dropdown.hideOthers();
  410. dropdown.set.active();
  411. dropdown.animate.show(dropdown.set.visible);
  412. if( dropdown.can.click() ) {
  413. dropdown.bind.intent();
  414. }
  415. $.proxy(settings.onShow, element)();
  416. }
  417. },
  418. hide: function() {
  419. if( dropdown.is.visible() ) {
  420. dropdown.debug('Hiding dropdown');
  421. if( dropdown.can.click() ) {
  422. dropdown.unbind.intent();
  423. }
  424. dropdown.remove.active();
  425. dropdown.animate.hide(dropdown.remove.visible);
  426. $.proxy(settings.onHide, element)();
  427. }
  428. },
  429. delay: {
  430. show: function() {
  431. dropdown.verbose('Delaying show event to ensure user intent');
  432. clearTimeout(dropdown.timer);
  433. dropdown.timer = setTimeout(dropdown.show, settings.delay.show);
  434. },
  435. hide: function() {
  436. dropdown.verbose('Delaying hide event to ensure user intent');
  437. clearTimeout(dropdown.timer);
  438. dropdown.timer = setTimeout(dropdown.hide, settings.delay.hide);
  439. }
  440. },
  441. hideOthers: function() {
  442. dropdown.verbose('Finding other dropdowns to hide');
  443. $allDropdowns
  444. .not($dropdown)
  445. .has(selector.menu + ':visible')
  446. .dropdown('hide')
  447. ;
  448. },
  449. toggle: function() {
  450. dropdown.verbose('Toggling menu visibility');
  451. if( dropdown.is.hidden() ) {
  452. dropdown.show();
  453. }
  454. else {
  455. dropdown.hide();
  456. }
  457. },
  458. setting: function(name, value) {
  459. if(value !== undefined) {
  460. if( $.isPlainObject(name) ) {
  461. $.extend(true, settings, name);
  462. }
  463. else {
  464. settings[name] = value;
  465. }
  466. }
  467. else {
  468. return settings[name];
  469. }
  470. },
  471. internal: function(name, value) {
  472. if(value !== undefined) {
  473. if( $.isPlainObject(name) ) {
  474. $.extend(true, dropdown, name);
  475. }
  476. else {
  477. dropdown[name] = value;
  478. }
  479. }
  480. else {
  481. return dropdown[name];
  482. }
  483. },
  484. debug: function() {
  485. if(settings.debug) {
  486. if(settings.performance) {
  487. dropdown.performance.log(arguments);
  488. }
  489. else {
  490. dropdown.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  491. dropdown.debug.apply(console, arguments);
  492. }
  493. }
  494. },
  495. verbose: function() {
  496. if(settings.verbose && settings.debug) {
  497. if(settings.performance) {
  498. dropdown.performance.log(arguments);
  499. }
  500. else {
  501. dropdown.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  502. dropdown.verbose.apply(console, arguments);
  503. }
  504. }
  505. },
  506. error: function() {
  507. dropdown.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  508. dropdown.error.apply(console, arguments);
  509. },
  510. performance: {
  511. log: function(message) {
  512. var
  513. currentTime,
  514. executionTime,
  515. previousTime
  516. ;
  517. if(settings.performance) {
  518. currentTime = new Date().getTime();
  519. previousTime = time || currentTime;
  520. executionTime = currentTime - previousTime;
  521. time = currentTime;
  522. performance.push({
  523. 'Element' : element,
  524. 'Name' : message[0],
  525. 'Arguments' : [].slice.call(message, 1) || '',
  526. 'Execution Time' : executionTime
  527. });
  528. }
  529. clearTimeout(dropdown.performance.timer);
  530. dropdown.performance.timer = setTimeout(dropdown.performance.display, 100);
  531. },
  532. display: function() {
  533. var
  534. title = settings.name + ':',
  535. totalTime = 0
  536. ;
  537. time = false;
  538. clearTimeout(dropdown.performance.timer);
  539. $.each(performance, function(index, data) {
  540. totalTime += data['Execution Time'];
  541. });
  542. title += ' ' + totalTime + 'ms';
  543. if(dropdownSelector) {
  544. title += ' \'' + dropdownSelector + '\'';
  545. }
  546. title += ' ' + '(' + $allDropdowns.size() + ')';
  547. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  548. console.groupCollapsed(title);
  549. if(console.table) {
  550. console.table(performance);
  551. }
  552. else {
  553. $.each(performance, function(index, data) {
  554. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  555. });
  556. }
  557. console.groupEnd();
  558. }
  559. performance = [];
  560. }
  561. },
  562. invoke: function(query, passedArguments, context) {
  563. var
  564. maxDepth,
  565. found
  566. ;
  567. passedArguments = passedArguments || queryArguments;
  568. context = element || context;
  569. if(typeof query == 'string' && instance !== undefined) {
  570. query = query.split(/[\. ]/);
  571. maxDepth = query.length - 1;
  572. $.each(query, function(depth, value) {
  573. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  574. instance = instance[value];
  575. }
  576. else if( instance[value] !== undefined ) {
  577. found = instance[value];
  578. }
  579. else {
  580. dropdown.error(error.method);
  581. }
  582. });
  583. }
  584. if ( $.isFunction( found ) ) {
  585. return found.apply(context, passedArguments);
  586. }
  587. return found || false;
  588. }
  589. };
  590. if(methodInvoked) {
  591. if(instance === undefined) {
  592. dropdown.initialize();
  593. }
  594. invokedResponse = dropdown.invoke(query);
  595. }
  596. else {
  597. if(instance !== undefined) {
  598. dropdown.destroy();
  599. }
  600. dropdown.initialize();
  601. }
  602. })
  603. ;
  604. return (invokedResponse)
  605. ? invokedResponse
  606. : this
  607. ;
  608. };
  609. $.fn.dropdown.settings = {
  610. name : 'Dropdown',
  611. namespace : 'dropdown',
  612. verbose : true,
  613. debug : true,
  614. performance : true,
  615. on : 'click',
  616. action : 'hide',
  617. delay: {
  618. show: 200,
  619. hide: 300
  620. },
  621. transition : 'slide down',
  622. duration : 250,
  623. onChange : function(){},
  624. onShow : function(){},
  625. onHide : function(){},
  626. error : {
  627. action : 'You called a dropdown action that was not defined',
  628. method : 'The method you called is not defined.',
  629. transition : 'The requested transition was not found'
  630. },
  631. metadata: {
  632. text : 'text',
  633. value : 'value'
  634. },
  635. selector : {
  636. menu : '.menu',
  637. item : '.menu > .item',
  638. text : '> .text',
  639. input : '> input[type="hidden"]'
  640. },
  641. className : {
  642. active : 'active',
  643. placeholder : 'default',
  644. disabled : 'disabled',
  645. visible : 'visible'
  646. }
  647. };
  648. })( jQuery, window , document );