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.

712 lines
22 KiB

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