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.

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