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

10 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. module.exports = function(parameters) {
  12. var
  13. settings = $.extend(true, {}, module.exports.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. object = instance,
  528. maxDepth,
  529. found,
  530. response
  531. ;
  532. passedArguments = passedArguments || queryArguments;
  533. context = element || context;
  534. if(typeof query == 'string' && object !== undefined) {
  535. query = query.split(/[\. ]/);
  536. maxDepth = query.length - 1;
  537. $.each(query, function(depth, value) {
  538. var camelCaseValue = (depth != maxDepth)
  539. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  540. : query
  541. ;
  542. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  543. object = object[camelCaseValue];
  544. }
  545. else if( object[camelCaseValue] !== undefined ) {
  546. found = object[camelCaseValue];
  547. return false;
  548. }
  549. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  550. object = object[value];
  551. }
  552. else if( object[value] !== undefined ) {
  553. found = object[value];
  554. return false;
  555. }
  556. else {
  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. module.exports.settings = {
  601. name : 'Tab',
  602. debug : false,
  603. verbose : 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. })( require("jquery"), window , document );