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
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, query);
  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 );