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.

646 lines
18 KiB

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