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.

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