tabs.js 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561
  1. /*!
  2. * AngularJS Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v1.1.8-master-aba7b2b
  6. */
  7. goog.provide('ngmaterial.components.tabs');
  8. goog.require('ngmaterial.components.icon');
  9. goog.require('ngmaterial.core');
  10. /**
  11. * @ngdoc module
  12. * @name material.components.tabs
  13. * @description
  14. *
  15. * Tabs, created with the `<md-tabs>` directive provide *tabbed* navigation with different styles.
  16. * The Tabs component consists of clickable tabs that are aligned horizontally side-by-side.
  17. *
  18. * Features include support for:
  19. *
  20. * - static or dynamic tabs,
  21. * - responsive designs,
  22. * - accessibility support (ARIA),
  23. * - tab pagination,
  24. * - external or internal tab content,
  25. * - focus indicators and arrow-key navigations,
  26. * - programmatic lookup and access to tab controllers, and
  27. * - dynamic transitions through different tab contents.
  28. *
  29. */
  30. /*
  31. * @see js folder for tabs implementation
  32. */
  33. angular.module('material.components.tabs', [
  34. 'material.core',
  35. 'material.components.icon'
  36. ]);
  37. angular
  38. .module('material.components.tabs')
  39. .service('MdTabsPaginationService', MdTabsPaginationService);
  40. /**
  41. * @private
  42. * @module material.components.tabs
  43. * @name MdTabsPaginationService
  44. * @description Provides many standalone functions to ease in pagination calculations.
  45. *
  46. * Most functions accept the elements and the current offset.
  47. *
  48. * The `elements` parameter is typically the value returned from the `getElements()` function of the
  49. * tabsController.
  50. *
  51. * The `offset` parameter is always positive regardless of LTR or RTL (we simply make the LTR one
  52. * negative when we apply our transform). This is typically the `ctrl.leftOffset` variable in the
  53. * tabsController.
  54. *
  55. * @returns MdTabsPaginationService
  56. * @constructor
  57. */
  58. function MdTabsPaginationService() {
  59. return {
  60. decreasePageOffset: decreasePageOffset,
  61. increasePageOffset: increasePageOffset,
  62. getTabOffsets: getTabOffsets,
  63. getTotalTabsWidth: getTotalTabsWidth
  64. };
  65. /**
  66. * Returns the offset for the next decreasing page.
  67. *
  68. * @param elements
  69. * @param currentOffset
  70. * @returns {number}
  71. */
  72. function decreasePageOffset(elements, currentOffset) {
  73. var canvas = elements.canvas,
  74. tabOffsets = getTabOffsets(elements),
  75. i, firstVisibleTabOffset;
  76. // Find the first fully visible tab in offset range
  77. for (i = 0; i < tabOffsets.length; i++) {
  78. if (tabOffsets[i] >= currentOffset) {
  79. firstVisibleTabOffset = tabOffsets[i];
  80. break;
  81. }
  82. }
  83. // Return (the first visible tab offset - the tabs container width) without going negative
  84. return Math.max(0, firstVisibleTabOffset - canvas.clientWidth);
  85. }
  86. /**
  87. * Returns the offset for the next increasing page.
  88. *
  89. * @param elements
  90. * @param currentOffset
  91. * @returns {number}
  92. */
  93. function increasePageOffset(elements, currentOffset) {
  94. var canvas = elements.canvas,
  95. maxOffset = getTotalTabsWidth(elements) - canvas.clientWidth,
  96. tabOffsets = getTabOffsets(elements),
  97. i, firstHiddenTabOffset;
  98. // Find the first partially (or fully) invisible tab
  99. for (i = 0; i < tabOffsets.length, tabOffsets[i] <= currentOffset + canvas.clientWidth; i++) {
  100. firstHiddenTabOffset = tabOffsets[i];
  101. }
  102. // Return the offset of the first hidden tab, or the maximum offset (whichever is smaller)
  103. return Math.min(maxOffset, firstHiddenTabOffset);
  104. }
  105. /**
  106. * Returns the offsets of all of the tabs based on their widths.
  107. *
  108. * @param elements
  109. * @returns {number[]}
  110. */
  111. function getTabOffsets(elements) {
  112. var i, tab, currentOffset = 0, offsets = [];
  113. for (i = 0; i < elements.tabs.length; i++) {
  114. tab = elements.tabs[i];
  115. offsets.push(currentOffset);
  116. currentOffset += tab.offsetWidth;
  117. }
  118. return offsets;
  119. }
  120. /**
  121. * Sum the width of all tabs.
  122. *
  123. * @param elements
  124. * @returns {number}
  125. */
  126. function getTotalTabsWidth(elements) {
  127. var sum = 0, i, tab;
  128. for (i = 0; i < elements.tabs.length; i++) {
  129. tab = elements.tabs[i];
  130. sum += tab.offsetWidth;
  131. }
  132. return sum;
  133. }
  134. }
  135. /**
  136. * @ngdoc directive
  137. * @name mdTab
  138. * @module material.components.tabs
  139. *
  140. * @restrict E
  141. *
  142. * @description
  143. * The `<md-tab>` is a nested directive used within `<md-tabs>` to specify a tab with a **label** and optional *view content*.
  144. *
  145. * If the `label` attribute is not specified, then an optional `<md-tab-label>` tag can be used to specify more
  146. * complex tab header markup. If neither the **label** nor the **md-tab-label** are specified, then the nested
  147. * markup of the `<md-tab>` is used as the tab header markup.
  148. *
  149. * Please note that if you use `<md-tab-label>`, your content **MUST** be wrapped in the `<md-tab-body>` tag. This
  150. * is to define a clear separation between the tab content and the tab label.
  151. *
  152. * This container is used by the TabsController to show/hide the active tab's content view. This synchronization is
  153. * automatically managed by the internal TabsController whenever the tab selection changes. Selection changes can
  154. * be initiated via data binding changes, programmatic invocation, or user gestures.
  155. *
  156. * @param {string=} label Optional attribute to specify a simple string as the tab label
  157. * @param {boolean=} ng-disabled If present and expression evaluates to truthy, disabled tab selection.
  158. * @param {expression=} md-on-deselect Expression to be evaluated after the tab has been de-selected.
  159. * @param {expression=} md-on-select Expression to be evaluated after the tab has been selected.
  160. * @param {boolean=} md-active When true, sets the active tab. Note: There can only be one active tab at a time.
  161. *
  162. *
  163. * @usage
  164. *
  165. * <hljs lang="html">
  166. * <md-tab label="" ng-disabled md-on-select="" md-on-deselect="" >
  167. * <h3>My Tab content</h3>
  168. * </md-tab>
  169. *
  170. * <md-tab >
  171. * <md-tab-label>
  172. * <h3>My Tab content</h3>
  173. * </md-tab-label>
  174. * <md-tab-body>
  175. * <p>
  176. * Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
  177. * totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
  178. * dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
  179. * sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
  180. * </p>
  181. * </md-tab-body>
  182. * </md-tab>
  183. * </hljs>
  184. *
  185. */
  186. angular
  187. .module('material.components.tabs')
  188. .directive('mdTab', MdTab);
  189. function MdTab () {
  190. return {
  191. require: '^?mdTabs',
  192. terminal: true,
  193. compile: function (element, attr) {
  194. var label = firstChild(element, 'md-tab-label'),
  195. body = firstChild(element, 'md-tab-body');
  196. if (label.length === 0) {
  197. label = angular.element('<md-tab-label></md-tab-label>');
  198. if (attr.label) label.text(attr.label);
  199. else label.append(element.contents());
  200. if (body.length === 0) {
  201. var contents = element.contents().detach();
  202. body = angular.element('<md-tab-body></md-tab-body>');
  203. body.append(contents);
  204. }
  205. }
  206. element.append(label);
  207. if (body.html()) element.append(body);
  208. return postLink;
  209. },
  210. scope: {
  211. active: '=?mdActive',
  212. disabled: '=?ngDisabled',
  213. select: '&?mdOnSelect',
  214. deselect: '&?mdOnDeselect'
  215. }
  216. };
  217. function postLink (scope, element, attr, ctrl) {
  218. if (!ctrl) return;
  219. var index = ctrl.getTabElementIndex(element),
  220. body = firstChild(element, 'md-tab-body').remove(),
  221. label = firstChild(element, 'md-tab-label').remove(),
  222. data = ctrl.insertTab({
  223. scope: scope,
  224. parent: scope.$parent,
  225. index: index,
  226. element: element,
  227. template: body.html(),
  228. label: label.html()
  229. }, index);
  230. scope.select = scope.select || angular.noop;
  231. scope.deselect = scope.deselect || angular.noop;
  232. scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex(), true); });
  233. scope.$watch('disabled', function () { ctrl.refreshIndex(); });
  234. scope.$watch(
  235. function () {
  236. return ctrl.getTabElementIndex(element);
  237. },
  238. function (newIndex) {
  239. data.index = newIndex;
  240. ctrl.updateTabOrder();
  241. }
  242. );
  243. scope.$on('$destroy', function () { ctrl.removeTab(data); });
  244. }
  245. function firstChild (element, tagName) {
  246. var children = element[0].children;
  247. for (var i = 0, len = children.length; i < len; i++) {
  248. var child = children[i];
  249. if (child.tagName === tagName.toUpperCase()) return angular.element(child);
  250. }
  251. return angular.element();
  252. }
  253. }
  254. angular
  255. .module('material.components.tabs')
  256. .directive('mdTabItem', MdTabItem);
  257. function MdTabItem () {
  258. return {
  259. require: '^?mdTabs',
  260. link: function link (scope, element, attr, ctrl) {
  261. if (!ctrl) return;
  262. ctrl.attachRipple(scope, element);
  263. }
  264. };
  265. }
  266. angular
  267. .module('material.components.tabs')
  268. .directive('mdTabLabel', MdTabLabel);
  269. function MdTabLabel () {
  270. return { terminal: true };
  271. }
  272. MdTabScroll['$inject'] = ["$parse"];angular.module('material.components.tabs')
  273. .directive('mdTabScroll', MdTabScroll);
  274. function MdTabScroll ($parse) {
  275. return {
  276. restrict: 'A',
  277. compile: function ($element, attr) {
  278. var fn = $parse(attr.mdTabScroll, null, true);
  279. return function ngEventHandler (scope, element) {
  280. element.on('mousewheel', function (event) {
  281. scope.$apply(function () { fn(scope, { $event: event }); });
  282. });
  283. };
  284. }
  285. };
  286. }
  287. MdTabsController['$inject'] = ["$scope", "$element", "$window", "$mdConstant", "$mdTabInkRipple", "$mdUtil", "$animateCss", "$attrs", "$compile", "$mdTheming", "$mdInteraction", "MdTabsPaginationService"];angular
  288. .module('material.components.tabs')
  289. .controller('MdTabsController', MdTabsController);
  290. /**
  291. * ngInject
  292. */
  293. function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipple, $mdUtil,
  294. $animateCss, $attrs, $compile, $mdTheming, $mdInteraction,
  295. MdTabsPaginationService) {
  296. // define private properties
  297. var ctrl = this,
  298. locked = false,
  299. elements = getElements(),
  300. queue = [],
  301. destroyed = false,
  302. loaded = false;
  303. // Define public methods
  304. ctrl.$onInit = $onInit;
  305. ctrl.updatePagination = $mdUtil.debounce(updatePagination, 100);
  306. ctrl.redirectFocus = redirectFocus;
  307. ctrl.attachRipple = attachRipple;
  308. ctrl.insertTab = insertTab;
  309. ctrl.removeTab = removeTab;
  310. ctrl.select = select;
  311. ctrl.scroll = scroll;
  312. ctrl.nextPage = nextPage;
  313. ctrl.previousPage = previousPage;
  314. ctrl.keydown = keydown;
  315. ctrl.canPageForward = canPageForward;
  316. ctrl.canPageBack = canPageBack;
  317. ctrl.refreshIndex = refreshIndex;
  318. ctrl.incrementIndex = incrementIndex;
  319. ctrl.getTabElementIndex = getTabElementIndex;
  320. ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100);
  321. ctrl.updateTabOrder = $mdUtil.debounce(updateTabOrder, 100);
  322. ctrl.getFocusedTabId = getFocusedTabId;
  323. // For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
  324. // manually call the $onInit hook.
  325. if (angular.version.major === 1 && angular.version.minor <= 4) {
  326. this.$onInit();
  327. }
  328. /**
  329. * AngularJS Lifecycle hook for newer AngularJS versions.
  330. * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook.
  331. */
  332. function $onInit() {
  333. // Define one-way bindings
  334. defineOneWayBinding('stretchTabs', handleStretchTabs);
  335. // Define public properties with change handlers
  336. defineProperty('focusIndex', handleFocusIndexChange, ctrl.selectedIndex || 0);
  337. defineProperty('offsetLeft', handleOffsetChange, 0);
  338. defineProperty('hasContent', handleHasContent, false);
  339. defineProperty('maxTabWidth', handleMaxTabWidth, getMaxTabWidth());
  340. defineProperty('shouldPaginate', handleShouldPaginate, false);
  341. // Define boolean attributes
  342. defineBooleanAttribute('noInkBar', handleInkBar);
  343. defineBooleanAttribute('dynamicHeight', handleDynamicHeight);
  344. defineBooleanAttribute('noPagination');
  345. defineBooleanAttribute('swipeContent');
  346. defineBooleanAttribute('noDisconnect');
  347. defineBooleanAttribute('autoselect');
  348. defineBooleanAttribute('noSelectClick');
  349. defineBooleanAttribute('centerTabs', handleCenterTabs, false);
  350. defineBooleanAttribute('enableDisconnect');
  351. // Define public properties
  352. ctrl.scope = $scope;
  353. ctrl.parent = $scope.$parent;
  354. ctrl.tabs = [];
  355. ctrl.lastSelectedIndex = null;
  356. ctrl.hasFocus = false;
  357. ctrl.styleTabItemFocus = false;
  358. ctrl.shouldCenterTabs = shouldCenterTabs();
  359. ctrl.tabContentPrefix = 'tab-content-';
  360. // Setup the tabs controller after all bindings are available.
  361. setupTabsController();
  362. }
  363. /**
  364. * Perform setup for the controller, setup events and watcher(s)
  365. */
  366. function setupTabsController () {
  367. ctrl.selectedIndex = ctrl.selectedIndex || 0;
  368. compileTemplate();
  369. configureWatchers();
  370. bindEvents();
  371. $mdTheming($element);
  372. $mdUtil.nextTick(function () {
  373. // Note that the element references need to be updated, because certain "browsers"
  374. // (IE/Edge) lose them and start throwing "Invalid calling object" errors, when we
  375. // compile the element contents down in `compileElement`.
  376. elements = getElements();
  377. updateHeightFromContent();
  378. adjustOffset();
  379. updateInkBarStyles();
  380. ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
  381. loaded = true;
  382. updatePagination();
  383. });
  384. }
  385. /**
  386. * Compiles the template provided by the user. This is passed as an attribute from the tabs
  387. * directive's template function.
  388. */
  389. function compileTemplate () {
  390. var template = $attrs.$mdTabsTemplate,
  391. element = angular.element($element[0].querySelector('md-tab-data'));
  392. element.html(template);
  393. $compile(element.contents())(ctrl.parent);
  394. delete $attrs.$mdTabsTemplate;
  395. }
  396. /**
  397. * Binds events used by the tabs component.
  398. */
  399. function bindEvents () {
  400. angular.element($window).on('resize', handleWindowResize);
  401. $scope.$on('$destroy', cleanup);
  402. }
  403. /**
  404. * Configure watcher(s) used by Tabs
  405. */
  406. function configureWatchers () {
  407. $scope.$watch('$mdTabsCtrl.selectedIndex', handleSelectedIndexChange);
  408. }
  409. /**
  410. * Creates a one-way binding manually rather than relying on AngularJS's isolated scope
  411. * @param key
  412. * @param handler
  413. */
  414. function defineOneWayBinding (key, handler) {
  415. var attr = $attrs.$normalize('md-' + key);
  416. if (handler) defineProperty(key, handler);
  417. $attrs.$observe(attr, function (newValue) { ctrl[ key ] = newValue; });
  418. }
  419. /**
  420. * Defines boolean attributes with default value set to true. (ie. md-stretch-tabs with no value
  421. * will be treated as being truthy)
  422. * @param key
  423. * @param handler
  424. */
  425. function defineBooleanAttribute (key, handler) {
  426. var attr = $attrs.$normalize('md-' + key);
  427. if (handler) defineProperty(key, handler);
  428. if ($attrs.hasOwnProperty(attr)) updateValue($attrs[attr]);
  429. $attrs.$observe(attr, updateValue);
  430. function updateValue (newValue) {
  431. ctrl[ key ] = newValue !== 'false';
  432. }
  433. }
  434. /**
  435. * Remove any events defined by this controller
  436. */
  437. function cleanup () {
  438. destroyed = true;
  439. angular.element($window).off('resize', handleWindowResize);
  440. }
  441. // Change handlers
  442. /**
  443. * Toggles stretch tabs class and updates inkbar when tab stretching changes
  444. * @param stretchTabs
  445. */
  446. function handleStretchTabs (stretchTabs) {
  447. var elements = getElements();
  448. angular.element(elements.wrapper).toggleClass('md-stretch-tabs', shouldStretchTabs());
  449. updateInkBarStyles();
  450. }
  451. function handleCenterTabs (newValue) {
  452. ctrl.shouldCenterTabs = shouldCenterTabs();
  453. }
  454. function handleMaxTabWidth (newWidth, oldWidth) {
  455. if (newWidth !== oldWidth) {
  456. var elements = getElements();
  457. // Set the max width for the real tabs
  458. angular.forEach(elements.tabs, function(tab) {
  459. tab.style.maxWidth = newWidth + 'px';
  460. });
  461. // Set the max width for the dummy tabs too
  462. angular.forEach(elements.dummies, function(tab) {
  463. tab.style.maxWidth = newWidth + 'px';
  464. });
  465. $mdUtil.nextTick(ctrl.updateInkBarStyles);
  466. }
  467. }
  468. function handleShouldPaginate (newValue, oldValue) {
  469. if (newValue !== oldValue) {
  470. ctrl.maxTabWidth = getMaxTabWidth();
  471. ctrl.shouldCenterTabs = shouldCenterTabs();
  472. $mdUtil.nextTick(function () {
  473. ctrl.maxTabWidth = getMaxTabWidth();
  474. adjustOffset(ctrl.selectedIndex);
  475. });
  476. }
  477. }
  478. /**
  479. * Add/remove the `md-no-tab-content` class depending on `ctrl.hasContent`
  480. * @param hasContent
  481. */
  482. function handleHasContent (hasContent) {
  483. $element[ hasContent ? 'removeClass' : 'addClass' ]('md-no-tab-content');
  484. }
  485. /**
  486. * Apply ctrl.offsetLeft to the paging element when it changes
  487. * @param left
  488. */
  489. function handleOffsetChange (left) {
  490. var elements = getElements();
  491. var newValue = ((ctrl.shouldCenterTabs || isRtl() ? '' : '-') + left + 'px');
  492. // Fix double-negative which can happen with RTL support
  493. newValue = newValue.replace('--', '');
  494. angular.element(elements.paging).css($mdConstant.CSS.TRANSFORM, 'translate3d(' + newValue + ', 0, 0)');
  495. $scope.$broadcast('$mdTabsPaginationChanged');
  496. }
  497. /**
  498. * Update the UI whenever `ctrl.focusIndex` is updated
  499. * @param newIndex
  500. * @param oldIndex
  501. */
  502. function handleFocusIndexChange (newIndex, oldIndex) {
  503. if (newIndex === oldIndex) return;
  504. if (!getElements().tabs[ newIndex ]) return;
  505. adjustOffset();
  506. redirectFocus();
  507. }
  508. /**
  509. * Update the UI whenever the selected index changes. Calls user-defined select/deselect methods.
  510. * @param newValue
  511. * @param oldValue
  512. */
  513. function handleSelectedIndexChange (newValue, oldValue) {
  514. if (newValue === oldValue) return;
  515. ctrl.selectedIndex = getNearestSafeIndex(newValue);
  516. ctrl.lastSelectedIndex = oldValue;
  517. ctrl.updateInkBarStyles();
  518. updateHeightFromContent();
  519. adjustOffset(newValue);
  520. $scope.$broadcast('$mdTabsChanged');
  521. ctrl.tabs[ oldValue ] && ctrl.tabs[ oldValue ].scope.deselect();
  522. ctrl.tabs[ newValue ] && ctrl.tabs[ newValue ].scope.select();
  523. }
  524. function getTabElementIndex(tabEl){
  525. var tabs = $element[0].getElementsByTagName('md-tab');
  526. return Array.prototype.indexOf.call(tabs, tabEl[0]);
  527. }
  528. /**
  529. * Queues up a call to `handleWindowResize` when a resize occurs while the tabs component is
  530. * hidden.
  531. */
  532. function handleResizeWhenVisible () {
  533. // if there is already a watcher waiting for resize, do nothing
  534. if (handleResizeWhenVisible.watcher) return;
  535. // otherwise, we will abuse the $watch function to check for visible
  536. handleResizeWhenVisible.watcher = $scope.$watch(function () {
  537. // since we are checking for DOM size, we use $mdUtil.nextTick() to wait for after the DOM updates
  538. $mdUtil.nextTick(function () {
  539. // if the watcher has already run (ie. multiple digests in one cycle), do nothing
  540. if (!handleResizeWhenVisible.watcher) return;
  541. if ($element.prop('offsetParent')) {
  542. handleResizeWhenVisible.watcher();
  543. handleResizeWhenVisible.watcher = null;
  544. handleWindowResize();
  545. }
  546. }, false);
  547. });
  548. }
  549. // Event handlers / actions
  550. /**
  551. * Handle user keyboard interactions
  552. * @param event
  553. */
  554. function keydown (event) {
  555. switch (event.keyCode) {
  556. case $mdConstant.KEY_CODE.LEFT_ARROW:
  557. event.preventDefault();
  558. incrementIndex(-1, true);
  559. break;
  560. case $mdConstant.KEY_CODE.RIGHT_ARROW:
  561. event.preventDefault();
  562. incrementIndex(1, true);
  563. break;
  564. case $mdConstant.KEY_CODE.SPACE:
  565. case $mdConstant.KEY_CODE.ENTER:
  566. event.preventDefault();
  567. if (!locked) select(ctrl.focusIndex);
  568. break;
  569. case $mdConstant.KEY_CODE.TAB:
  570. // On tabbing out of the tablist, reset hasFocus to reset ng-focused and
  571. // its md-focused class if the focused tab is not the active tab.
  572. if (ctrl.focusIndex !== ctrl.selectedIndex) {
  573. ctrl.focusIndex = ctrl.selectedIndex;
  574. }
  575. break;
  576. }
  577. }
  578. /**
  579. * Update the selected index. Triggers a click event on the original `md-tab` element in order
  580. * to fire user-added click events if canSkipClick or `md-no-select-click` are false.
  581. * @param index
  582. * @param canSkipClick Optionally allow not firing the click event if `md-no-select-click` is also true.
  583. */
  584. function select (index, canSkipClick) {
  585. if (!locked) ctrl.focusIndex = ctrl.selectedIndex = index;
  586. // skip the click event if noSelectClick is enabled
  587. if (canSkipClick && ctrl.noSelectClick) return;
  588. // nextTick is required to prevent errors in user-defined click events
  589. $mdUtil.nextTick(function () {
  590. ctrl.tabs[ index ].element.triggerHandler('click');
  591. }, false);
  592. }
  593. /**
  594. * When pagination is on, this makes sure the selected index is in view.
  595. * @param event
  596. */
  597. function scroll (event) {
  598. if (!ctrl.shouldPaginate) return;
  599. event.preventDefault();
  600. ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta);
  601. }
  602. /**
  603. * Slides the tabs over approximately one page forward.
  604. */
  605. function nextPage () {
  606. if (!ctrl.canPageForward()) { return; }
  607. var newOffset = MdTabsPaginationService.increasePageOffset(getElements(), ctrl.offsetLeft);
  608. ctrl.offsetLeft = fixOffset(newOffset);
  609. }
  610. /**
  611. * Slides the tabs over approximately one page backward.
  612. */
  613. function previousPage () {
  614. if (!ctrl.canPageBack()) { return; }
  615. var newOffset = MdTabsPaginationService.decreasePageOffset(getElements(), ctrl.offsetLeft);
  616. // Set the new offset
  617. ctrl.offsetLeft = fixOffset(newOffset);
  618. }
  619. /**
  620. * Update size calculations when the window is resized.
  621. */
  622. function handleWindowResize () {
  623. ctrl.lastSelectedIndex = ctrl.selectedIndex;
  624. ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
  625. $mdUtil.nextTick(function () {
  626. ctrl.updateInkBarStyles();
  627. updatePagination();
  628. });
  629. }
  630. function handleInkBar (hide) {
  631. angular.element(getElements().inkBar).toggleClass('ng-hide', hide);
  632. }
  633. /**
  634. * Toggle dynamic height class when value changes
  635. * @param value
  636. */
  637. function handleDynamicHeight (value) {
  638. $element.toggleClass('md-dynamic-height', value);
  639. }
  640. /**
  641. * Remove a tab from the data and select the nearest valid tab.
  642. * @param tabData
  643. */
  644. function removeTab (tabData) {
  645. if (destroyed) return;
  646. var selectedIndex = ctrl.selectedIndex,
  647. tab = ctrl.tabs.splice(tabData.getIndex(), 1)[ 0 ];
  648. refreshIndex();
  649. // when removing a tab, if the selected index did not change, we have to manually trigger the
  650. // tab select/deselect events
  651. if (ctrl.selectedIndex === selectedIndex) {
  652. tab.scope.deselect();
  653. ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
  654. }
  655. $mdUtil.nextTick(function () {
  656. updatePagination();
  657. ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
  658. });
  659. }
  660. /**
  661. * Create an entry in the tabs array for a new tab at the specified index.
  662. * @param tabData
  663. * @param index
  664. * @returns {*}
  665. */
  666. function insertTab (tabData, index) {
  667. var hasLoaded = loaded;
  668. var proto = {
  669. getIndex: function () { return ctrl.tabs.indexOf(tab); },
  670. isActive: function () { return this.getIndex() === ctrl.selectedIndex; },
  671. isLeft: function () { return this.getIndex() < ctrl.selectedIndex; },
  672. isRight: function () { return this.getIndex() > ctrl.selectedIndex; },
  673. shouldRender: function () { return !ctrl.noDisconnect || this.isActive(); },
  674. hasFocus: function () {
  675. return ctrl.styleTabItemFocus
  676. && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex;
  677. },
  678. id: $mdUtil.nextUid(),
  679. hasContent: !!(tabData.template && tabData.template.trim())
  680. },
  681. tab = angular.extend(proto, tabData);
  682. if (angular.isDefined(index)) {
  683. ctrl.tabs.splice(index, 0, tab);
  684. } else {
  685. ctrl.tabs.push(tab);
  686. }
  687. processQueue();
  688. updateHasContent();
  689. $mdUtil.nextTick(function () {
  690. updatePagination();
  691. setAriaControls(tab);
  692. // if autoselect is enabled, select the newly added tab
  693. if (hasLoaded && ctrl.autoselect) $mdUtil.nextTick(function () {
  694. $mdUtil.nextTick(function () { select(ctrl.tabs.indexOf(tab)); });
  695. });
  696. });
  697. return tab;
  698. }
  699. // Getter methods
  700. /**
  701. * Gathers references to all of the DOM elements used by this controller.
  702. * @returns {{}}
  703. */
  704. function getElements () {
  705. var elements = {};
  706. var node = $element[0];
  707. // gather tab bar elements
  708. elements.wrapper = node.querySelector('md-tabs-wrapper');
  709. elements.canvas = elements.wrapper.querySelector('md-tabs-canvas');
  710. elements.paging = elements.canvas.querySelector('md-pagination-wrapper');
  711. elements.inkBar = elements.paging.querySelector('md-ink-bar');
  712. elements.nextButton = node.querySelector('md-next-button');
  713. elements.prevButton = node.querySelector('md-prev-button');
  714. elements.contents = node.querySelectorAll('md-tabs-content-wrapper > md-tab-content');
  715. elements.tabs = elements.paging.querySelectorAll('md-tab-item');
  716. elements.dummies = elements.canvas.querySelectorAll('md-dummy-tab');
  717. return elements;
  718. }
  719. /**
  720. * Determines whether or not the left pagination arrow should be enabled.
  721. * @returns {boolean}
  722. */
  723. function canPageBack () {
  724. // This works for both LTR and RTL
  725. return ctrl.offsetLeft > 0;
  726. }
  727. /**
  728. * Determines whether or not the right pagination arrow should be enabled.
  729. * @returns {*|boolean}
  730. */
  731. function canPageForward () {
  732. var elements = getElements();
  733. var lastTab = elements.tabs[ elements.tabs.length - 1 ];
  734. if (isRtl()) {
  735. return ctrl.offsetLeft < elements.paging.offsetWidth - elements.canvas.offsetWidth;
  736. }
  737. return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth +
  738. ctrl.offsetLeft;
  739. }
  740. /**
  741. * Returns currently focused tab item's element ID
  742. */
  743. function getFocusedTabId() {
  744. var focusedTab = ctrl.tabs[ctrl.focusIndex];
  745. if (!focusedTab || !focusedTab.id) {
  746. return null;
  747. }
  748. return 'tab-item-' + focusedTab.id;
  749. }
  750. /**
  751. * Determines if the UI should stretch the tabs to fill the available space.
  752. * @returns {*}
  753. */
  754. function shouldStretchTabs () {
  755. switch (ctrl.stretchTabs) {
  756. case 'always':
  757. return true;
  758. case 'never':
  759. return false;
  760. default:
  761. return !ctrl.shouldPaginate
  762. && $window.matchMedia('(max-width: 600px)').matches;
  763. }
  764. }
  765. /**
  766. * Determines if the tabs should appear centered.
  767. * @returns {string|boolean}
  768. */
  769. function shouldCenterTabs () {
  770. return ctrl.centerTabs && !ctrl.shouldPaginate;
  771. }
  772. /**
  773. * Determines if pagination is necessary to display the tabs within the available space.
  774. * @returns {boolean}
  775. */
  776. function shouldPaginate () {
  777. if (ctrl.noPagination || !loaded) return false;
  778. var canvasWidth = $element.prop('clientWidth');
  779. angular.forEach(getElements().tabs, function (tab) {
  780. canvasWidth -= tab.offsetWidth;
  781. });
  782. return canvasWidth < 0;
  783. }
  784. /**
  785. * Finds the nearest tab index that is available. This is primarily used for when the active
  786. * tab is removed.
  787. * @param newIndex
  788. * @returns {*}
  789. */
  790. function getNearestSafeIndex (newIndex) {
  791. if (newIndex === -1) return -1;
  792. var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex),
  793. i, tab;
  794. for (i = 0; i <= maxOffset; i++) {
  795. tab = ctrl.tabs[ newIndex + i ];
  796. if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
  797. tab = ctrl.tabs[ newIndex - i ];
  798. if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
  799. }
  800. return newIndex;
  801. }
  802. // Utility methods
  803. /**
  804. * Defines a property using a getter and setter in order to trigger a change handler without
  805. * using `$watch` to observe changes.
  806. * @param key
  807. * @param handler
  808. * @param value
  809. */
  810. function defineProperty (key, handler, value) {
  811. Object.defineProperty(ctrl, key, {
  812. get: function () { return value; },
  813. set: function (newValue) {
  814. var oldValue = value;
  815. value = newValue;
  816. handler && handler(newValue, oldValue);
  817. }
  818. });
  819. }
  820. /**
  821. * Updates whether or not pagination should be displayed.
  822. */
  823. function updatePagination () {
  824. ctrl.maxTabWidth = getMaxTabWidth();
  825. ctrl.shouldPaginate = shouldPaginate();
  826. }
  827. /**
  828. * Calculates the width of the pagination wrapper by summing the widths of the dummy tabs.
  829. * @returns {number}
  830. */
  831. function calcPagingWidth () {
  832. return calcTabsWidth(getElements().tabs);
  833. }
  834. function calcTabsWidth(tabs) {
  835. var width = 0;
  836. angular.forEach(tabs, function (tab) {
  837. //-- Uses the larger value between `getBoundingClientRect().width` and `offsetWidth`. This
  838. // prevents `offsetWidth` value from being rounded down and causing wrapping issues, but
  839. // also handles scenarios where `getBoundingClientRect()` is inaccurate (ie. tabs inside
  840. // of a dialog)
  841. width += Math.max(tab.offsetWidth, tab.getBoundingClientRect().width);
  842. });
  843. return Math.ceil(width);
  844. }
  845. function getMaxTabWidth () {
  846. var elements = getElements(),
  847. containerWidth = elements.canvas.clientWidth,
  848. // See https://material.google.com/components/tabs.html#tabs-specs
  849. specMax = 264;
  850. // Do the spec maximum, or the canvas width; whichever is *smaller* (tabs larger than the canvas
  851. // width can break the pagination) but not less than 0
  852. return Math.max(0, Math.min(containerWidth - 1, specMax));
  853. }
  854. function getMinTabWidth() {
  855. var elements = getElements(),
  856. containerWidth = elements.canvas.clientWidth,
  857. xsBreakpoint = 600,
  858. // See https://material.google.com/components/tabs.html#tabs-specs
  859. specMin = containerWidth > xsBreakpoint ? 160 : 72;
  860. // Do the spec minimum, or the canvas width; whichever is *smaller* (tabs larger than the canvas
  861. // width can break the pagination) but not less than 0
  862. return Math.max(0, Math.min(containerWidth - 1, specMin));
  863. }
  864. /**
  865. * Re-orders the tabs and updates the selected and focus indexes to their new positions.
  866. * This is triggered by `tabDirective.js` when the user's tabs have been re-ordered.
  867. */
  868. function updateTabOrder () {
  869. var selectedItem = ctrl.tabs[ ctrl.selectedIndex ],
  870. focusItem = ctrl.tabs[ ctrl.focusIndex ];
  871. ctrl.tabs = ctrl.tabs.sort(function (a, b) {
  872. return a.index - b.index;
  873. });
  874. ctrl.selectedIndex = ctrl.tabs.indexOf(selectedItem);
  875. ctrl.focusIndex = ctrl.tabs.indexOf(focusItem);
  876. }
  877. /**
  878. * This moves the selected or focus index left or right. This is used by the keydown handler.
  879. * @param inc
  880. */
  881. function incrementIndex (inc, focus) {
  882. var newIndex,
  883. key = focus ? 'focusIndex' : 'selectedIndex',
  884. index = ctrl[ key ];
  885. for (newIndex = index + inc;
  886. ctrl.tabs[ newIndex ] && ctrl.tabs[ newIndex ].scope.disabled;
  887. newIndex += inc) { /* do nothing */ }
  888. newIndex = (index + inc + ctrl.tabs.length) % ctrl.tabs.length;
  889. if (ctrl.tabs[ newIndex ]) {
  890. ctrl[ key ] = newIndex;
  891. }
  892. }
  893. /**
  894. * This is used to forward focus to tab container elements. This method is necessary to avoid
  895. * animation issues when attempting to focus an item that is out of view.
  896. */
  897. function redirectFocus () {
  898. ctrl.styleTabItemFocus = ($mdInteraction.getLastInteractionType() === 'keyboard');
  899. getElements().tabs[ ctrl.focusIndex ].focus();
  900. }
  901. /**
  902. * Forces the pagination to move the focused tab into view.
  903. */
  904. function adjustOffset (index) {
  905. var elements = getElements();
  906. if (!angular.isNumber(index)) index = ctrl.focusIndex;
  907. if (!elements.tabs[ index ]) return;
  908. if (ctrl.shouldCenterTabs) return;
  909. var tab = elements.tabs[ index ],
  910. left = tab.offsetLeft,
  911. right = tab.offsetWidth + left,
  912. extraOffset = 32;
  913. // If we are selecting the first tab (in LTR and RTL), always set the offset to 0
  914. if (index == 0) {
  915. ctrl.offsetLeft = 0;
  916. return;
  917. }
  918. if (isRtl()) {
  919. var tabWidthsBefore = calcTabsWidth(Array.prototype.slice.call(elements.tabs, 0, index));
  920. var tabWidthsIncluding = calcTabsWidth(Array.prototype.slice.call(elements.tabs, 0, index + 1));
  921. ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(tabWidthsBefore));
  922. ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(tabWidthsIncluding - elements.canvas.clientWidth));
  923. } else {
  924. ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth + extraOffset));
  925. ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left));
  926. }
  927. }
  928. /**
  929. * Iterates through all queued functions and clears the queue. This is used for functions that
  930. * are called before the UI is ready, such as size calculations.
  931. */
  932. function processQueue () {
  933. queue.forEach(function (func) { $mdUtil.nextTick(func); });
  934. queue = [];
  935. }
  936. /**
  937. * Determines if the tab content area is needed.
  938. */
  939. function updateHasContent () {
  940. var hasContent = false;
  941. for (var i = 0; i < ctrl.tabs.length; i++) {
  942. if (ctrl.tabs[i].hasContent) {
  943. hasContent = true;
  944. break;
  945. }
  946. }
  947. ctrl.hasContent = hasContent;
  948. }
  949. /**
  950. * Moves the indexes to their nearest valid values.
  951. */
  952. function refreshIndex () {
  953. ctrl.selectedIndex = getNearestSafeIndex(ctrl.selectedIndex);
  954. ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex);
  955. }
  956. /**
  957. * Calculates the content height of the current tab.
  958. * @returns {*}
  959. */
  960. function updateHeightFromContent () {
  961. if (!ctrl.dynamicHeight) return $element.css('height', '');
  962. if (!ctrl.tabs.length) return queue.push(updateHeightFromContent);
  963. var elements = getElements();
  964. var tabContent = elements.contents[ ctrl.selectedIndex ],
  965. contentHeight = tabContent ? tabContent.offsetHeight : 0,
  966. tabsHeight = elements.wrapper.offsetHeight,
  967. newHeight = contentHeight + tabsHeight,
  968. currentHeight = $element.prop('clientHeight');
  969. if (currentHeight === newHeight) return;
  970. // Adjusts calculations for when the buttons are bottom-aligned since this relies on absolute
  971. // positioning. This should probably be cleaned up if a cleaner solution is possible.
  972. if ($element.attr('md-align-tabs') === 'bottom') {
  973. currentHeight -= tabsHeight;
  974. newHeight -= tabsHeight;
  975. // Need to include bottom border in these calculations
  976. if ($element.attr('md-border-bottom') !== undefined) ++currentHeight;
  977. }
  978. // Lock during animation so the user can't change tabs
  979. locked = true;
  980. var fromHeight = { height: currentHeight + 'px' },
  981. toHeight = { height: newHeight + 'px' };
  982. // Set the height to the current, specific pixel height to fix a bug on iOS where the height
  983. // first animates to 0, then back to the proper height causing a visual glitch
  984. $element.css(fromHeight);
  985. // Animate the height from the old to the new
  986. $animateCss($element, {
  987. from: fromHeight,
  988. to: toHeight,
  989. easing: 'cubic-bezier(0.35, 0, 0.25, 1)',
  990. duration: 0.5
  991. }).start().done(function () {
  992. // Then (to fix the same iOS issue as above), disable transitions and remove the specific
  993. // pixel height so the height can size with browser width/content changes, etc.
  994. $element.css({
  995. transition: 'none',
  996. height: ''
  997. });
  998. // In the next tick, re-allow transitions (if we do it all at once, $element.css is "smart"
  999. // enough to batch it for us instead of doing it immediately, which undoes the original
  1000. // transition: none)
  1001. $mdUtil.nextTick(function() {
  1002. $element.css('transition', '');
  1003. });
  1004. // And unlock so tab changes can occur
  1005. locked = false;
  1006. });
  1007. }
  1008. /**
  1009. * Repositions the ink bar to the selected tab.
  1010. * @returns {*}
  1011. */
  1012. function updateInkBarStyles () {
  1013. var elements = getElements();
  1014. if (!elements.tabs[ ctrl.selectedIndex ]) {
  1015. angular.element(elements.inkBar).css({ left: 'auto', right: 'auto' });
  1016. return;
  1017. }
  1018. if (!ctrl.tabs.length) return queue.push(ctrl.updateInkBarStyles);
  1019. // if the element is not visible, we will not be able to calculate sizes until it is
  1020. // we should treat that as a resize event rather than just updating the ink bar
  1021. if (!$element.prop('offsetParent')) return handleResizeWhenVisible();
  1022. var index = ctrl.selectedIndex,
  1023. totalWidth = elements.paging.offsetWidth,
  1024. tab = elements.tabs[ index ],
  1025. left = tab.offsetLeft,
  1026. right = totalWidth - left - tab.offsetWidth;
  1027. if (ctrl.shouldCenterTabs) {
  1028. // We need to use the same calculate process as in the pagination wrapper, to avoid rounding deviations.
  1029. var tabWidth = calcTabsWidth(elements.tabs);
  1030. if (totalWidth > tabWidth) {
  1031. $mdUtil.nextTick(updateInkBarStyles, false);
  1032. }
  1033. }
  1034. updateInkBarClassName();
  1035. angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' });
  1036. }
  1037. /**
  1038. * Adds left/right classes so that the ink bar will animate properly.
  1039. */
  1040. function updateInkBarClassName () {
  1041. var elements = getElements();
  1042. var newIndex = ctrl.selectedIndex,
  1043. oldIndex = ctrl.lastSelectedIndex,
  1044. ink = angular.element(elements.inkBar);
  1045. if (!angular.isNumber(oldIndex)) return;
  1046. ink
  1047. .toggleClass('md-left', newIndex < oldIndex)
  1048. .toggleClass('md-right', newIndex > oldIndex);
  1049. }
  1050. /**
  1051. * Takes an offset value and makes sure that it is within the min/max allowed values.
  1052. * @param value
  1053. * @returns {*}
  1054. */
  1055. function fixOffset (value) {
  1056. var elements = getElements();
  1057. if (!elements.tabs.length || !ctrl.shouldPaginate) return 0;
  1058. var lastTab = elements.tabs[ elements.tabs.length - 1 ],
  1059. totalWidth = lastTab.offsetLeft + lastTab.offsetWidth;
  1060. if (isRtl()) {
  1061. value = Math.min(elements.paging.offsetWidth - elements.canvas.clientWidth, value);
  1062. value = Math.max(0, value);
  1063. } else {
  1064. value = Math.max(0, value);
  1065. value = Math.min(totalWidth - elements.canvas.clientWidth, value);
  1066. }
  1067. return value;
  1068. }
  1069. /**
  1070. * Attaches a ripple to the tab item element.
  1071. * @param scope
  1072. * @param element
  1073. */
  1074. function attachRipple (scope, element) {
  1075. var elements = getElements();
  1076. var options = { colorElement: angular.element(elements.inkBar) };
  1077. $mdTabInkRipple.attach(scope, element, options);
  1078. }
  1079. /**
  1080. * Sets the `aria-controls` attribute to the elements that
  1081. * correspond to the passed-in tab.
  1082. * @param tab
  1083. */
  1084. function setAriaControls (tab) {
  1085. if (tab.hasContent) {
  1086. var nodes = $element[0].querySelectorAll('[md-tab-id="' + tab.id + '"]');
  1087. angular.element(nodes).attr('aria-controls', ctrl.tabContentPrefix + tab.id);
  1088. }
  1089. }
  1090. function isRtl() {
  1091. return ($mdUtil.bidi() == 'rtl');
  1092. }
  1093. }
  1094. /**
  1095. * @ngdoc directive
  1096. * @name mdTabs
  1097. * @module material.components.tabs
  1098. *
  1099. * @restrict E
  1100. *
  1101. * @description
  1102. * The `<md-tabs>` directive serves as the container for 1..n `<md-tab>` child directives to
  1103. * produces a Tabs components. In turn, the nested `<md-tab>` directive is used to specify a tab
  1104. * label for the **header button** and a [optional] tab view content that will be associated with
  1105. * each tab button.
  1106. *
  1107. * Below is the markup for its simplest usage:
  1108. *
  1109. * <hljs lang="html">
  1110. * <md-tabs>
  1111. * <md-tab label="Tab #1"></md-tab>
  1112. * <md-tab label="Tab #2"></md-tab>
  1113. * <md-tab label="Tab #3"></md-tab>
  1114. * </md-tabs>
  1115. * </hljs>
  1116. *
  1117. * Tabs supports three (3) usage scenarios:
  1118. *
  1119. * 1. Tabs (buttons only)
  1120. * 2. Tabs with internal view content
  1121. * 3. Tabs with external view content
  1122. *
  1123. * **Tab-only** support is useful when tab buttons are used for custom navigation regardless of any
  1124. * other components, content, or views.
  1125. *
  1126. * <i><b>Note:</b> If you are using the Tabs component for page-level navigation, please take a look
  1127. * at the <a ng-href="./api/directive/mdNavBar">NavBar component</a> instead as it can handle this
  1128. * case a bit more natively.</i>
  1129. *
  1130. * **Tabs with internal views** are the traditional usages where each tab has associated view
  1131. * content and the view switching is managed internally by the Tabs component.
  1132. *
  1133. * **Tabs with external view content** is often useful when content associated with each tab is
  1134. * independently managed and data-binding notifications announce tab selection changes.
  1135. *
  1136. * Additional features also include:
  1137. *
  1138. * * Content can include any markup.
  1139. * * If a tab is disabled while active/selected, then the next tab will be auto-selected.
  1140. *
  1141. * ### Explanation of tab stretching
  1142. *
  1143. * Initially, tabs will have an inherent size. This size will either be defined by how much space is needed to accommodate their text or set by the user through CSS. Calculations will be based on this size.
  1144. *
  1145. * On mobile devices, tabs will be expanded to fill the available horizontal space. When this happens, all tabs will become the same size.
  1146. *
  1147. * On desktops, by default, stretching will never occur.
  1148. *
  1149. * This default behavior can be overridden through the `md-stretch-tabs` attribute. Here is a table showing when stretching will occur:
  1150. *
  1151. * `md-stretch-tabs` | mobile | desktop
  1152. * ------------------|-----------|--------
  1153. * `auto` | stretched | ---
  1154. * `always` | stretched | stretched
  1155. * `never` | --- | ---
  1156. *
  1157. * @param {integer=} md-selected Index of the active/selected tab
  1158. * @param {boolean=} md-no-ink If present, disables ink ripple effects.
  1159. * @param {boolean=} md-no-ink-bar If present, disables the selection ink bar.
  1160. * @param {string=} md-align-tabs Attribute to indicate position of tab buttons: `bottom` or `top`; default is `top`
  1161. * @param {string=} md-stretch-tabs Attribute to indicate whether or not to stretch tabs: `auto`, `always`, or `never`; default is `auto`
  1162. * @param {boolean=} md-dynamic-height When enabled, the tab wrapper will resize based on the contents of the selected tab
  1163. * @param {boolean=} md-border-bottom If present, shows a solid `1px` border between the tabs and their content
  1164. * @param {boolean=} md-center-tabs When enabled, tabs will be centered provided there is no need for pagination
  1165. * @param {boolean=} md-no-pagination When enabled, pagination will remain off
  1166. * @param {boolean=} md-swipe-content When enabled, swipe gestures will be enabled for the content area to jump between tabs
  1167. * @param {boolean=} md-enable-disconnect When enabled, scopes will be disconnected for tabs that are not being displayed. This provides a performance boost, but may also cause unexpected issues and is not recommended for most users.
  1168. * @param {boolean=} md-autoselect When present, any tabs added after the initial load will be automatically selected
  1169. * @param {boolean=} md-no-select-click When enabled, click events will not be fired when selecting tabs
  1170. *
  1171. * @usage
  1172. * <hljs lang="html">
  1173. * <md-tabs md-selected="selectedIndex" >
  1174. * <img ng-src="img/angular.png" class="centered">
  1175. * <md-tab
  1176. * ng-repeat="tab in tabs | orderBy:predicate:reversed"
  1177. * md-on-select="onTabSelected(tab)"
  1178. * md-on-deselect="announceDeselected(tab)"
  1179. * ng-disabled="tab.disabled">
  1180. * <md-tab-label>
  1181. * {{tab.title}}
  1182. * <img src="img/removeTab.png" ng-click="removeTab(tab)" class="delete">
  1183. * </md-tab-label>
  1184. * <md-tab-body>
  1185. * {{tab.content}}
  1186. * </md-tab-body>
  1187. * </md-tab>
  1188. * </md-tabs>
  1189. * </hljs>
  1190. *
  1191. */
  1192. MdTabs['$inject'] = ["$$mdSvgRegistry"];
  1193. angular
  1194. .module('material.components.tabs')
  1195. .directive('mdTabs', MdTabs);
  1196. function MdTabs ($$mdSvgRegistry) {
  1197. return {
  1198. scope: {
  1199. selectedIndex: '=?mdSelected'
  1200. },
  1201. template: function (element, attr) {
  1202. attr.$mdTabsTemplate = element.html();
  1203. return '' +
  1204. '<md-tabs-wrapper> ' +
  1205. '<md-tab-data></md-tab-data> ' +
  1206. '<md-prev-button ' +
  1207. 'tabindex="-1" ' +
  1208. 'role="button" ' +
  1209. 'aria-label="Previous Page" ' +
  1210. 'aria-disabled="{{!$mdTabsCtrl.canPageBack()}}" ' +
  1211. 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageBack() }" ' +
  1212. 'ng-if="$mdTabsCtrl.shouldPaginate" ' +
  1213. 'ng-click="$mdTabsCtrl.previousPage()"> ' +
  1214. '<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
  1215. '</md-prev-button> ' +
  1216. '<md-next-button ' +
  1217. 'tabindex="-1" ' +
  1218. 'role="button" ' +
  1219. 'aria-label="Next Page" ' +
  1220. 'aria-disabled="{{!$mdTabsCtrl.canPageForward()}}" ' +
  1221. 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageForward() }" ' +
  1222. 'ng-if="$mdTabsCtrl.shouldPaginate" ' +
  1223. 'ng-click="$mdTabsCtrl.nextPage()"> ' +
  1224. '<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
  1225. '</md-next-button> ' +
  1226. '<md-tabs-canvas ' +
  1227. 'ng-focus="$mdTabsCtrl.redirectFocus()" ' +
  1228. 'ng-class="{ ' +
  1229. '\'md-paginated\': $mdTabsCtrl.shouldPaginate, ' +
  1230. '\'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs ' +
  1231. '}" ' +
  1232. 'ng-keydown="$mdTabsCtrl.keydown($event)"> ' +
  1233. '<md-pagination-wrapper ' +
  1234. 'ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs }" ' +
  1235. 'md-tab-scroll="$mdTabsCtrl.scroll($event)" ' +
  1236. 'role="tablist"> ' +
  1237. '<md-tab-item ' +
  1238. 'tabindex="{{ tab.isActive() ? 0 : -1 }}" ' +
  1239. 'class="md-tab" ' +
  1240. 'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
  1241. 'role="tab" ' +
  1242. 'id="tab-item-{{::tab.id}}" ' +
  1243. 'md-tab-id="{{::tab.id}}"' +
  1244. 'aria-selected="{{tab.isActive()}}" ' +
  1245. 'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
  1246. 'ng-click="$mdTabsCtrl.select(tab.getIndex())" ' +
  1247. 'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
  1248. 'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
  1249. 'ng-class="{ ' +
  1250. '\'md-active\': tab.isActive(), ' +
  1251. '\'md-focused\': tab.hasFocus(), ' +
  1252. '\'md-disabled\': tab.scope.disabled ' +
  1253. '}" ' +
  1254. 'ng-disabled="tab.scope.disabled" ' +
  1255. 'md-swipe-left="$mdTabsCtrl.nextPage()" ' +
  1256. 'md-swipe-right="$mdTabsCtrl.previousPage()" ' +
  1257. 'md-tabs-template="::tab.label" ' +
  1258. 'md-scope="::tab.parent"></md-tab-item> ' +
  1259. '<md-ink-bar></md-ink-bar> ' +
  1260. '</md-pagination-wrapper> ' +
  1261. '<md-tabs-dummy-wrapper aria-hidden="true" class="md-visually-hidden md-dummy-wrapper"> ' +
  1262. '<md-dummy-tab ' +
  1263. 'class="md-tab" ' +
  1264. 'tabindex="-1" ' +
  1265. 'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
  1266. 'md-tabs-template="::tab.label" ' +
  1267. 'md-scope="::tab.parent"></md-dummy-tab> ' +
  1268. '</md-tabs-dummy-wrapper> ' +
  1269. '</md-tabs-canvas> ' +
  1270. '</md-tabs-wrapper> ' +
  1271. '<md-tabs-content-wrapper ng-show="$mdTabsCtrl.hasContent && $mdTabsCtrl.selectedIndex >= 0" class="_md"> ' +
  1272. '<md-tab-content ' +
  1273. 'id="{{:: $mdTabsCtrl.tabContentPrefix + tab.id}}" ' +
  1274. 'class="_md" ' +
  1275. 'role="tabpanel" ' +
  1276. 'aria-labelledby="tab-item-{{::tab.id}}" ' +
  1277. 'md-swipe-left="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(1)" ' +
  1278. 'md-swipe-right="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(-1)" ' +
  1279. 'ng-if="tab.hasContent" ' +
  1280. 'ng-repeat="(index, tab) in $mdTabsCtrl.tabs" ' +
  1281. 'ng-class="{ ' +
  1282. '\'md-no-transition\': $mdTabsCtrl.lastSelectedIndex == null, ' +
  1283. '\'md-active\': tab.isActive(), ' +
  1284. '\'md-left\': tab.isLeft(), ' +
  1285. '\'md-right\': tab.isRight(), ' +
  1286. '\'md-no-scroll\': $mdTabsCtrl.dynamicHeight ' +
  1287. '}"> ' +
  1288. '<div ' +
  1289. 'md-tabs-template="::tab.template" ' +
  1290. 'md-connected-if="tab.isActive()" ' +
  1291. 'md-scope="::tab.parent" ' +
  1292. 'ng-if="$mdTabsCtrl.enableDisconnect || tab.shouldRender()"></div> ' +
  1293. '</md-tab-content> ' +
  1294. '</md-tabs-content-wrapper>';
  1295. },
  1296. controller: 'MdTabsController',
  1297. controllerAs: '$mdTabsCtrl',
  1298. bindToController: true
  1299. };
  1300. }
  1301. MdTabsDummyWrapper['$inject'] = ["$mdUtil", "$window"];angular
  1302. .module('material.components.tabs')
  1303. .directive('mdTabsDummyWrapper', MdTabsDummyWrapper);
  1304. /**
  1305. * @private
  1306. *
  1307. * @param $mdUtil
  1308. * @param $window
  1309. * @returns {{require: string, link: link}}
  1310. * @constructor
  1311. *
  1312. * ngInject
  1313. */
  1314. function MdTabsDummyWrapper ($mdUtil, $window) {
  1315. return {
  1316. require: '^?mdTabs',
  1317. link: function link (scope, element, attr, ctrl) {
  1318. if (!ctrl) return;
  1319. var observer;
  1320. var disconnect;
  1321. var mutationCallback = function() {
  1322. ctrl.updatePagination();
  1323. ctrl.updateInkBarStyles();
  1324. };
  1325. if('MutationObserver' in $window) {
  1326. var config = {
  1327. childList: true,
  1328. subtree: true,
  1329. // Per https://bugzilla.mozilla.org/show_bug.cgi?id=1138368, browsers will not fire
  1330. // the childList mutation, once a <span> element's innerText changes.
  1331. // The characterData of the <span> element will change.
  1332. characterData: true
  1333. };
  1334. observer = new MutationObserver(mutationCallback);
  1335. observer.observe(element[0], config);
  1336. disconnect = observer.disconnect.bind(observer);
  1337. } else {
  1338. var debounced = $mdUtil.debounce(mutationCallback, 15, null, false);
  1339. element.on('DOMSubtreeModified', debounced);
  1340. disconnect = element.off.bind(element, 'DOMSubtreeModified', debounced);
  1341. }
  1342. // Disconnect the observer
  1343. scope.$on('$destroy', function() {
  1344. disconnect();
  1345. });
  1346. }
  1347. };
  1348. }
  1349. MdTabsTemplate['$inject'] = ["$compile", "$mdUtil"];angular
  1350. .module('material.components.tabs')
  1351. .directive('mdTabsTemplate', MdTabsTemplate);
  1352. function MdTabsTemplate ($compile, $mdUtil) {
  1353. return {
  1354. restrict: 'A',
  1355. link: link,
  1356. scope: {
  1357. template: '=mdTabsTemplate',
  1358. connected: '=?mdConnectedIf',
  1359. compileScope: '=mdScope'
  1360. },
  1361. require: '^?mdTabs'
  1362. };
  1363. function link (scope, element, attr, ctrl) {
  1364. if (!ctrl) return;
  1365. var compileScope = ctrl.enableDisconnect ? scope.compileScope.$new() : scope.compileScope;
  1366. element.html(scope.template);
  1367. $compile(element.contents())(compileScope);
  1368. return $mdUtil.nextTick(handleScope);
  1369. function handleScope () {
  1370. scope.$watch('connected', function (value) { value === false ? disconnect() : reconnect(); });
  1371. scope.$on('$destroy', reconnect);
  1372. }
  1373. function disconnect () {
  1374. if (ctrl.enableDisconnect) $mdUtil.disconnectScope(compileScope);
  1375. }
  1376. function reconnect () {
  1377. if (ctrl.enableDisconnect) $mdUtil.reconnectScope(compileScope);
  1378. }
  1379. }
  1380. }
  1381. ngmaterial.components.tabs = angular.module("material.components.tabs");