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.

690 lines
21 KiB

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