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.

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