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.

677 lines
20 KiB

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