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.

594 lines
18 KiB

  1. /* ******************************
  2. Module - Simple Tab Navigation
  3. Author: Jack Lukic
  4. Notes: First Commit Aug 15, 2012
  5. History based tab navigation
  6. ****************************** */
  7. ;(function ($, window, document, undefined) {
  8. $.fn.tabNavigation = function(parameters) {
  9. var
  10. settings = $.extend(true, {}, $.fn.tabNavigation.settings, parameters),
  11. $module = $(this),
  12. $tabs = $(settings.context).find(settings.selector.tabs),
  13. firstLoad = true,
  14. cache = {},
  15. recursionDepth = 0,
  16. activeTabPath,
  17. parameterArray,
  18. historyEvent,
  19. element = this,
  20. time = new Date().getTime(),
  21. performance = [],
  22. moduleSelector = $module.selector || '',
  23. className = settings.className,
  24. metadata = settings.metadata,
  25. namespace = settings.namespace,
  26. errors = settings.errors,
  27. instance = $module.data('module'),
  28. query = arguments[0],
  29. methodInvoked = (instance !== undefined && typeof query == 'string'),
  30. passedArguments = [].slice.call(arguments, 1),
  31. module,
  32. invokedResponse
  33. ;
  34. module = {
  35. initialize: function() {
  36. module.debug('Initializing Tabs', $module);
  37. // attach history events
  38. if(settings.history) {
  39. if( $.address === undefined ) {
  40. module.error(errors.state);
  41. return false;
  42. }
  43. else if(settings.path === false) {
  44. module.error(errors.path);
  45. return false;
  46. }
  47. else {
  48. module.verbose('Address library found adding state change event');
  49. $.address
  50. .state(settings.path)
  51. .change(module.event.history.change)
  52. ;
  53. }
  54. }
  55. // attach events if navigation wasn't set to window
  56. if( !$.isWindow( $module.get(0) ) ) {
  57. $module
  58. .on('click.' + namespace, module.event.click)
  59. ;
  60. }
  61. $module
  62. .data('module', module)
  63. ;
  64. },
  65. destroy: function() {
  66. module.debug('Destroying tabs', $module);
  67. $module
  68. .off('.' + namespace)
  69. ;
  70. },
  71. event: {
  72. click: function() {
  73. module.debug('Navigation clicked');
  74. var
  75. tabPath = $(this).data(metadata.tab)
  76. ;
  77. if(tabPath !== undefined) {
  78. if(tabPath !== activeTabPath) {
  79. if(settings.history) {
  80. $.address.value(tabPath);
  81. }
  82. else {
  83. module.change(tabPath);
  84. }
  85. }
  86. }
  87. else {
  88. module.debug('No tab specified');
  89. }
  90. },
  91. history: {
  92. change: function(event) {
  93. var
  94. tabPath = event.pathNames.join('/') || module.get.initialPath(),
  95. pageTitle = settings.templates.determineTitle(tabPath) || false
  96. ;
  97. module.debug('History change event', tabPath, event);
  98. historyEvent = event;
  99. if(tabPath !== undefined) {
  100. module.change(tabPath);
  101. }
  102. if(pageTitle) {
  103. $.address.title(pageTitle);
  104. }
  105. }
  106. }
  107. },
  108. refresh: function() {
  109. if(activeTabPath) {
  110. module.debug('Refreshing tab', activeTabPath);
  111. module.change(activeTabPath);
  112. }
  113. },
  114. cache: {
  115. read: function(tabPath) {
  116. return (tabPath !== undefined)
  117. ? cache[tabPath]
  118. : cache
  119. ;
  120. },
  121. add: function(tabPath, content) {
  122. tabPath = tabPath || activeTabPath;
  123. module.debug('Adding cached content for', tabPath);
  124. cache[tabPath] = content;
  125. },
  126. remove: function(tabPath) {
  127. tabPath = tabPath || activeTabPath;
  128. module.debug('Removing cached content for', tabPath);
  129. delete cache[tabPath];
  130. }
  131. },
  132. change: function(tabPath) {
  133. var
  134. pathArray = module.get.defaultPathArray(tabPath)
  135. ;
  136. module.deactivate.all();
  137. $.each(pathArray, function(index, tab) {
  138. var
  139. currentPathArray = pathArray.slice(0, index + 1),
  140. currentPath = module.utils.arrayToPath(currentPathArray),
  141. isLastTab = (module.utils.last(pathArray) == currentPath),
  142. isTab = module.is.tab(currentPath),
  143. isParam = !(isTab),
  144. pushStateAvailable = (window.history && window.history.pushState),
  145. shouldIgnoreLoad = (pushStateAvailable && settings.ignoreFirstLoad && firstLoad),
  146. remoteContent = $.isPlainObject(settings.apiSettings),
  147. $tab = module.get.tabElement(currentPath)
  148. ;
  149. module.verbose('Looking for tab', tab);
  150. if(isParam) {
  151. module.verbose('Tab is not found, assuming it is a parameter', tab);
  152. return true;
  153. }
  154. else if(isTab) {
  155. // scope up
  156. module.verbose('Tab was found', tab);
  157. activeTabPath = currentPath;
  158. parameterArray = module.utils.filterArray(pathArray, currentPathArray);
  159. if(isLastTab && remoteContent) {
  160. if(!shouldIgnoreLoad) {
  161. module.activate.navigation(currentPath);
  162. module.content.fetch(currentPath, settings.onTabLoad);
  163. }
  164. else {
  165. module.debug('Ignoring remote content on first tab load', currentPath);
  166. firstLoad = false;
  167. cache[tabPath] = $tab.html();
  168. module.activate.all(currentPath);
  169. $.proxy(settings.onTabInit, $tab)(currentPath, parameterArray, historyEvent);
  170. }
  171. }
  172. else {
  173. module.debug('Opened tab', currentPath);
  174. module.activate.all(currentPath);
  175. $.proxy(settings.onTabLoad, $tab)(currentPath, parameterArray, historyEvent);
  176. }
  177. }
  178. });
  179. },
  180. content: {
  181. fetch: function(tabPath) {
  182. var
  183. $tab = module.get.tabElement(tabPath),
  184. cachedContent = cache[tabPath] || false,
  185. apiSettings = {
  186. dataType : 'html',
  187. stateContext : $tab,
  188. success : function(response) {
  189. cache[tabPath] = response;
  190. module.content.update(tabPath, response);
  191. if(tabPath == activeTabPath) {
  192. module.debug('Content loaded', tabPath);
  193. module.activate.tab(tabPath);
  194. }
  195. else {
  196. module.debug('Content loaded in background', tabPath);
  197. }
  198. $.proxy(settings.onTabInit, $tab)(tabPath, parameterArray, historyEvent);
  199. },
  200. urlData: { tab: tabPath }
  201. },
  202. request = $tab.data(metadata.promise) || false,
  203. existingRequest = ( request && request.state() === 'pending' )
  204. ;
  205. if(settings.cache && cachedContent) {
  206. module.debug('Showing existing content', tabPath);
  207. // module.content.update(tabPath, cachedContent);
  208. module.activate.tab(tabPath);
  209. $.proxy(settings.onTabLoad, $tab)(tabPath, parameterArray, historyEvent);
  210. }
  211. else if(existingRequest) {
  212. module.debug('Content is already loading', tabPath);
  213. $tab
  214. .addClass(className.loading)
  215. ;
  216. }
  217. else if($.api !== undefined) {
  218. module.debug('Retrieving content', tabPath);
  219. $.api( $.extend(true, {}, settings.apiSettings, apiSettings) );
  220. }
  221. else {
  222. module.error(errors.api);
  223. }
  224. },
  225. update: function(tabPath, html) {
  226. module.debug('Updating html for', tabPath);
  227. var
  228. $tab = module.get.tabElement(tabPath)
  229. ;
  230. $tab
  231. .html(html)
  232. ;
  233. }
  234. },
  235. activate: {
  236. all: function(tabPath) {
  237. module.activate.tab(tabPath);
  238. module.activate.navigation(tabPath);
  239. },
  240. tab: function(tabPath) {
  241. var
  242. $tab = module.get.tabElement(tabPath)
  243. ;
  244. module.verbose('Showing tab content for', $tab);
  245. $tab.addClass(className.active);
  246. },
  247. navigation: function(tabPath) {
  248. var
  249. $nav = module.get.navElement(tabPath)
  250. ;
  251. module.verbose('Activating tab navigation for', $nav);
  252. $nav.addClass(className.active);
  253. }
  254. },
  255. deactivate: {
  256. all: function() {
  257. module.deactivate.navigation();
  258. module.deactivate.tabs();
  259. },
  260. navigation: function() {
  261. $module
  262. .removeClass(className.active)
  263. ;
  264. },
  265. tabs: function() {
  266. $tabs
  267. .removeClass(className.active + ' ' + className.loading)
  268. ;
  269. }
  270. },
  271. is: {
  272. tab: function(tabName) {
  273. return ( module.get.tabElement(tabName).size() > 0 );
  274. }
  275. },
  276. get: {
  277. initialPath: function() {
  278. return $module.eq(0).data(metadata.tab) || $tabs.eq(0).data(metadata.tab);
  279. },
  280. // adds default tabs to tab path
  281. defaultPathArray: function(tabPath) {
  282. return module.utils.pathToArray( module.get.defaultPath(tabPath) );
  283. },
  284. defaultPath: function(tabPath) {
  285. var
  286. $defaultNav = $module.filter('[data-' + metadata.tab + '^="' + tabPath + '/"]').eq(0),
  287. defaultTab = $defaultNav.data(metadata.tab) || false
  288. ;
  289. if( defaultTab ) {
  290. module.debug('Found default tab', defaultTab);
  291. if(recursionDepth < settings.maxDepth) {
  292. recursionDepth++;
  293. return module.get.defaultPath(defaultTab);
  294. }
  295. module.error(errors.recursion);
  296. }
  297. recursionDepth = 0;
  298. return tabPath;
  299. },
  300. navElement: function(tabPath) {
  301. tabPath = tabPath || activeTabPath;
  302. return $module.filter('[data-' + metadata.tab + '="' + tabPath + '"]');
  303. },
  304. tabElement: function(tabPath) {
  305. var
  306. $fullPathTab,
  307. $simplePathTab,
  308. tabPathArray,
  309. lastTab
  310. ;
  311. tabPath = tabPath || activeTabPath;
  312. tabPathArray = module.utils.pathToArray(tabPath);
  313. lastTab = module.utils.last(tabPathArray);
  314. $fullPathTab = $tabs.filter('[data-' + metadata.tab + '="' + lastTab + '"]');
  315. $simplePathTab = $tabs.filter('[data-' + metadata.tab + '="' + tabPath + '"]');
  316. return ($fullPathTab.size() > 0)
  317. ? $fullPathTab
  318. : $simplePathTab
  319. ;
  320. },
  321. tab: function() {
  322. return activeTabPath;
  323. }
  324. },
  325. utils: {
  326. filterArray: function(keepArray, removeArray) {
  327. return $.grep(keepArray, function(keepValue) {
  328. return ( $.inArray(keepValue, removeArray) == -1);
  329. });
  330. },
  331. last: function(array) {
  332. return $.isArray(array)
  333. ? array[ array.length - 1]
  334. : false
  335. ;
  336. },
  337. pathToArray: function(pathName) {
  338. if(pathName === undefined) {
  339. pathName = activeTabPath;
  340. }
  341. return typeof pathName == 'string'
  342. ? pathName.split('/')
  343. : [pathName]
  344. ;
  345. },
  346. arrayToPath: function(pathArray) {
  347. return $.isArray(pathArray)
  348. ? pathArray.join('/')
  349. : false
  350. ;
  351. }
  352. },
  353. setting: function(name, value) {
  354. module.debug('Changing setting', name, value);
  355. if(value !== undefined) {
  356. if( $.isPlainObject(name) ) {
  357. $.extend(true, settings, name);
  358. }
  359. else {
  360. settings[name] = value;
  361. }
  362. }
  363. else {
  364. return settings[name];
  365. }
  366. },
  367. internal: function(name, value) {
  368. module.debug('Changing internal', name, value);
  369. if(value !== undefined) {
  370. if( $.isPlainObject(name) ) {
  371. $.extend(true, module, name);
  372. }
  373. else {
  374. module[name] = value;
  375. }
  376. }
  377. else {
  378. return module[name];
  379. }
  380. },
  381. verbose: function() {
  382. if(settings.verbose && settings.debug) {
  383. module.performance.log(arguments[0]);
  384. module.verbose = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  385. }
  386. },
  387. debug: function() {
  388. if(settings.debug) {
  389. module.performance.log(arguments[0]);
  390. module.debug = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  391. }
  392. },
  393. error: function() {
  394. if(console.log !== undefined) {
  395. module.error = Function.prototype.bind.call(console.error, console, settings.moduleName + ':');
  396. }
  397. },
  398. performance: {
  399. log: function(message) {
  400. var
  401. currentTime,
  402. executionTime,
  403. previousTime
  404. ;
  405. if(settings.performance) {
  406. currentTime = new Date().getTime();
  407. previousTime = time || currentTime,
  408. executionTime = currentTime - previousTime;
  409. time = currentTime;
  410. performance.push({
  411. 'Element' : element,
  412. 'Name' : message[0],
  413. 'Arguments' : message[1] || '',
  414. 'Execution Time' : executionTime
  415. });
  416. }
  417. clearTimeout(module.performance.timer);
  418. module.performance.timer = setTimeout(module.performance.display, 100);
  419. },
  420. display: function() {
  421. var
  422. title = settings.moduleName + ':',
  423. totalTime = 0
  424. ;
  425. time = false;
  426. $.each(performance, function(index, data) {
  427. totalTime += data['Execution Time'];
  428. });
  429. title += ' ' + totalTime + 'ms';
  430. if(moduleSelector) {
  431. title += ' \'' + moduleSelector + '\'';
  432. }
  433. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  434. console.groupCollapsed(title);
  435. if(console.table) {
  436. console.table(performance);
  437. }
  438. else {
  439. $.each(performance, function(index, data) {
  440. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  441. });
  442. }
  443. console.groupEnd();
  444. }
  445. performance = [];
  446. }
  447. },
  448. invoke: function(query, context, passedArguments) {
  449. var
  450. maxDepth,
  451. found
  452. ;
  453. passedArguments = passedArguments || [].slice.call( arguments, 2 );
  454. if(typeof query == 'string' && instance !== undefined) {
  455. query = query.split('.');
  456. maxDepth = query.length - 1;
  457. $.each(query, function(depth, value) {
  458. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  459. instance = instance[value];
  460. return true;
  461. }
  462. else if( instance[value] !== undefined ) {
  463. found = instance[value];
  464. return true;
  465. }
  466. module.error(settings.errors.method);
  467. return false;
  468. });
  469. }
  470. if ( $.isFunction( found ) ) {
  471. return found.apply(context, passedArguments);
  472. }
  473. // return retrieved variable or chain
  474. return found;
  475. }
  476. };
  477. if(methodInvoked) {
  478. if(instance === undefined) {
  479. module.initialize();
  480. }
  481. invokedResponse = module.invoke(query);
  482. }
  483. else {
  484. if(instance !== undefined) {
  485. module.destroy();
  486. }
  487. module.initialize();
  488. }
  489. return (invokedResponse !== undefined)
  490. ? invokedResponse
  491. : this
  492. ;
  493. };
  494. // shortcut for tabbed content with no defined navigation
  495. $.tabNavigation = function(settings) {
  496. $(window).tabNavigation(settings);
  497. };
  498. $.fn.tabNavigation.settings = {
  499. moduleName : 'Tab Module',
  500. verbose : false,
  501. debug : true,
  502. namespace : 'tab',
  503. // only called first time a tab's content is loaded (when remote source)
  504. onTabInit : function(tabPath, parameterArray, historyEvent) {},
  505. // called on every load
  506. onTabLoad : function(tabPath, parameterArray, historyEvent) {},
  507. templates: {
  508. determineTitle: function(tabArray) {}
  509. },
  510. history : false,
  511. path : false,
  512. context : 'body',
  513. // max depth a tab can be nested
  514. maxDepth : 25,
  515. // dont load content on first load
  516. ignoreFirstLoad : true,
  517. // load tab content new every tab click
  518. alwaysRefresh : false,
  519. // cache the content requests to pull locally
  520. cache : true,
  521. // settings for api call
  522. apiSettings : false,
  523. errors: {
  524. api : 'You attempted to load content without API module',
  525. noContent : 'The tab you specified is missing a content url.',
  526. method : 'The method you called is not defined',
  527. state : 'The state library has not been initialized',
  528. missingTab : 'Missing tab: ',
  529. path : 'History enabled, but no path was specified',
  530. recursion : 'Max recursive depth reached'
  531. },
  532. metadata : {
  533. tab : 'tab',
  534. loaded : 'loaded',
  535. promise: 'promise'
  536. },
  537. className : {
  538. loading : 'loading',
  539. active : 'active'
  540. },
  541. selector : {
  542. tabs : '.tab'
  543. }
  544. };
  545. })( jQuery, window , document );