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.

707 lines
22 KiB

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