diff --git a/misc/demo/index.html b/misc/demo/index.html index 5251a873ee..ec3c1482dd 100644 --- a/misc/demo/index.html +++ b/misc/demo/index.html @@ -233,13 +233,13 @@

<%= module.displayName %>
- - + +
<%- module.docs.html %>
- +
<%- module.docs.js %>
diff --git a/src/tabs/docs/demo.html b/src/tabs/docs/demo.html index 62f66138c5..0c4c24d298 100644 --- a/src/tabs/docs/demo.html +++ b/src/tabs/docs/demo.html @@ -7,20 +7,20 @@

Select a tab by setting active binding to true:

- - + +


- - Static content - + + Static content + {{tab.content}} - + Alert! @@ -30,25 +30,25 @@
- - Vertical content 1 - Vertical content 2 + + Vertical content 1 + Vertical content 2
- - Justified content - Short Labeled Justified content - Long Labeled Justified content + + Justified content + Short Labeled Justified content + Long Labeled Justified content
Tabs using nested forms:
- - + +
@@ -56,10 +56,10 @@
- + Some Tab Content - + More Tab Content
diff --git a/src/tabs/docs/readme.md b/src/tabs/docs/readme.md index fe8d348051..2d41bf4d4b 100644 --- a/src/tabs/docs/readme.md +++ b/src/tabs/docs/readme.md @@ -2,6 +2,11 @@ AngularJS version of the tabs directive. ### uib-tabset settings +* `active` + $ + _(Default: Index of first tab)_ + Active index of tab. + * `justified` $ _(Default: `false`)_ - @@ -37,6 +42,9 @@ AngularJS version of the tabs directive. * `heading` - Heading text. +* `index` - + Tab index + * `select()` $ - An optional expression called when tab is activated. diff --git a/src/tabs/tabs.js b/src/tabs/tabs.js index 527db24ee5..6cbce5818f 100644 --- a/src/tabs/tabs.js +++ b/src/tabs/tabs.js @@ -2,66 +2,103 @@ angular.module('ui.bootstrap.tabs', []) .controller('UibTabsetController', ['$scope', function ($scope) { var ctrl = this, - tabs = ctrl.tabs = $scope.tabs = []; - - ctrl.select = function(selectedTab) { - angular.forEach(tabs, function(tab) { - if (tab.active && tab !== selectedTab) { - tab.active = false; - tab.onDeselect(); - selectedTab.selectCalled = false; + oldIndex; + ctrl.tabs = []; + + ctrl.select = function(index) { + if (!destroyed) { + var previousIndex = findTabIndex(oldIndex); + var previousSelected = ctrl.tabs[previousIndex]; + if (previousSelected) { + previousSelected.tab.onDeselect(); + previousSelected.tab.active = false; + } + + var selected = ctrl.tabs[index]; + if (selected) { + selected.tab.onSelect(); + selected.tab.active = true; + ctrl.active = selected.index; + oldIndex = selected.index; + } else if (!selected && angular.isNumber(oldIndex)) { + ctrl.active = null; + oldIndex = null; } - }); - selectedTab.active = true; - // only call select if it has not already been called - if (!selectedTab.selectCalled) { - selectedTab.onSelect(); - selectedTab.selectCalled = true; } }; ctrl.addTab = function addTab(tab) { - tabs.push(tab); - // we can't run the select function on the first tab - // since that would select it twice - if (tabs.length === 1 && tab.active !== false) { - tab.active = true; - } else if (tab.active) { - ctrl.select(tab); - } else { - tab.active = false; + ctrl.tabs.push({ + tab: tab, + index: tab.index + }); + ctrl.tabs.sort(function(t1, t2) { + if (t1.index > t2.index) { + return 1; + } + + if (t1.index < t2.index) { + return -1; + } + + return 0; + }); + + if (tab.index === ctrl.active || !angular.isNumber(ctrl.active) && ctrl.tabs.length === 1) { + var newActiveIndex = findTabIndex(tab.index); + ctrl.select(newActiveIndex); } }; ctrl.removeTab = function removeTab(tab) { - var index = tabs.indexOf(tab); - //Select a new tab if the tab to be removed is selected and not destroyed - if (tab.active && tabs.length > 1 && !destroyed) { - //If this is the last tab, select the previous tab. else, the next tab. - var newActiveIndex = index === tabs.length - 1 ? index - 1 : index + 1; - ctrl.select(tabs[newActiveIndex]); + var index = findTabIndex(tab.index); + + if (tab.index === ctrl.active) { + var newActiveTabIndex = index === ctrl.tabs.length - 1 ? + index - 1 : index + 1 % ctrl.tabs.length; + ctrl.select(newActiveTabIndex); } - tabs.splice(index, 1); + + ctrl.tabs.splice(index, 1); }; + $scope.$watch('tabset.active', function(val) { + if (angular.isNumber(val) && val !== oldIndex) { + ctrl.select(findTabIndex(val)); + } + }); + var destroyed; $scope.$on('$destroy', function() { destroyed = true; }); + + function findTabIndex(index) { + for (var i = 0; i < ctrl.tabs.length; i++) { + if (ctrl.tabs[i].index === index) { + return i; + } + } + } }]) .directive('uibTabset', function() { return { transclude: true, replace: true, - scope: { + scope: {}, + bindToController: { + active: '=', type: '@' }, controller: 'UibTabsetController', + controllerAs: 'tabset', templateUrl: 'uib/template/tabs/tabset.html', link: function(scope, element, attrs) { - scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; - scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; + scope.vertical = angular.isDefined(attrs.vertical) ? + scope.$parent.$eval(attrs.vertical) : false; + scope.justified = angular.isDefined(attrs.justified) ? + scope.$parent.$eval(attrs.justified) : false; } }; }) @@ -73,8 +110,8 @@ angular.module('ui.bootstrap.tabs', []) templateUrl: 'uib/template/tabs/tab.html', transclude: true, scope: { - active: '=?', heading: '@', + index: '=', onSelect: '&select', //This callback is called in contentHeadingTransclude //once it inserts the tab's content into the dom onDeselect: '&deselect' @@ -84,12 +121,6 @@ angular.module('ui.bootstrap.tabs', []) }, controllerAs: 'tab', link: function(scope, elm, attrs, tabsetCtrl, transclude) { - scope.$watch('active', function(active) { - if (active) { - tabsetCtrl.select(scope); - } - }); - scope.disabled = false; if (attrs.disable) { scope.$parent.$watch($parse(attrs.disable), function(value) { @@ -99,7 +130,15 @@ angular.module('ui.bootstrap.tabs', []) scope.select = function() { if (!scope.disabled) { - scope.active = true; + var index; + for (var i = 0; i < tabsetCtrl.tabs.length; i++) { + if (tabsetCtrl.tabs[i].tab === scope) { + index = i; + break; + } + } + + tabsetCtrl.select(index); } }; @@ -135,7 +174,7 @@ angular.module('ui.bootstrap.tabs', []) restrict: 'A', require: '^uibTabset', link: function(scope, elm, attrs) { - var tab = scope.$eval(attrs.uibTabContentTransclude); + var tab = scope.$eval(attrs.uibTabContentTransclude).tab; //Now our tab is ready to be transcluded: both the tab heading area //and the tab content area are loaded. Transclude 'em both. diff --git a/src/tabs/test/tabs.spec.js b/src/tabs/test/tabs.spec.js index bbc0a0d80f..544b442706 100644 --- a/src/tabs/test/tabs.spec.js +++ b/src/tabs/test/tabs.spec.js @@ -15,7 +15,7 @@ describe('tabs', function() { function expectTitles(titlesArray) { var t = titles(); expect(t.length).toEqual(titlesArray.length); - for (var i=0; i', - ' ', + '', + ' ', ' first content is {{first}}', ' ', - ' ', + ' ', ' Second Tab {{second}}', ' second content is {{second}}', ' ', @@ -71,8 +71,7 @@ describe('tabs', function() { expectContents(['first content is 1', 'second content is 2']); expect(titles().eq(0)).toHaveClass('active'); expect(titles().eq(1)).not.toHaveClass('active'); - expect(scope.actives.one).toBe(true); - expect(scope.actives.two).toBeFalsy(); + expect(scope.active).toBe(1); }); it('should change active on click', function() { @@ -80,8 +79,7 @@ describe('tabs', function() { expect(contents().eq(1)).toHaveClass('active'); expect(titles().eq(0)).not.toHaveClass('active'); expect(titles().eq(1)).toHaveClass('active'); - expect(scope.actives.one).toBe(false); - expect(scope.actives.two).toBe(true); + expect(scope.active).toBe(2); }); it('should call select callback on select', function() { @@ -93,10 +91,11 @@ describe('tabs', function() { it('should call deselect callback on deselect', function() { titles().eq(1).find('> a').click(); + expect(scope.deselectFirst).toHaveBeenCalled(); titles().eq(0).find('> a').click(); expect(scope.deselectSecond).toHaveBeenCalled(); titles().eq(1).find('> a').click(); - expect(scope.deselectFirst).toHaveBeenCalled(); + expect(scope.deselectFirst.calls.count()).toBe(2); }); }); @@ -104,24 +103,25 @@ describe('tabs', function() { beforeEach(inject(function($compile, $rootScope) { scope = $rootScope.$new(); - function makeTab(active) { + function makeTab(index) { return { - active: !!active, + index: index, select: jasmine.createSpy() }; } scope.tabs = [ - makeTab(), makeTab(), makeTab(true), makeTab() + makeTab(1), makeTab(3), makeTab(5), makeTab(7) ]; + scope.active = 5; elm = $compile([ - '', - ' ', + '', + ' ', ' ', - ' ', + ' ', ' ', - ' ', + ' ', ' ', - ' ', + ' ', ' ', '' ].join('\n'))(scope); @@ -132,13 +132,13 @@ describe('tabs', function() { var _titles = titles(); angular.forEach(scope.tabs, function(tab, i) { if (activeTab === tab) { - expect(tab.active).toBe(true); + expect(scope.active).toBe(tab.index); //It should only call select ONCE for each select expect(tab.select).toHaveBeenCalled(); expect(_titles.eq(i)).toHaveClass('active'); expect(contents().eq(i)).toHaveClass('active'); } else { - expect(tab.active).toBe(false); + expect(scope.active).not.toBe(tab.index); expect(_titles.eq(i)).not.toHaveClass('active'); } }); @@ -155,7 +155,6 @@ describe('tabs', function() { beforeEach(inject(function($compile, $rootScope) { scope = $rootScope.$new(); execOrder = []; - scope.actives = {}; scope.execute = function(id) { execOrder.push(id); @@ -163,9 +162,9 @@ describe('tabs', function() { elm = $compile([ '
', - ' ', - ' ', - ' ', + ' ', + ' ', + ' ', ' ', '
' ].join('\n'))(scope); @@ -227,11 +226,12 @@ describe('tabs', function() { scope = $rootScope.$new(); scope.tabs = [ - makeTab(), makeTab(), makeTab(true), makeTab() + makeTab(1), makeTab(3), makeTab(5), makeTab(7) ]; + scope.active = 5; elm = $compile([ - '', - ' ', + '', + ' ', ' heading {{index}}', ' content {{$index}}', ' ', @@ -240,9 +240,9 @@ describe('tabs', function() { scope.$apply(); })); - function makeTab(active) { + function makeTab(index) { return { - active: !!active, + index: index, select: jasmine.createSpy() }; } @@ -258,14 +258,14 @@ describe('tabs', function() { var _titles = titles(); angular.forEach(scope.tabs, function(tab, i) { if (activeTab === tab) { - expect(tab.active).toBe(true); + expect(scope.active).toBe(tab.index); //It should only call select ONCE for each select expect(tab.select).toHaveBeenCalled(); expect(_titles.eq(i)).toHaveClass('active'); expect(contents().eq(i).text().trim()).toBe('content ' + i); expect(contents().eq(i)).toHaveClass('active'); } else { - expect(tab.active).toBe(false); + expect(scope.active).not.toBe(tab.index); expect(_titles.eq(i)).not.toHaveClass('active'); } }); @@ -281,18 +281,18 @@ describe('tabs', function() { expectTabActive(scope.tabs[3]); }); - it('should switch active when setting active=true', function() { - scope.$apply('tabs[2].active = true'); + it('should switch active when changing active index', function() { + scope.$apply('active = 5'); expectTabActive(scope.tabs[2]); }); it('should deselect all when no tabs are active', function() { - angular.forEach(scope.tabs, function(t) { t.active = false; }); + scope.active = 101; scope.$apply(); expectTabActive(null); expect(contents().filter('.active').length).toBe(0); - scope.tabs[2].active = true; + scope.active = 5; scope.$apply(); expectTabActive(scope.tabs[2]); }); @@ -303,17 +303,17 @@ describe('tabs', function() { scope = $rootScope.$new(); scope.tabs = [ - makeTab(), makeTab(), makeTab(true), makeTab() + makeTab(2), makeTab(3), makeTab(5), makeTab(8) ]; - scope.foo = {active: true}; + scope.active = 13; scope.select = jasmine.createSpy(); elm = $compile([ - '', - ' ', + '', + ' ', ' heading {{index}}', ' content {{$index}}', ' ', - ' ', + ' ', ' heading foo', ' content foo', ' ', @@ -331,13 +331,13 @@ describe('tabs', function() { scope.myHtml = $sce.trustAsHtml('hello, there!'); scope.value = true; elm = $compile([ - '', - ' ', + '', + ' ', ' ', ' ', - ' 1', - '
2
', - '
3
', + ' 1', + '
2
', + '
3
', '
' ].join('\n'))(scope); scope.$apply(); @@ -377,22 +377,22 @@ describe('tabs', function() { { title:'Title 3', available:true } ]; elm = $compile([ - '', + '', ' ', '
div that makes troubles
', - ' First Static', + ' First Static', '
another div that may do evil
', - ' some content', + ' some content', ' ', ' Mid Static', ' a text node', ' ', ' yet another span that may do evil', - ' some content', + ' some content', ' a text node', ' yet another span that may do evil', ' ', - ' Last Static', + ' Last Static', ' a text node', ' yet another span that may do evil', ' ', @@ -439,14 +439,9 @@ describe('tabs', function() { }); describe('uib-tabset controller', function() { - function mockTab(isActive) { - var _isActive; - if (isActive || isActive === false) { - _isActive = isActive; - } - + function mockTab(index) { return { - active: _isActive, + index: index, onSelect : angular.noop, onDeselect : angular.noop }; @@ -461,76 +456,90 @@ describe('tabs', function() { describe('select', function() { it('should mark given tab selected', function() { - var tab = mockTab(); + ctrl.tabs = [ + { + tab: mockTab(0), + index: 0 + } + ]; - ctrl.select(tab); - expect(tab.active).toBe(true); + ctrl.select(0); + expect(ctrl.active).toBe(0); }); it('should deselect other tabs', function() { - var tab1 = mockTab(), tab2 = mockTab(), tab3 = mockTab(); + var tab1 = mockTab(1), tab2 = mockTab(2), tab3 = mockTab(3); ctrl.addTab(tab1); ctrl.addTab(tab2); ctrl.addTab(tab3); - ctrl.select(tab1); - expect(tab1.active).toBe(true); - expect(tab2.active).toBe(false); - expect(tab3.active).toBe(false); + ctrl.select(0); + expect(ctrl.active).toBe(1); - ctrl.select(tab2); - expect(tab1.active).toBe(false); - expect(tab2.active).toBe(true); - expect(tab3.active).toBe(false); + ctrl.select(1); + expect(ctrl.active).toBe(2); - ctrl.select(tab3); - expect(tab1.active).toBe(false); - expect(tab2.active).toBe(false); - expect(tab3.active).toBe(true); + ctrl.select(2); + expect(ctrl.active).toBe(3); }); }); describe('addTab', function() { it('should append tab', function() { - var tab1 = mockTab(), tab2 = mockTab(); + var tab1 = mockTab(1), tab2 = mockTab(2); expect(ctrl.tabs).toEqual([]); ctrl.addTab(tab1); - expect(ctrl.tabs).toEqual([tab1]); + expect(ctrl.tabs).toEqual([ + { + tab: tab1, + index: 1 + } + ]); ctrl.addTab(tab2); - expect(ctrl.tabs).toEqual([tab1, tab2]); + expect(ctrl.tabs).toEqual([ + { + tab: tab1, + index: 1 + }, + { + tab: tab2, + index: 2 + } + ]); }); it('should select the first one', function() { - var tab1 = mockTab(), tab2 = mockTab(); + var tab1 = mockTab(1), tab2 = mockTab(2); ctrl.addTab(tab1); - expect(tab1.active).toBe(true); + expect(ctrl.active).toBe(1); ctrl.addTab(tab2); - expect(tab1.active).toBe(true); + expect(ctrl.active).toBe(1); }); it('should not select first active === false tab as selected', function() { - var tab = mockTab(false); + var tab = mockTab(0); + ctrl.active = 1; ctrl.addTab(tab); - expect(tab.active).toBe(false); + expect(ctrl.active).toBe(1); }); - it('should select a tab added that\'s already active', function() { - var tab1 = mockTab(), tab2 = mockTab(true); + it('should retain active state when adding tab of different index', function() { + var tab1 = mockTab(1), tab2 = mockTab(2); + ctrl.active = 2; ctrl.addTab(tab1); - expect(tab1.active).toBe(true); + expect(ctrl.active).toBe(2); ctrl.addTab(tab2); - expect(tab1.active).toBe(false); - expect(tab2.active).toBe(true); + expect(ctrl.active).toBe(2); }); }); }); @@ -538,7 +547,7 @@ describe('tabs', function() { describe('remove', function() { it('should remove title tabs when elements are destroyed and change selection', inject(function($controller, $compile, $rootScope) { scope = $rootScope.$new(); - elm = $compile('Hellocontent {{i}}')(scope); + elm = $compile('Hellocontent {{i}}')(scope); scope.$apply(); expectTitles(['1']); @@ -579,27 +588,28 @@ describe('tabs', function() { it('should not select tabs when being destroyed', inject(function($controller, $compile, $rootScope) { var selectList = [], - deselectList = [], - getTab = function(active) { - return { - active: active, - select : function() { - selectList.push('select'); - }, - deselect : function() { - deselectList.push('deselect'); - } - }; + deselectList = [], + getTab = function(index) { + return { + index: index, + select: function() { + selectList.push('select'); + }, + deselect: function() { + deselectList.push('deselect'); + } }; + }; scope = $rootScope.$new(); scope.tabs = [ - getTab(true), - getTab(false) + getTab(0), + getTab(1) ]; + scope.active = 1; elm = $compile([ - '', - ' ', + '', + ' ', ' heading {{index}}', ' content {{$index}}', ' ', @@ -621,19 +631,20 @@ describe('tabs', function() { beforeEach(inject(function($compile, $rootScope) { scope = $rootScope.$new(); - function makeTab(disable) { + function makeTab(disable, index) { return { - active: false, + index: index, select: jasmine.createSpy(), disable: disable }; } scope.tabs = [ - makeTab(false), makeTab(true), makeTab(false), makeTab(true) + makeTab(false, 0), makeTab(true, 1), makeTab(false, 2), makeTab(true, 3) ]; + scope.active = 1; elm = $compile([ - '', - ' ', + '', + ' ', ' heading {{index}}', ' content {{$index}}', ' ', @@ -646,13 +657,13 @@ describe('tabs', function() { var _titles = titles(); angular.forEach(scope.tabs, function(tab, i) { if (activeTab === tab) { - expect(tab.active).toBe(true); + expect(scope.active).toBe(tab.index); expect(tab.select.calls.count()).toBe(tab.disable ? 0 : 1); expect(_titles.eq(i)).toHaveClass('active'); expect(contents().eq(i).text().trim()).toBe('content ' + i); expect(contents().eq(i)).toHaveClass('active'); } else { - expect(tab.active).toBe(false); + expect(scope.active).not.toBe(tab.index); expect(_titles.eq(i)).not.toHaveClass('active'); } }); diff --git a/template/tabs/tabset.html b/template/tabs/tabset.html index 294c86a78b..e5d17f8df8 100644 --- a/template/tabs/tabset.html +++ b/template/tabs/tabset.html @@ -1,9 +1,9 @@
- +
-