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.

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