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.

796 lines
25 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
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.search('/') == -1)
  279. ? $('#' + tabPath + ', a[name="' + tabPath + '"]')
  280. : $('#qqq'),
  281. currentPath = $anchor.closest('[data-tab]').data('tab');
  282. $tab = module.get.tabElement(currentPath);
  283. // if anchor exists use parent tab
  284. if($anchor && $anchor.size() > 0 && currentPath) {
  285. module.debug('No tab found, but deep anchor link present, opening parent tab');
  286. module.activate.all(currentPath);
  287. if( !module.cache.read(currentPath) ) {
  288. module.cache.add(currentPath, true);
  289. module.debug('First time tab loaded calling tab init');
  290. $.proxy(settings.onTabInit, $tab)(currentPath, parameterArray, historyEvent);
  291. }
  292. }
  293. else {
  294. module.error(error.missingTab, $module, currentPath);
  295. }
  296. return false;
  297. }
  298. });
  299. },
  300. content: {
  301. fetch: function(tabPath, fullTabPath) {
  302. var
  303. $tab = module.get.tabElement(tabPath),
  304. apiSettings = {
  305. dataType : 'html',
  306. stateContext : $tab,
  307. onSuccess : function(response) {
  308. module.cache.add(fullTabPath, response);
  309. module.content.update(tabPath, response);
  310. if(tabPath == activeTabPath) {
  311. module.debug('Content loaded', tabPath);
  312. module.activate.tab(tabPath);
  313. }
  314. else {
  315. module.debug('Content loaded in background', tabPath);
  316. }
  317. $.proxy(settings.onTabInit, $tab)(tabPath, parameterArray, historyEvent);
  318. $.proxy(settings.onTabLoad, $tab)(tabPath, parameterArray, historyEvent);
  319. },
  320. urlData: { tab: fullTabPath }
  321. },
  322. request = $tab.data(metadata.promise) || false,
  323. existingRequest = ( request && request.state() === 'pending' ),
  324. requestSettings,
  325. cachedContent
  326. ;
  327. fullTabPath = fullTabPath || tabPath;
  328. cachedContent = module.cache.read(fullTabPath);
  329. if(settings.cache && cachedContent) {
  330. module.debug('Showing existing content', fullTabPath);
  331. module.content.update(tabPath, cachedContent);
  332. module.activate.tab(tabPath);
  333. $.proxy(settings.onTabLoad, $tab)(tabPath, parameterArray, historyEvent);
  334. }
  335. else if(existingRequest) {
  336. module.debug('Content is already loading', fullTabPath);
  337. $tab
  338. .addClass(className.loading)
  339. ;
  340. }
  341. else if($.api !== undefined) {
  342. requestSettings = $.extend(true, { headers: { 'X-Remote': true } }, settings.apiSettings, apiSettings);
  343. module.debug('Retrieving remote content', fullTabPath, requestSettings);
  344. $.api( requestSettings );
  345. }
  346. else {
  347. module.error(error.api);
  348. }
  349. },
  350. update: function(tabPath, html) {
  351. module.debug('Updating html for', tabPath);
  352. var
  353. $tab = module.get.tabElement(tabPath)
  354. ;
  355. $tab
  356. .html(html)
  357. ;
  358. }
  359. },
  360. activate: {
  361. all: function(tabPath) {
  362. module.activate.tab(tabPath);
  363. module.activate.navigation(tabPath);
  364. },
  365. tab: function(tabPath) {
  366. var
  367. $tab = module.get.tabElement(tabPath)
  368. ;
  369. module.verbose('Showing tab content for', $tab);
  370. $tab
  371. .addClass(className.active)
  372. .siblings($tabs)
  373. .removeClass(className.active + ' ' + className.loading)
  374. ;
  375. },
  376. navigation: function(tabPath) {
  377. var
  378. $navigation = module.get.navElement(tabPath)
  379. ;
  380. module.verbose('Activating tab navigation for', $navigation, tabPath);
  381. $navigation
  382. .addClass(className.active)
  383. .siblings($allModules)
  384. .removeClass(className.active + ' ' + className.loading)
  385. ;
  386. }
  387. },
  388. deactivate: {
  389. all: function() {
  390. module.deactivate.navigation();
  391. module.deactivate.tabs();
  392. },
  393. navigation: function() {
  394. $allModules
  395. .removeClass(className.active)
  396. ;
  397. },
  398. tabs: function() {
  399. $tabs
  400. .removeClass(className.active + ' ' + className.loading)
  401. ;
  402. }
  403. },
  404. is: {
  405. tab: function(tabName) {
  406. return (tabName !== undefined)
  407. ? ( module.get.tabElement(tabName).size() > 0 )
  408. : false
  409. ;
  410. }
  411. },
  412. get: {
  413. initialPath: function() {
  414. return $allModules.eq(0).data(metadata.tab) || $tabs.eq(0).data(metadata.tab);
  415. },
  416. path: function() {
  417. return $.address.value();
  418. },
  419. // adds default tabs to tab path
  420. defaultPathArray: function(tabPath) {
  421. return module.utilities.pathToArray( module.get.defaultPath(tabPath) );
  422. },
  423. defaultPath: function(tabPath) {
  424. var
  425. $defaultNav = $allModules.filter('[data-' + metadata.tab + '^="' + tabPath + '/"]').eq(0),
  426. defaultTab = $defaultNav.data(metadata.tab) || false
  427. ;
  428. if( defaultTab ) {
  429. module.debug('Found default tab', defaultTab);
  430. if(recursionDepth < settings.maxDepth) {
  431. recursionDepth++;
  432. return module.get.defaultPath(defaultTab);
  433. }
  434. module.error(error.recursion);
  435. }
  436. else {
  437. module.debug('No default tabs found for', tabPath, $tabs);
  438. }
  439. recursionDepth = 0;
  440. return tabPath;
  441. },
  442. navElement: function(tabPath) {
  443. tabPath = tabPath || activeTabPath;
  444. return $allModules.filter('[data-' + metadata.tab + '="' + tabPath + '"]');
  445. },
  446. tabElement: function(tabPath) {
  447. var
  448. $fullPathTab,
  449. $simplePathTab,
  450. tabPathArray,
  451. lastTab
  452. ;
  453. tabPath = tabPath || activeTabPath;
  454. tabPathArray = module.utilities.pathToArray(tabPath);
  455. lastTab = module.utilities.last(tabPathArray);
  456. $fullPathTab = $tabs.filter('[data-' + metadata.tab + '="' + lastTab + '"]');
  457. $simplePathTab = $tabs.filter('[data-' + metadata.tab + '="' + tabPath + '"]');
  458. return ($fullPathTab.size() > 0)
  459. ? $fullPathTab
  460. : $simplePathTab
  461. ;
  462. },
  463. tab: function() {
  464. return activeTabPath;
  465. }
  466. },
  467. utilities: {
  468. filterArray: function(keepArray, removeArray) {
  469. return $.grep(keepArray, function(keepValue) {
  470. return ( $.inArray(keepValue, removeArray) == -1);
  471. });
  472. },
  473. last: function(array) {
  474. return $.isArray(array)
  475. ? array[ array.length - 1]
  476. : false
  477. ;
  478. },
  479. pathToArray: function(pathName) {
  480. if(pathName === undefined) {
  481. pathName = activeTabPath;
  482. }
  483. return typeof pathName == 'string'
  484. ? pathName.split('/')
  485. : [pathName]
  486. ;
  487. },
  488. arrayToPath: function(pathArray) {
  489. return $.isArray(pathArray)
  490. ? pathArray.join('/')
  491. : false
  492. ;
  493. }
  494. },
  495. setting: function(name, value) {
  496. module.debug('Changing setting', name, value);
  497. if( $.isPlainObject(name) ) {
  498. $.extend(true, settings, name);
  499. }
  500. else if(value !== undefined) {
  501. settings[name] = value;
  502. }
  503. else {
  504. return settings[name];
  505. }
  506. },
  507. internal: function(name, value) {
  508. if( $.isPlainObject(name) ) {
  509. $.extend(true, module, name);
  510. }
  511. else if(value !== undefined) {
  512. module[name] = value;
  513. }
  514. else {
  515. return module[name];
  516. }
  517. },
  518. debug: function() {
  519. if(settings.debug) {
  520. if(settings.performance) {
  521. module.performance.log(arguments);
  522. }
  523. else {
  524. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  525. module.debug.apply(console, arguments);
  526. }
  527. }
  528. },
  529. verbose: function() {
  530. if(settings.verbose && settings.debug) {
  531. if(settings.performance) {
  532. module.performance.log(arguments);
  533. }
  534. else {
  535. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  536. module.verbose.apply(console, arguments);
  537. }
  538. }
  539. },
  540. error: function() {
  541. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  542. module.error.apply(console, arguments);
  543. },
  544. performance: {
  545. log: function(message) {
  546. var
  547. currentTime,
  548. executionTime,
  549. previousTime
  550. ;
  551. if(settings.performance) {
  552. currentTime = new Date().getTime();
  553. previousTime = time || currentTime;
  554. executionTime = currentTime - previousTime;
  555. time = currentTime;
  556. performance.push({
  557. 'Name' : message[0],
  558. 'Arguments' : [].slice.call(message, 1) || '',
  559. 'Element' : element,
  560. 'Execution Time' : executionTime
  561. });
  562. }
  563. clearTimeout(module.performance.timer);
  564. module.performance.timer = setTimeout(module.performance.display, 100);
  565. },
  566. display: function() {
  567. var
  568. title = settings.name + ':',
  569. totalTime = 0
  570. ;
  571. time = false;
  572. clearTimeout(module.performance.timer);
  573. $.each(performance, function(index, data) {
  574. totalTime += data['Execution Time'];
  575. });
  576. title += ' ' + totalTime + 'ms';
  577. if(moduleSelector) {
  578. title += ' \'' + moduleSelector + '\'';
  579. }
  580. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  581. console.groupCollapsed(title);
  582. if(console.table) {
  583. console.table(performance);
  584. }
  585. else {
  586. $.each(performance, function(index, data) {
  587. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  588. });
  589. }
  590. console.groupEnd();
  591. }
  592. performance = [];
  593. }
  594. },
  595. invoke: function(query, passedArguments, context) {
  596. var
  597. object = instance,
  598. maxDepth,
  599. found,
  600. response
  601. ;
  602. passedArguments = passedArguments || queryArguments;
  603. context = element || context;
  604. if(typeof query == 'string' && object !== undefined) {
  605. query = query.split(/[\. ]/);
  606. maxDepth = query.length - 1;
  607. $.each(query, function(depth, value) {
  608. var camelCaseValue = (depth != maxDepth)
  609. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  610. : query
  611. ;
  612. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  613. object = object[camelCaseValue];
  614. }
  615. else if( object[camelCaseValue] !== undefined ) {
  616. found = object[camelCaseValue];
  617. return false;
  618. }
  619. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  620. object = object[value];
  621. }
  622. else if( object[value] !== undefined ) {
  623. found = object[value];
  624. return false;
  625. }
  626. else {
  627. module.error(error.method, query);
  628. return false;
  629. }
  630. });
  631. }
  632. if ( $.isFunction( found ) ) {
  633. response = found.apply(context, passedArguments);
  634. }
  635. else if(found !== undefined) {
  636. response = found;
  637. }
  638. if($.isArray(returnedValue)) {
  639. returnedValue.push(response);
  640. }
  641. else if(returnedValue !== undefined) {
  642. returnedValue = [returnedValue, response];
  643. }
  644. else if(response !== undefined) {
  645. returnedValue = response;
  646. }
  647. return found;
  648. }
  649. };
  650. if(methodInvoked) {
  651. if(instance === undefined) {
  652. module.initialize();
  653. }
  654. module.invoke(query);
  655. }
  656. else {
  657. if(instance !== undefined) {
  658. module.destroy();
  659. }
  660. module.initialize();
  661. }
  662. })
  663. ;
  664. if(module && !methodInvoked) {
  665. module.initializeHistory();
  666. }
  667. return (returnedValue !== undefined)
  668. ? returnedValue
  669. : this
  670. ;
  671. };
  672. // shortcut for tabbed content with no defined navigation
  673. $.tab = function(settings) {
  674. $(window).tab(settings);
  675. };
  676. $.fn.tab.settings = {
  677. name : 'Tab',
  678. namespace : 'tab',
  679. debug : false,
  680. verbose : false,
  681. performance : false,
  682. // uses pjax style endpoints fetching content from same url with remote-content headers
  683. auto : false,
  684. history : false,
  685. historyType : 'hash',
  686. path : false,
  687. context : false,
  688. childrenOnly : false,
  689. // max depth a tab can be nested
  690. maxDepth : 25,
  691. // dont load content on first load
  692. ignoreFirstLoad : false,
  693. // load tab content new every tab click
  694. alwaysRefresh : false,
  695. // cache the content requests to pull locally
  696. cache : true,
  697. // settings for api call
  698. apiSettings : false,
  699. // only called first time a tab's content is loaded (when remote source)
  700. onTabInit : function(tabPath, parameterArray, historyEvent) {},
  701. // called on every load
  702. onTabLoad : function(tabPath, parameterArray, historyEvent) {},
  703. templates : {
  704. determineTitle: function(tabArray) {}
  705. },
  706. error: {
  707. api : 'You attempted to load content without API module',
  708. method : 'The method you called is not defined',
  709. missingTab : 'Activated tab cannot be found for this context.',
  710. noContent : 'The tab you specified is missing a content url.',
  711. path : 'History enabled, but no path was specified',
  712. recursion : 'Max recursive depth reached',
  713. state : 'The state library has not been initialized'
  714. },
  715. metadata : {
  716. tab : 'tab',
  717. loaded : 'loaded',
  718. promise: 'promise'
  719. },
  720. className : {
  721. loading : 'loading',
  722. active : 'active',
  723. ui : 'ui'
  724. },
  725. selector : {
  726. tabs : '.ui.tab'
  727. }
  728. };
  729. })( jQuery, window , document );