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.

726 lines
23 KiB

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