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.

794 lines
25 KiB

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