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.

710 lines
22 KiB

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