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.

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