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.

674 lines
21 KiB

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