src/tab/tab.js
'use strict';
angular.module('mgcrea.ngStrap.tab', [])
.provider('$tab', function () {
var defaults = this.defaults = {
animation: 'am-fade',
template: 'tab/tab.tpl.html',
navClass: 'nav-tabs',
activeClass: 'active'
};
var _tabsHash = {};
var _addTabControl = function (key, control) {
if (!_tabsHash[key]) _tabsHash[key] = control;
};
var controller = this.controller = function ($scope, $element, $attrs) {
var self = this;
// Attributes options
self.$options = angular.copy(defaults);
angular.forEach(['animation', 'navClass', 'activeClass'], function (key) {
if (angular.isDefined($attrs[key])) self.$options[key] = $attrs[key];
});
// Publish options on scope
$scope.$navClass = self.$options.navClass;
$scope.$activeClass = self.$options.activeClass;
self.$panes = $scope.$panes = [];
// Please use $activePaneChangeListeners if you use `bsActivePane`
// Because we removed `ngModel` as default, we rename viewChangeListeners to
// activePaneChangeListeners to make more sense.
self.$activePaneChangeListeners = self.$viewChangeListeners = [];
self.$push = function (pane) {
if (angular.isUndefined(self.$panes.$active)) {
$scope.$setActive(pane.name || 0);
}
self.$panes.push(pane);
};
self.$remove = function (pane) {
var index = self.$panes.indexOf(pane);
var active = self.$panes.$active;
var activeIndex;
if (angular.isString(active)) {
activeIndex = self.$panes.map(function (pane) {
return pane.name;
}).indexOf(active);
} else {
activeIndex = self.$panes.$active;
}
// remove pane from $panes array
self.$panes.splice(index, 1);
if (index < activeIndex) {
// we removed a pane before the active pane, so we need to
// decrement the active pane index
activeIndex--;
} else if (index === activeIndex && activeIndex === self.$panes.length) {
// we remove the active pane and it was the one at the end,
// so select the previous one
activeIndex--;
}
if (activeIndex >= 0 && activeIndex < self.$panes.length) {
self.$setActive(self.$panes[activeIndex].name || activeIndex);
} else {
self.$setActive();
}
};
self.$setActive = $scope.$setActive = function (value) {
self.$panes.$active = value;
self.$activePaneChangeListeners.forEach(function (fn) {
fn();
});
};
self.$isActive = $scope.$isActive = function ($pane, $index) {
return self.$panes.$active === $pane.name || self.$panes.$active === $index;
};
self.$onKeyPress = $scope.$onKeyPress = function (e, index) {
if (e.keyCode === 32 || e.charCode === 32 || e.keyCode === 13 || e.charCode === 13) {
self.$setActive(index);
}
};
};
this.$get = function () {
var $tab = {};
$tab.defaults = defaults;
$tab.controller = controller;
$tab.addTabControl = _addTabControl;
$tab.tabsHash = _tabsHash;
return $tab;
};
})
.directive('bsTabs', function ($window, $animate, $tab, $parse) {
var defaults = $tab.defaults;
return {
require: ['?ngModel', 'bsTabs'],
transclude: true,
scope: true,
controller: ['$scope', '$element', '$attrs', $tab.controller],
templateUrl: function (element, attr) {
return attr.template || defaults.template;
},
link: function postLink (scope, element, attrs, controllers) {
var ngModelCtrl = controllers[0];
var bsTabsCtrl = controllers[1];
// Add a way for developers to access tab scope if needed. This allows for more fine grained control over what
// tabs are available in the tab component
if (attrs.tabKey !== '' && attrs.tabKey !== undefined) {
$tab.addTabControl(attrs.tabKey, bsTabsCtrl);
}
// 'ngModel' does interfere with form validation
// and status, use `bsActivePane` instead to avoid it
if (ngModelCtrl) {
// Update the modelValue following
bsTabsCtrl.$activePaneChangeListeners.push(function () {
ngModelCtrl.$setViewValue(bsTabsCtrl.$panes.$active);
});
// modelValue -> $formatters -> viewValue
ngModelCtrl.$formatters.push(function (modelValue) {
// console.warn('$formatter("%s"): modelValue=%o (%o)', element.attr('ng-model'), modelValue, typeof modelValue);
bsTabsCtrl.$setActive(modelValue);
return modelValue;
});
}
if (attrs.bsActivePane) {
// adapted from angularjs ngModelController bindings
// https://github.com/angular/angular.js/blob/v1.3.1/src%2Fng%2Fdirective%2Finput.js#L1730
var parsedBsActivePane = $parse(attrs.bsActivePane);
// Update bsActivePane value with change
bsTabsCtrl.$activePaneChangeListeners.push(function () {
parsedBsActivePane.assign(scope, bsTabsCtrl.$panes.$active);
});
// watch bsActivePane for value changes
scope.$watch(attrs.bsActivePane, function (newValue, oldValue) {
bsTabsCtrl.$setActive(newValue);
}, true);
}
}
};
})
.directive('bsPane', function ($window, $animate, $sce) {
return {
require: ['^?ngModel', '^bsTabs'],
scope: true,
link: function postLink (scope, element, attrs, controllers) {
// var ngModelCtrl = controllers[0];
var bsTabsCtrl = controllers[1];
// Add base class
element.addClass('tab-pane');
// Observe title attribute for change
attrs.$observe('title', function (newValue, oldValue) {
scope.title = $sce.trustAsHtml(newValue);
});
// Save tab name into scope
scope.name = attrs.name;
// Add animation class
if (bsTabsCtrl.$options.animation) {
element.addClass(bsTabsCtrl.$options.animation);
}
attrs.$observe('disabled', function (newValue, oldValue) {
scope.disabled = scope.$eval(newValue);
});
// Push pane to parent bsTabs controller
bsTabsCtrl.$push(scope);
// remove pane from tab controller when pane is destroyed
scope.$on('$destroy', function () {
bsTabsCtrl.$remove(scope);
});
function render () {
var index = bsTabsCtrl.$panes.indexOf(scope);
$animate[bsTabsCtrl.$isActive(scope, index) ? 'addClass' : 'removeClass'](element, bsTabsCtrl.$options.activeClass);
}
bsTabsCtrl.$activePaneChangeListeners.push(function () {
render();
});
render();
}
};
});