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.

788 lines
26 KiB

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