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