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.

772 lines
24 KiB

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