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.

777 lines
24 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/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. settings = ( $.isPlainObject(parameters) )
  18. ? $.extend(true, {}, $.fn.tab.settings, parameters)
  19. : $.extend({}, $.fn.tab.settings),
  20. moduleSelector = $allModules.selector || '',
  21. time = new Date().getTime(),
  22. performance = [],
  23. query = arguments[0],
  24. methodInvoked = (typeof query == 'string'),
  25. queryArguments = [].slice.call(arguments, 1),
  26. module,
  27. returnedValue
  28. ;
  29. $allModules
  30. .each(function() {
  31. var
  32. className = settings.className,
  33. metadata = settings.metadata,
  34. selector = settings.selector,
  35. error = settings.error,
  36. eventNamespace = '.' + settings.namespace,
  37. moduleNamespace = 'module-' + settings.namespace,
  38. $module = $(this),
  39. $tabs = $(selector.tabs),
  40. cache = {},
  41. firstLoad = true,
  42. recursionDepth = 0,
  43. $context,
  44. activeTabPath,
  45. parameterArray,
  46. historyEvent,
  47. element = this,
  48. instance = $module.data(moduleNamespace)
  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.instantiate();
  72. },
  73. determineTabs: function() {
  74. var
  75. $reference
  76. ;
  77. if(settings.context === 'parent') {
  78. if($module.closest('.' + className.ui).size() > 0) {
  79. $reference = $module.closest('.' + className.ui);
  80. module.verbose('Using closest UI element for determining parent', $reference);
  81. }
  82. else {
  83. $reference = $module;
  84. }
  85. $context = $reference.parent();
  86. module.verbose('Determining parent element for creating context', $context);
  87. }
  88. else {
  89. $context = $(settings.context);
  90. module.verbose('Using selector for tab context', settings.context, $context);
  91. }
  92. if(settings.childrenOnly) {
  93. $tabs = $context.children(selector.tabs);
  94. module.debug('Searching tab context children for tabs', $context, $tabs);
  95. }
  96. else {
  97. $tabs = $context.find(selector.tabs);
  98. module.debug('Searching tab context for tabs', $context, $tabs);
  99. }
  100. },
  101. initializeHistory: function() {
  102. if(settings.history) {
  103. module.debug('Initializing page state');
  104. if( $.address === undefined ) {
  105. module.error(error.state);
  106. return false;
  107. }
  108. else {
  109. if(settings.historyType == 'state') {
  110. module.debug('Using HTML5 to manage state');
  111. if(settings.path !== false) {
  112. $.address
  113. .history(true)
  114. .state(settings.path)
  115. ;
  116. }
  117. else {
  118. module.error(error.path);
  119. return false;
  120. }
  121. }
  122. $.address
  123. .bind('change', module.event.history.change)
  124. ;
  125. }
  126. }
  127. },
  128. instantiate: function () {
  129. module.verbose('Storing instance of module', module);
  130. $module
  131. .data(moduleNamespace, module)
  132. ;
  133. },
  134. destroy: function() {
  135. module.debug('Destroying tabs', $module);
  136. $module
  137. .removeData(moduleNamespace)
  138. .off(eventNamespace)
  139. ;
  140. },
  141. event: {
  142. click: function(event) {
  143. var
  144. tabPath = $(this).data(metadata.tab)
  145. ;
  146. if(tabPath !== undefined) {
  147. if(settings.history) {
  148. module.verbose('Updating page state', event);
  149. $.address.value(tabPath);
  150. }
  151. else {
  152. module.verbose('Changing tab', event);
  153. module.changeTab(tabPath);
  154. }
  155. event.preventDefault();
  156. }
  157. else {
  158. module.debug('No tab specified');
  159. }
  160. },
  161. history: {
  162. change: function(event) {
  163. var
  164. tabPath = event.pathNames.join('/') || module.get.initialPath(),
  165. pageTitle = settings.templates.determineTitle(tabPath) || false
  166. ;
  167. module.performance.display();
  168. module.debug('History change event', tabPath, event);
  169. historyEvent = event;
  170. if(tabPath !== undefined) {
  171. module.changeTab(tabPath);
  172. }
  173. if(pageTitle) {
  174. $.address.title(pageTitle);
  175. }
  176. }
  177. }
  178. },
  179. refresh: function() {
  180. if(activeTabPath) {
  181. module.debug('Refreshing tab', activeTabPath);
  182. module.changeTab(activeTabPath);
  183. }
  184. },
  185. cache: {
  186. read: function(cacheKey) {
  187. return (cacheKey !== undefined)
  188. ? cache[cacheKey]
  189. : false
  190. ;
  191. },
  192. add: function(cacheKey, content) {
  193. cacheKey = cacheKey || activeTabPath;
  194. module.debug('Adding cached content for', cacheKey);
  195. cache[cacheKey] = content;
  196. },
  197. remove: function(cacheKey) {
  198. cacheKey = cacheKey || activeTabPath;
  199. module.debug('Removing cached content for', cacheKey);
  200. delete cache[cacheKey];
  201. }
  202. },
  203. set: {
  204. state: function(state) {
  205. $.address.value(state);
  206. }
  207. },
  208. changeTab: function(tabPath) {
  209. var
  210. pushStateAvailable = (window.history && window.history.pushState),
  211. shouldIgnoreLoad = (pushStateAvailable && settings.ignoreFirstLoad && firstLoad),
  212. remoteContent = (settings.auto || $.isPlainObject(settings.apiSettings) ),
  213. // only get default path if not remote content
  214. pathArray = (remoteContent && !shouldIgnoreLoad)
  215. ? module.utilities.pathToArray(tabPath)
  216. : module.get.defaultPathArray(tabPath)
  217. ;
  218. tabPath = module.utilities.arrayToPath(pathArray);
  219. $.each(pathArray, function(index, tab) {
  220. var
  221. currentPathArray = pathArray.slice(0, index + 1),
  222. currentPath = module.utilities.arrayToPath(currentPathArray),
  223. isTab = module.is.tab(currentPath),
  224. isLastIndex = (index + 1 == pathArray.length),
  225. $tab = module.get.tabElement(currentPath),
  226. nextPathArray,
  227. nextPath,
  228. isLastTab
  229. ;
  230. module.verbose('Looking for tab', tab);
  231. if(isTab) {
  232. module.verbose('Tab was found', tab);
  233. // scope up
  234. activeTabPath = currentPath;
  235. parameterArray = module.utilities.filterArray(pathArray, currentPathArray);
  236. if(isLastIndex) {
  237. isLastTab = true;
  238. }
  239. else {
  240. nextPathArray = pathArray.slice(0, index + 2);
  241. nextPath = module.utilities.arrayToPath(nextPathArray);
  242. isLastTab = ( !module.is.tab(nextPath) );
  243. if(isLastTab) {
  244. module.verbose('Tab parameters found', nextPathArray);
  245. }
  246. }
  247. if(isLastTab && remoteContent) {
  248. if(!shouldIgnoreLoad) {
  249. module.activate.navigation(currentPath);
  250. module.content.fetch(currentPath, tabPath);
  251. }
  252. else {
  253. module.debug('Ignoring remote content on first tab load', currentPath);
  254. firstLoad = false;
  255. module.cache.add(tabPath, $tab.html());
  256. module.activate.all(currentPath);
  257. $.proxy(settings.onTabInit, $tab)(currentPath, parameterArray, historyEvent);
  258. $.proxy(settings.onTabLoad, $tab)(currentPath, parameterArray, historyEvent);
  259. }
  260. return false;
  261. }
  262. else {
  263. module.debug('Opened local tab', currentPath);
  264. module.activate.all(currentPath);
  265. if( !module.cache.read(currentPath) ) {
  266. module.cache.add(currentPath, true);
  267. module.debug('First time tab loaded calling tab init');
  268. $.proxy(settings.onTabInit, $tab)(currentPath, parameterArray, historyEvent);
  269. }
  270. $.proxy(settings.onTabLoad, $tab)(currentPath, parameterArray, historyEvent);
  271. }
  272. }
  273. else {
  274. if(!settings.history) {
  275. module.error(error.missingTab, $module, currentPath);
  276. }
  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. onSuccess : 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. if(module && !methodInvoked) {
  646. module.initializeHistory();
  647. }
  648. return (returnedValue !== undefined)
  649. ? returnedValue
  650. : this
  651. ;
  652. };
  653. // shortcut for tabbed content with no defined navigation
  654. $.tab = function(settings) {
  655. $(window).tab(settings);
  656. };
  657. $.fn.tab.settings = {
  658. name : 'Tab',
  659. namespace : 'tab',
  660. debug : true,
  661. verbose : true,
  662. performance : true,
  663. // only called first time a tab's content is loaded (when remote source)
  664. onTabInit : function(tabPath, parameterArray, historyEvent) {},
  665. // called on every load
  666. onTabLoad : function(tabPath, parameterArray, historyEvent) {},
  667. templates : {
  668. determineTitle: function(tabArray) {}
  669. },
  670. // uses pjax style endpoints fetching content from same url with remote-content headers
  671. auto : false,
  672. history : false,
  673. historyType : 'hash',
  674. path : false,
  675. context : false,
  676. childrenOnly : false,
  677. // max depth a tab can be nested
  678. maxDepth : 25,
  679. // dont load content on first load
  680. ignoreFirstLoad : false,
  681. // load tab content new every tab click
  682. alwaysRefresh : false,
  683. // cache the content requests to pull locally
  684. cache : true,
  685. // settings for api call
  686. apiSettings : false,
  687. error: {
  688. api : 'You attempted to load content without API module',
  689. method : 'The method you called is not defined',
  690. missingTab : 'Activated tab cannot be found for this context.',
  691. noContent : 'The tab you specified is missing a content url.',
  692. path : 'History enabled, but no path was specified',
  693. recursion : 'Max recursive depth reached',
  694. state : 'The state library has not been initialized'
  695. },
  696. metadata : {
  697. tab : 'tab',
  698. loaded : 'loaded',
  699. promise: 'promise'
  700. },
  701. className : {
  702. loading : 'loading',
  703. active : 'active',
  704. ui : 'ui'
  705. },
  706. selector : {
  707. tabs : '.ui.tab'
  708. }
  709. };
  710. })( jQuery, window , document );