-.directive('accordionTransclude', function() {
+.directive('uibAccordionTransclude', function() {
return {
- require: '^accordionGroup',
- link: function(scope, element, attr, controller) {
- scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
- if ( heading ) {
- element.html('');
- element.append(heading);
+ require: '^uibAccordionGroup',
+ link: function(scope, element, attrs, controller) {
+ scope.$watch(function() { return controller[attrs.uibAccordionTransclude]; }, function(heading) {
+ if (heading) {
+ var elem = angular.element(element[0].querySelector('[uib-accordion-header]'));
+ elem.html('');
+ elem.append(heading);
}
});
}
@@ -299,17 +245,28 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
angular.module('ui.bootstrap.alert', [])
-.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) {
- $scope.closeable = 'close' in $attrs;
+.controller('UibAlertController', ['$scope', '$attrs', '$interpolate', '$timeout', function($scope, $attrs, $interpolate, $timeout) {
+ $scope.closeable = !!$attrs.close;
+
+ var dismissOnTimeout = angular.isDefined($attrs.dismissOnTimeout) ?
+ $interpolate($attrs.dismissOnTimeout)($scope.$parent) : null;
+
+ if (dismissOnTimeout) {
+ $timeout(function() {
+ $scope.close();
+ }, parseInt(dismissOnTimeout, 10));
+ }
}])
-.directive('alert', function () {
+.directive('uibAlert', function() {
return {
- restrict:'EA',
- controller:'AlertController',
- templateUrl:'template/alert/alert.html',
- transclude:true,
- replace:true,
+ controller: 'UibAlertController',
+ controllerAs: 'alert',
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/alert/alert.html';
+ },
+ transclude: true,
+ replace: true,
scope: {
type: '@',
close: '&'
@@ -317,62 +274,69 @@ angular.module('ui.bootstrap.alert', [])
};
});
-angular.module('ui.bootstrap.bindHtml', [])
-
- .directive('bindHtmlUnsafe', function () {
- return function (scope, element, attr) {
- element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe);
- scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) {
- element.html(value || '');
- });
- };
- });
angular.module('ui.bootstrap.buttons', [])
-.constant('buttonConfig', {
+.constant('uibButtonConfig', {
activeClass: 'active',
toggleEvent: 'click'
})
-.controller('ButtonsController', ['buttonConfig', function(buttonConfig) {
+.controller('UibButtonsController', ['uibButtonConfig', function(buttonConfig) {
this.activeClass = buttonConfig.activeClass || 'active';
this.toggleEvent = buttonConfig.toggleEvent || 'click';
}])
-.directive('btnRadio', function () {
+.directive('uibBtnRadio', ['$parse', function($parse) {
return {
- require: ['btnRadio', 'ngModel'],
- controller: 'ButtonsController',
- link: function (scope, element, attrs, ctrls) {
+ require: ['uibBtnRadio', 'ngModel'],
+ controller: 'UibButtonsController',
+ controllerAs: 'buttons',
+ link: function(scope, element, attrs, ctrls) {
var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+ var uncheckableExpr = $parse(attrs.uibUncheckable);
+
+ element.find('input').css({display: 'none'});
//model -> UI
- ngModelCtrl.$render = function () {
- element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio)));
+ ngModelCtrl.$render = function() {
+ element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.uibBtnRadio)));
};
//ui->model
- element.bind(buttonsCtrl.toggleEvent, function () {
+ element.on(buttonsCtrl.toggleEvent, function() {
+ if (attrs.disabled) {
+ return;
+ }
+
var isActive = element.hasClass(buttonsCtrl.activeClass);
if (!isActive || angular.isDefined(attrs.uncheckable)) {
- scope.$apply(function () {
- ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio));
+ scope.$apply(function() {
+ ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.uibBtnRadio));
ngModelCtrl.$render();
});
}
});
+
+ if (attrs.uibUncheckable) {
+ scope.$watch(uncheckableExpr, function(uncheckable) {
+ attrs.$set('uncheckable', uncheckable ? '' : undefined);
+ });
+ }
}
};
-})
+}])
-.directive('btnCheckbox', function () {
+.directive('uibBtnCheckbox', function() {
return {
- require: ['btnCheckbox', 'ngModel'],
- controller: 'ButtonsController',
- link: function (scope, element, attrs, ctrls) {
+ require: ['uibBtnCheckbox', 'ngModel'],
+ controller: 'UibButtonsController',
+ controllerAs: 'button',
+ link: function(scope, element, attrs, ctrls) {
var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+ element.find('input').css({display: 'none'});
+
function getTrueValue() {
return getCheckboxValue(attrs.btnCheckboxTrue, true);
}
@@ -381,19 +345,22 @@ angular.module('ui.bootstrap.buttons', [])
return getCheckboxValue(attrs.btnCheckboxFalse, false);
}
- function getCheckboxValue(attributeValue, defaultValue) {
- var val = scope.$eval(attributeValue);
- return angular.isDefined(val) ? val : defaultValue;
+ function getCheckboxValue(attribute, defaultValue) {
+ return angular.isDefined(attribute) ? scope.$eval(attribute) : defaultValue;
}
//model -> UI
- ngModelCtrl.$render = function () {
+ ngModelCtrl.$render = function() {
element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
};
//ui->model
- element.bind(buttonsCtrl.toggleEvent, function () {
- scope.$apply(function () {
+ element.on(buttonsCtrl.toggleEvent, function() {
+ if (attrs.disabled) {
+ return;
+ }
+
+ scope.$apply(function() {
ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
ngModelCtrl.$render();
});
@@ -402,141 +369,139 @@ angular.module('ui.bootstrap.buttons', [])
};
});
-/**
-* @ngdoc overview
-* @name ui.bootstrap.carousel
-*
-* @description
-* AngularJS version of an image carousel.
-*
-*/
-angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
-.controller('CarouselController', ['$scope', '$timeout', '$transition', function ($scope, $timeout, $transition) {
+angular.module('ui.bootstrap.carousel', [])
+
+.controller('UibCarouselController', ['$scope', '$element', '$interval', '$timeout', '$animate', function($scope, $element, $interval, $timeout, $animate) {
var self = this,
slides = self.slides = $scope.slides = [],
- currentIndex = -1,
- currentTimeout, isPlaying;
- self.currentSlide = null;
+ SLIDE_DIRECTION = 'uib-slideDirection',
+ currentIndex = $scope.active,
+ currentInterval, isPlaying, bufferedTransitions = [];
var destroyed = false;
+
+ self.addSlide = function(slide, element) {
+ slides.push({
+ slide: slide,
+ element: element
+ });
+ slides.sort(function(a, b) {
+ return +a.slide.index - +b.slide.index;
+ });
+ //if this is the first slide or the slide is set to active, select it
+ if (slide.index === $scope.active || slides.length === 1 && !angular.isNumber($scope.active)) {
+ if ($scope.$currentTransition) {
+ $scope.$currentTransition = null;
+ }
+
+ currentIndex = slide.index;
+ $scope.active = slide.index;
+ setActive(currentIndex);
+ self.select(slides[findSlideIndex(slide)]);
+ if (slides.length === 1) {
+ $scope.play();
+ }
+ }
+ };
+
+ self.getCurrentIndex = function() {
+ for (var i = 0; i < slides.length; i++) {
+ if (slides[i].slide.index === currentIndex) {
+ return i;
+ }
+ }
+ };
+
+ self.next = $scope.next = function() {
+ var newIndex = (self.getCurrentIndex() + 1) % slides.length;
+
+ if (newIndex === 0 && $scope.noWrap()) {
+ $scope.pause();
+ return;
+ }
+
+ return self.select(slides[newIndex], 'next');
+ };
+
+ self.prev = $scope.prev = function() {
+ var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1;
+
+ if ($scope.noWrap() && newIndex === slides.length - 1) {
+ $scope.pause();
+ return;
+ }
+
+ return self.select(slides[newIndex], 'prev');
+ };
+
+ self.removeSlide = function(slide) {
+ var index = findSlideIndex(slide);
+
+ var bufferedIndex = bufferedTransitions.indexOf(slides[index]);
+ if (bufferedIndex !== -1) {
+ bufferedTransitions.splice(bufferedIndex, 1);
+ }
+
+ //get the index of the slide inside the carousel
+ slides.splice(index, 1);
+ if (slides.length > 0 && currentIndex === index) {
+ if (index >= slides.length) {
+ currentIndex = slides.length - 1;
+ $scope.active = currentIndex;
+ setActive(currentIndex);
+ self.select(slides[slides.length - 1]);
+ } else {
+ currentIndex = index;
+ $scope.active = currentIndex;
+ setActive(currentIndex);
+ self.select(slides[index]);
+ }
+ } else if (currentIndex > index) {
+ currentIndex--;
+ $scope.active = currentIndex;
+ }
+
+ //clean the active value when no more slide
+ if (slides.length === 0) {
+ currentIndex = null;
+ $scope.active = null;
+ clearBufferedTransitions();
+ }
+ };
+
/* direction: "prev" or "next" */
self.select = $scope.select = function(nextSlide, direction) {
- var nextIndex = slides.indexOf(nextSlide);
+ var nextIndex = findSlideIndex(nextSlide.slide);
//Decide direction if it's not given
if (direction === undefined) {
- direction = nextIndex > currentIndex ? 'next' : 'prev';
+ direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev';
}
- if (nextSlide && nextSlide !== self.currentSlide) {
- if ($scope.$currentTransition) {
- $scope.$currentTransition.cancel();
- //Timeout so ng-class in template has time to fix classes for finished slide
- $timeout(goNext);
- } else {
- goNext();
- }
- }
- function goNext() {
- // Scope has been destroyed, stop here.
- if (destroyed) { return; }
- //If we have a slide to transition from and we have a transition type and we're allowed, go
- if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) {
- //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime
- nextSlide.$element.addClass(direction);
- var reflow = nextSlide.$element[0].offsetWidth; //force reflow
-
- //Set all other slides to stop doing their stuff for the new transition
- angular.forEach(slides, function(slide) {
- angular.extend(slide, {direction: '', entering: false, leaving: false, active: false});
- });
- angular.extend(nextSlide, {direction: direction, active: true, entering: true});
- angular.extend(self.currentSlide||{}, {direction: direction, leaving: true});
-
- $scope.$currentTransition = $transition(nextSlide.$element, {});
- //We have to create new pointers inside a closure since next & current will change
- (function(next,current) {
- $scope.$currentTransition.then(
- function(){ transitionDone(next, current); },
- function(){ transitionDone(next, current); }
- );
- }(nextSlide, self.currentSlide));
- } else {
- transitionDone(nextSlide, self.currentSlide);
- }
- self.currentSlide = nextSlide;
- currentIndex = nextIndex;
- //every time you change slides, reset the timer
- restartTimer();
- }
- function transitionDone(next, current) {
- angular.extend(next, {direction: '', active: true, leaving: false, entering: false});
- angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false});
- $scope.$currentTransition = null;
+ //Prevent this user-triggered transition from occurring if there is already one in progress
+ if (nextSlide.slide.index !== currentIndex &&
+ !$scope.$currentTransition) {
+ goNext(nextSlide.slide, nextIndex, direction);
+ } else if (nextSlide && nextSlide.slide.index !== currentIndex && $scope.$currentTransition) {
+ bufferedTransitions.push(slides[nextIndex]);
}
};
- $scope.$on('$destroy', function () {
- destroyed = true;
- });
/* Allow outside people to call indexOf on slides array */
- self.indexOfSlide = function(slide) {
- return slides.indexOf(slide);
- };
-
- $scope.next = function() {
- var newIndex = (currentIndex + 1) % slides.length;
-
- //Prevent this user-triggered transition from occurring if there is already one in progress
- if (!$scope.$currentTransition) {
- return self.select(slides[newIndex], 'next');
- }
- };
-
- $scope.prev = function() {
- var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1;
-
- //Prevent this user-triggered transition from occurring if there is already one in progress
- if (!$scope.$currentTransition) {
- return self.select(slides[newIndex], 'prev');
- }
+ $scope.indexOfSlide = function(slide) {
+ return +slide.slide.index;
};
$scope.isActive = function(slide) {
- return self.currentSlide === slide;
+ return $scope.active === slide.slide.index;
};
- $scope.$watch('interval', restartTimer);
- $scope.$on('$destroy', resetTimer);
-
- function restartTimer() {
- resetTimer();
- var interval = +$scope.interval;
- if (!isNaN(interval) && interval>=0) {
- currentTimeout = $timeout(timerFn, interval);
- }
- }
-
- function resetTimer() {
- if (currentTimeout) {
- $timeout.cancel(currentTimeout);
- currentTimeout = null;
- }
- }
-
- function timerFn() {
- if (isPlaying) {
- $scope.next();
- restartTimer();
- } else {
- $scope.pause();
- }
- }
-
- $scope.play = function() {
- if (!isPlaying) {
- isPlaying = true;
- restartTimer();
- }
+ $scope.isPrevDisabled = function() {
+ return $scope.active === 0 && $scope.noWrap();
};
+
+ $scope.isNextDisabled = function() {
+ return $scope.active === slides.length - 1 && $scope.noWrap();
+ };
+
$scope.pause = function() {
if (!$scope.noPause) {
isPlaying = false;
@@ -544,141 +509,175 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
}
};
- self.addSlide = function(slide, element) {
- slide.$element = element;
- slides.push(slide);
- //if this is the first slide or the slide is set to active, select it
- if(slides.length === 1 || slide.active) {
- self.select(slides[slides.length-1]);
- if (slides.length == 1) {
- $scope.play();
- }
- } else {
- slide.active = false;
+ $scope.play = function() {
+ if (!isPlaying) {
+ isPlaying = true;
+ restartTimer();
}
};
- self.removeSlide = function(slide) {
- //get the index of the slide inside the carousel
- var index = slides.indexOf(slide);
- slides.splice(index, 1);
- if (slides.length > 0 && slide.active) {
- if (index >= slides.length) {
- self.select(slides[index-1]);
- } else {
+ $scope.$on('$destroy', function() {
+ destroyed = true;
+ resetTimer();
+ });
+
+ $scope.$watch('noTransition', function(noTransition) {
+ $animate.enabled($element, !noTransition);
+ });
+
+ $scope.$watch('interval', restartTimer);
+
+ $scope.$watchCollection('slides', resetTransition);
+
+ $scope.$watch('active', function(index) {
+ if (angular.isNumber(index) && currentIndex !== index) {
+ for (var i = 0; i < slides.length; i++) {
+ if (slides[i].slide.index === index) {
+ index = i;
+ break;
+ }
+ }
+
+ var slide = slides[index];
+ if (slide) {
+ setActive(index);
self.select(slides[index]);
+ currentIndex = index;
}
- } else if (currentIndex > index) {
- currentIndex--;
}
- };
+ });
+ function clearBufferedTransitions() {
+ while (bufferedTransitions.length) {
+ bufferedTransitions.shift();
+ }
+ }
+
+ function getSlideByIndex(index) {
+ for (var i = 0, l = slides.length; i < l; ++i) {
+ if (slides[i].index === index) {
+ return slides[i];
+ }
+ }
+ }
+
+ function setActive(index) {
+ for (var i = 0; i < slides.length; i++) {
+ slides[i].slide.active = i === index;
+ }
+ }
+
+ function goNext(slide, index, direction) {
+ if (destroyed) {
+ return;
+ }
+
+ angular.extend(slide, {direction: direction});
+ angular.extend(slides[currentIndex].slide || {}, {direction: direction});
+ if ($animate.enabled($element) && !$scope.$currentTransition &&
+ slides[index].element && self.slides.length > 1) {
+ slides[index].element.data(SLIDE_DIRECTION, slide.direction);
+ var currentIdx = self.getCurrentIndex();
+
+ if (angular.isNumber(currentIdx) && slides[currentIdx].element) {
+ slides[currentIdx].element.data(SLIDE_DIRECTION, slide.direction);
+ }
+
+ $scope.$currentTransition = true;
+ $animate.on('addClass', slides[index].element, function(element, phase) {
+ if (phase === 'close') {
+ $scope.$currentTransition = null;
+ $animate.off('addClass', element);
+ if (bufferedTransitions.length) {
+ var nextSlide = bufferedTransitions.pop().slide;
+ var nextIndex = nextSlide.index;
+ var nextDirection = nextIndex > self.getCurrentIndex() ? 'next' : 'prev';
+ clearBufferedTransitions();
+
+ goNext(nextSlide, nextIndex, nextDirection);
+ }
+ }
+ });
+ }
+
+ $scope.active = slide.index;
+ currentIndex = slide.index;
+ setActive(index);
+
+ //every time you change slides, reset the timer
+ restartTimer();
+ }
+
+ function findSlideIndex(slide) {
+ for (var i = 0; i < slides.length; i++) {
+ if (slides[i].slide === slide) {
+ return i;
+ }
+ }
+ }
+
+ function resetTimer() {
+ if (currentInterval) {
+ $interval.cancel(currentInterval);
+ currentInterval = null;
+ }
+ }
+
+ function resetTransition(slides) {
+ if (!slides.length) {
+ $scope.$currentTransition = null;
+ clearBufferedTransitions();
+ }
+ }
+
+ function restartTimer() {
+ resetTimer();
+ var interval = +$scope.interval;
+ if (!isNaN(interval) && interval > 0) {
+ currentInterval = $interval(timerFn, interval);
+ }
+ }
+
+ function timerFn() {
+ var interval = +$scope.interval;
+ if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) {
+ $scope.next();
+ } else {
+ $scope.pause();
+ }
+ }
}])
-/**
- * @ngdoc directive
- * @name ui.bootstrap.carousel.directive:carousel
- * @restrict EA
- *
- * @description
- * Carousel is the outer container for a set of image 'slides' to showcase.
- *
- * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide.
- * @param {boolean=} noTransition Whether to disable transitions on the carousel.
- * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover).
- *
- * @example
-
-
-
-
-
-
-
Beautiful!
-
-
-
-
-
-
D'aww!
-
-
-
-
-
- .carousel-indicators {
- top: auto;
- bottom: 15px;
- }
-
-
- */
-.directive('carousel', [function() {
+.directive('uibCarousel', function() {
return {
- restrict: 'EA',
transclude: true,
replace: true,
- controller: 'CarouselController',
- require: 'carousel',
- templateUrl: 'template/carousel/carousel.html',
+ controller: 'UibCarouselController',
+ controllerAs: 'carousel',
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/carousel/carousel.html';
+ },
scope: {
+ active: '=',
interval: '=',
noTransition: '=',
- noPause: '='
+ noPause: '=',
+ noWrap: '&'
}
};
-}])
+})
-/**
- * @ngdoc directive
- * @name ui.bootstrap.carousel.directive:slide
- * @restrict EA
- *
- * @description
- * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element.
- *
- * @param {boolean=} active Model binding, whether or not this slide is currently active.
- *
- * @example
-
-
-
-
-
-
-
-
Slide {{$index}}
-
{{slide.text}}
-
-
-
- Interval, in milliseconds:
- Enter a negative number to stop the interval.
-
-
-
-function CarouselDemoCtrl($scope) {
- $scope.myInterval = 5000;
-}
-
-
- .carousel-indicators {
- top: auto;
- bottom: 15px;
- }
-
-
-*/
-
-.directive('slide', function() {
+.directive('uibSlide', function() {
return {
- require: '^carousel',
- restrict: 'EA',
+ require: '^uibCarousel',
transclude: true,
replace: true,
- templateUrl: 'template/carousel/slide.html',
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/carousel/slide.html';
+ },
scope: {
- active: '=?'
+ actual: '=?',
+ index: '=?'
},
link: function (scope, element, attrs, carouselCtrl) {
carouselCtrl.addSlide(scope, element);
@@ -686,85 +685,349 @@ function CarouselDemoCtrl($scope) {
scope.$on('$destroy', function() {
carouselCtrl.removeSlide(scope);
});
-
- scope.$watch('active', function(active) {
- if (active) {
- carouselCtrl.select(scope);
- }
- });
}
};
-});
+})
+
+.animation('.item', ['$animateCss',
+function($animateCss) {
+ var SLIDE_DIRECTION = 'uib-slideDirection';
+
+ function removeClass(element, className, callback) {
+ element.removeClass(className);
+ if (callback) {
+ callback();
+ }
+ }
+
+ return {
+ beforeAddClass: function(element, className, done) {
+ if (className === 'active') {
+ var stopped = false;
+ var direction = element.data(SLIDE_DIRECTION);
+ var directionClass = direction === 'next' ? 'left' : 'right';
+ var removeClassFn = removeClass.bind(this, element,
+ directionClass + ' ' + direction, done);
+ element.addClass(direction);
+
+ $animateCss(element, {addClass: directionClass})
+ .start()
+ .done(removeClassFn);
+
+ return function() {
+ stopped = true;
+ };
+ }
+ done();
+ },
+ beforeRemoveClass: function (element, className, done) {
+ if (className === 'active') {
+ var stopped = false;
+ var direction = element.data(SLIDE_DIRECTION);
+ var directionClass = direction === 'next' ? 'left' : 'right';
+ var removeClassFn = removeClass.bind(this, element, directionClass, done);
+
+ $animateCss(element, {addClass: directionClass})
+ .start()
+ .done(removeClassFn);
+
+ return function() {
+ stopped = true;
+ };
+ }
+ done();
+ }
+ };
+}]);
angular.module('ui.bootstrap.dateparser', [])
-.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) {
+.service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', function($log, $locale, dateFilter, orderByFilter) {
+ // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
+ var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
- this.parsers = {};
+ var localeId;
+ var formatCodeToRegex;
- var formatCodeToRegex = {
- 'yyyy': {
- regex: '\\d{4}',
- apply: function(value) { this.year = +value; }
- },
- 'yy': {
- regex: '\\d{2}',
- apply: function(value) { this.year = +value + 2000; }
- },
- 'y': {
- regex: '\\d{1,4}',
- apply: function(value) { this.year = +value; }
- },
- 'MMMM': {
- regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
- apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }
- },
- 'MMM': {
- regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
- apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }
- },
- 'MM': {
- regex: '0[1-9]|1[0-2]',
- apply: function(value) { this.month = value - 1; }
- },
- 'M': {
- regex: '[1-9]|1[0-2]',
- apply: function(value) { this.month = value - 1; }
- },
- 'dd': {
- regex: '[0-2][0-9]{1}|3[0-1]{1}',
- apply: function(value) { this.date = +value; }
- },
- 'd': {
- regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
- apply: function(value) { this.date = +value; }
- },
- 'EEEE': {
- regex: $locale.DATETIME_FORMATS.DAY.join('|')
- },
- 'EEE': {
- regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|')
- }
+ this.init = function() {
+ localeId = $locale.id;
+
+ this.parsers = {};
+ this.formatters = {};
+
+ formatCodeToRegex = [
+ {
+ key: 'yyyy',
+ regex: '\\d{4}',
+ apply: function(value) { this.year = +value; },
+ formatter: function(date) {
+ var _date = new Date();
+ _date.setFullYear(Math.abs(date.getFullYear()));
+ return dateFilter(_date, 'yyyy');
+ }
+ },
+ {
+ key: 'yy',
+ regex: '\\d{2}',
+ apply: function(value) { value = +value; this.year = value < 69 ? value + 2000 : value + 1900; },
+ formatter: function(date) {
+ var _date = new Date();
+ _date.setFullYear(Math.abs(date.getFullYear()));
+ return dateFilter(_date, 'yy');
+ }
+ },
+ {
+ key: 'y',
+ regex: '\\d{1,4}',
+ apply: function(value) { this.year = +value; },
+ formatter: function(date) {
+ var _date = new Date();
+ _date.setFullYear(Math.abs(date.getFullYear()));
+ return dateFilter(_date, 'y');
+ }
+ },
+ {
+ key: 'M!',
+ regex: '0?[1-9]|1[0-2]',
+ apply: function(value) { this.month = value - 1; },
+ formatter: function(date) {
+ var value = date.getMonth();
+ if (/^[0-9]$/.test(value)) {
+ return dateFilter(date, 'MM');
+ }
+
+ return dateFilter(date, 'M');
+ }
+ },
+ {
+ key: 'MMMM',
+ regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
+ apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); },
+ formatter: function(date) { return dateFilter(date, 'MMMM'); }
+ },
+ {
+ key: 'MMM',
+ regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
+ apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); },
+ formatter: function(date) { return dateFilter(date, 'MMM'); }
+ },
+ {
+ key: 'MM',
+ regex: '0[1-9]|1[0-2]',
+ apply: function(value) { this.month = value - 1; },
+ formatter: function(date) { return dateFilter(date, 'MM'); }
+ },
+ {
+ key: 'M',
+ regex: '[1-9]|1[0-2]',
+ apply: function(value) { this.month = value - 1; },
+ formatter: function(date) { return dateFilter(date, 'M'); }
+ },
+ {
+ key: 'd!',
+ regex: '[0-2]?[0-9]{1}|3[0-1]{1}',
+ apply: function(value) { this.date = +value; },
+ formatter: function(date) {
+ var value = date.getDate();
+ if (/^[1-9]$/.test(value)) {
+ return dateFilter(date, 'dd');
+ }
+
+ return dateFilter(date, 'd');
+ }
+ },
+ {
+ key: 'dd',
+ regex: '[0-2][0-9]{1}|3[0-1]{1}',
+ apply: function(value) { this.date = +value; },
+ formatter: function(date) { return dateFilter(date, 'dd'); }
+ },
+ {
+ key: 'd',
+ regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
+ apply: function(value) { this.date = +value; },
+ formatter: function(date) { return dateFilter(date, 'd'); }
+ },
+ {
+ key: 'EEEE',
+ regex: $locale.DATETIME_FORMATS.DAY.join('|'),
+ formatter: function(date) { return dateFilter(date, 'EEEE'); }
+ },
+ {
+ key: 'EEE',
+ regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|'),
+ formatter: function(date) { return dateFilter(date, 'EEE'); }
+ },
+ {
+ key: 'HH',
+ regex: '(?:0|1)[0-9]|2[0-3]',
+ apply: function(value) { this.hours = +value; },
+ formatter: function(date) { return dateFilter(date, 'HH'); }
+ },
+ {
+ key: 'hh',
+ regex: '0[0-9]|1[0-2]',
+ apply: function(value) { this.hours = +value; },
+ formatter: function(date) { return dateFilter(date, 'hh'); }
+ },
+ {
+ key: 'H',
+ regex: '1?[0-9]|2[0-3]',
+ apply: function(value) { this.hours = +value; },
+ formatter: function(date) { return dateFilter(date, 'H'); }
+ },
+ {
+ key: 'h',
+ regex: '[0-9]|1[0-2]',
+ apply: function(value) { this.hours = +value; },
+ formatter: function(date) { return dateFilter(date, 'h'); }
+ },
+ {
+ key: 'mm',
+ regex: '[0-5][0-9]',
+ apply: function(value) { this.minutes = +value; },
+ formatter: function(date) { return dateFilter(date, 'mm'); }
+ },
+ {
+ key: 'm',
+ regex: '[0-9]|[1-5][0-9]',
+ apply: function(value) { this.minutes = +value; },
+ formatter: function(date) { return dateFilter(date, 'm'); }
+ },
+ {
+ key: 'sss',
+ regex: '[0-9][0-9][0-9]',
+ apply: function(value) { this.milliseconds = +value; },
+ formatter: function(date) { return dateFilter(date, 'sss'); }
+ },
+ {
+ key: 'ss',
+ regex: '[0-5][0-9]',
+ apply: function(value) { this.seconds = +value; },
+ formatter: function(date) { return dateFilter(date, 'ss'); }
+ },
+ {
+ key: 's',
+ regex: '[0-9]|[1-5][0-9]',
+ apply: function(value) { this.seconds = +value; },
+ formatter: function(date) { return dateFilter(date, 's'); }
+ },
+ {
+ key: 'a',
+ regex: $locale.DATETIME_FORMATS.AMPMS.join('|'),
+ apply: function(value) {
+ if (this.hours === 12) {
+ this.hours = 0;
+ }
+
+ if (value === 'PM') {
+ this.hours += 12;
+ }
+ },
+ formatter: function(date) { return dateFilter(date, 'a'); }
+ },
+ {
+ key: 'Z',
+ regex: '[+-]\\d{4}',
+ apply: function(value) {
+ var matches = value.match(/([+-])(\d{2})(\d{2})/),
+ sign = matches[1],
+ hours = matches[2],
+ minutes = matches[3];
+ this.hours += toInt(sign + hours);
+ this.minutes += toInt(sign + minutes);
+ },
+ formatter: function(date) {
+ return dateFilter(date, 'Z');
+ }
+ },
+ {
+ key: 'ww',
+ regex: '[0-4][0-9]|5[0-3]',
+ formatter: function(date) { return dateFilter(date, 'ww'); }
+ },
+ {
+ key: 'w',
+ regex: '[0-9]|[1-4][0-9]|5[0-3]',
+ formatter: function(date) { return dateFilter(date, 'w'); }
+ },
+ {
+ key: 'GGGG',
+ regex: $locale.DATETIME_FORMATS.ERANAMES.join('|').replace(/\s/g, '\\s'),
+ formatter: function(date) { return dateFilter(date, 'GGGG'); }
+ },
+ {
+ key: 'GGG',
+ regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
+ formatter: function(date) { return dateFilter(date, 'GGG'); }
+ },
+ {
+ key: 'GG',
+ regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
+ formatter: function(date) { return dateFilter(date, 'GG'); }
+ },
+ {
+ key: 'G',
+ regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
+ formatter: function(date) { return dateFilter(date, 'G'); }
+ }
+ ];
};
- this.createParser = function(format) {
+ this.init();
+
+ function createParser(format, func) {
var map = [], regex = format.split('');
- angular.forEach(formatCodeToRegex, function(data, code) {
- var index = format.indexOf(code);
+ // check for literal values
+ var quoteIndex = format.indexOf('\'');
+ if (quoteIndex > -1) {
+ var inLiteral = false;
+ format = format.split('');
+ for (var i = quoteIndex; i < format.length; i++) {
+ if (inLiteral) {
+ if (format[i] === '\'') {
+ if (i + 1 < format.length && format[i+1] === '\'') { // escaped single quote
+ format[i+1] = '$';
+ regex[i+1] = '';
+ } else { // end of literal
+ regex[i] = '';
+ inLiteral = false;
+ }
+ }
+ format[i] = '$';
+ } else {
+ if (format[i] === '\'') { // start of literal
+ format[i] = '$';
+ regex[i] = '';
+ inLiteral = true;
+ }
+ }
+ }
+
+ format = format.join('');
+ }
+
+ angular.forEach(formatCodeToRegex, function(data) {
+ var index = format.indexOf(data.key);
if (index > -1) {
format = format.split('');
regex[index] = '(' + data.regex + ')';
format[index] = '$'; // Custom symbol to define consumed part of format
- for (var i = index + 1, n = index + code.length; i < n; i++) {
+ for (var i = index + 1, n = index + data.key.length; i < n; i++) {
regex[i] = '';
format[i] = '$';
}
format = format.join('');
- map.push({ index: index, apply: data.apply });
+ map.push({
+ index: index,
+ key: data.key,
+ apply: data[func],
+ matcher: data.regex
+ });
}
});
@@ -772,36 +1035,113 @@ angular.module('ui.bootstrap.dateparser', [])
regex: new RegExp('^' + regex.join('') + '$'),
map: orderByFilter(map, 'index')
};
- };
+ }
- this.parse = function(input, format) {
- if ( !angular.isString(input) ) {
- return input;
+ this.filter = function(date, format) {
+ if (!angular.isDate(date) || isNaN(date) || !format) {
+ return '';
}
format = $locale.DATETIME_FORMATS[format] || format;
- if ( !this.parsers[format] ) {
- this.parsers[format] = this.createParser(format);
+ if ($locale.id !== localeId) {
+ this.init();
+ }
+
+ if (!this.formatters[format]) {
+ this.formatters[format] = createParser(format, 'formatter');
+ }
+
+ var parser = this.formatters[format],
+ map = parser.map;
+
+ var _format = format;
+
+ return map.reduce(function(str, mapper, i) {
+ var match = _format.match(new RegExp('(.*)' + mapper.key));
+ if (match && angular.isString(match[1])) {
+ str += match[1];
+ _format = _format.replace(match[1] + mapper.key, '');
+ }
+
+ var endStr = i === map.length - 1 ? _format : '';
+
+ if (mapper.apply) {
+ return str + mapper.apply.call(null, date) + endStr;
+ }
+
+ return str + endStr;
+ }, '');
+ };
+
+ this.parse = function(input, format, baseDate) {
+ if (!angular.isString(input) || !format) {
+ return input;
+ }
+
+ format = $locale.DATETIME_FORMATS[format] || format;
+ format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&');
+
+ if ($locale.id !== localeId) {
+ this.init();
+ }
+
+ if (!this.parsers[format]) {
+ this.parsers[format] = createParser(format, 'apply');
}
var parser = this.parsers[format],
regex = parser.regex,
map = parser.map,
- results = input.match(regex);
+ results = input.match(regex),
+ tzOffset = false;
+ if (results && results.length) {
+ var fields, dt;
+ if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
+ fields = {
+ year: baseDate.getFullYear(),
+ month: baseDate.getMonth(),
+ date: baseDate.getDate(),
+ hours: baseDate.getHours(),
+ minutes: baseDate.getMinutes(),
+ seconds: baseDate.getSeconds(),
+ milliseconds: baseDate.getMilliseconds()
+ };
+ } else {
+ if (baseDate) {
+ $log.warn('dateparser:', 'baseDate is not a valid date');
+ }
+ fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
+ }
- if ( results && results.length ) {
- var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt;
+ for (var i = 1, n = results.length; i < n; i++) {
+ var mapper = map[i - 1];
+ if (mapper.matcher === 'Z') {
+ tzOffset = true;
+ }
- for( var i = 1, n = results.length; i < n; i++ ) {
- var mapper = map[i-1];
- if ( mapper.apply ) {
+ if (mapper.apply) {
mapper.apply.call(fields, results[i]);
}
}
- if ( isValid(fields.year, fields.month, fields.date) ) {
- dt = new Date( fields.year, fields.month, fields.date, fields.hours);
+ var datesetter = tzOffset ? Date.prototype.setUTCFullYear :
+ Date.prototype.setFullYear;
+ var timesetter = tzOffset ? Date.prototype.setUTCHours :
+ Date.prototype.setHours;
+
+ if (isValid(fields.year, fields.month, fields.date)) {
+ if (angular.isDate(baseDate) && !isNaN(baseDate.getTime()) && !tzOffset) {
+ dt = new Date(baseDate);
+ datesetter.call(dt, fields.year, fields.month, fields.date);
+ timesetter.call(dt, fields.hours, fields.minutes,
+ fields.seconds, fields.milliseconds);
+ } else {
+ dt = new Date(0);
+ datesetter.call(dt, fields.year, fields.month, fields.date);
+ timesetter.call(dt, fields.hours || 0, fields.minutes || 0,
+ fields.seconds || 0, fields.milliseconds || 0);
+ }
}
return dt;
@@ -811,218 +1151,302 @@ angular.module('ui.bootstrap.dateparser', [])
// Check if date is valid for specific month (and year for February).
// Month: 0 = Jan, 1 = Feb, etc
function isValid(year, month, date) {
- if ( month === 1 && date > 28) {
- return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
+ if (date < 1) {
+ return false;
}
- if ( month === 3 || month === 5 || month === 8 || month === 10) {
- return date < 31;
+ if (month === 1 && date > 28) {
+ return date === 29 && (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0);
+ }
+
+ if (month === 3 || month === 5 || month === 8 || month === 10) {
+ return date < 31;
}
return true;
}
+
+ function toInt(str) {
+ return parseInt(str, 10);
+ }
+
+ this.toTimezone = toTimezone;
+ this.fromTimezone = fromTimezone;
+ this.timezoneToOffset = timezoneToOffset;
+ this.addDateMinutes = addDateMinutes;
+ this.convertTimezoneToLocal = convertTimezoneToLocal;
+
+ function toTimezone(date, timezone) {
+ return date && timezone ? convertTimezoneToLocal(date, timezone) : date;
+ }
+
+ function fromTimezone(date, timezone) {
+ return date && timezone ? convertTimezoneToLocal(date, timezone, true) : date;
+ }
+
+ //https://github.com/angular/angular.js/blob/4daafd3dbe6a80d578f5a31df1bb99c77559543e/src/Angular.js#L1207
+ function timezoneToOffset(timezone, fallback) {
+ var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
+ return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
+ }
+
+ function addDateMinutes(date, minutes) {
+ date = new Date(date.getTime());
+ date.setMinutes(date.getMinutes() + minutes);
+ return date;
+ }
+
+ function convertTimezoneToLocal(date, timezone, reverse) {
+ reverse = reverse ? -1 : 1;
+ var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset());
+ return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset()));
+ }
}]);
-angular.module('ui.bootstrap.position', [])
+// Avoiding use of ng-class as it creates a lot of watchers when a class is to be applied to
+// at most one element.
+angular.module('ui.bootstrap.isClass', [])
+.directive('uibIsClass', [
+ '$animate',
+function ($animate) {
+ // 11111111 22222222
+ var ON_REGEXP = /^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/;
+ // 11111111 22222222
+ var IS_REGEXP = /^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/;
-/**
- * A set of utility methods that can be use to retrieve position of DOM elements.
- * It is meant to be used where we need to absolute-position DOM elements in
- * relation to other, existing elements (this is the case for tooltips, popovers,
- * typeahead suggestions etc.).
- */
- .factory('$position', ['$document', '$window', function ($document, $window) {
+ var dataPerTracked = {};
- function getStyle(el, cssprop) {
- if (el.currentStyle) { //IE
- return el.currentStyle[cssprop];
- } else if ($window.getComputedStyle) {
- return $window.getComputedStyle(el)[cssprop];
+ return {
+ restrict: 'A',
+ compile: function(tElement, tAttrs) {
+ var linkedScopes = [];
+ var instances = [];
+ var expToData = {};
+ var lastActivated = null;
+ var onExpMatches = tAttrs.uibIsClass.match(ON_REGEXP);
+ var onExp = onExpMatches[2];
+ var expsStr = onExpMatches[1];
+ var exps = expsStr.split(',');
+
+ return linkFn;
+
+ function linkFn(scope, element, attrs) {
+ linkedScopes.push(scope);
+ instances.push({
+ scope: scope,
+ element: element
+ });
+
+ exps.forEach(function(exp, k) {
+ addForExp(exp, scope);
+ });
+
+ scope.$on('$destroy', removeScope);
}
- // finally try and get inline style
- return el.style[cssprop];
- }
- /**
- * Checks if a given element is statically positioned
- * @param element - raw DOM element
- */
- function isStaticPositioned(element) {
- return (getStyle(element, 'position') || 'static' ) === 'static';
- }
-
- /**
- * returns the closest, non-statically positioned parentOffset of a given element
- * @param element
- */
- var parentOffsetEl = function (element) {
- var docDomEl = $document[0];
- var offsetParent = element.offsetParent || docDomEl;
- while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
- offsetParent = offsetParent.offsetParent;
- }
- return offsetParent || docDomEl;
- };
-
- return {
- /**
- * Provides read-only equivalent of jQuery's position function:
- * http://api.jquery.com/position/
- */
- position: function (element) {
- var elBCR = this.offset(element);
- var offsetParentBCR = { top: 0, left: 0 };
- var offsetParentEl = parentOffsetEl(element[0]);
- if (offsetParentEl != $document[0]) {
- offsetParentBCR = this.offset(angular.element(offsetParentEl));
- offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
- offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
+ function addForExp(exp, scope) {
+ var matches = exp.match(IS_REGEXP);
+ var clazz = scope.$eval(matches[1]);
+ var compareWithExp = matches[2];
+ var data = expToData[exp];
+ if (!data) {
+ var watchFn = function(compareWithVal) {
+ var newActivated = null;
+ instances.some(function(instance) {
+ var thisVal = instance.scope.$eval(onExp);
+ if (thisVal === compareWithVal) {
+ newActivated = instance;
+ return true;
+ }
+ });
+ if (data.lastActivated !== newActivated) {
+ if (data.lastActivated) {
+ $animate.removeClass(data.lastActivated.element, clazz);
+ }
+ if (newActivated) {
+ $animate.addClass(newActivated.element, clazz);
+ }
+ data.lastActivated = newActivated;
+ }
+ };
+ expToData[exp] = data = {
+ lastActivated: null,
+ scope: scope,
+ watchFn: watchFn,
+ compareWithExp: compareWithExp,
+ watcher: scope.$watch(compareWithExp, watchFn)
+ };
}
-
- var boundingClientRect = element[0].getBoundingClientRect();
- return {
- width: boundingClientRect.width || element.prop('offsetWidth'),
- height: boundingClientRect.height || element.prop('offsetHeight'),
- top: elBCR.top - offsetParentBCR.top,
- left: elBCR.left - offsetParentBCR.left
- };
- },
-
- /**
- * Provides read-only equivalent of jQuery's offset function:
- * http://api.jquery.com/offset/
- */
- offset: function (element) {
- var boundingClientRect = element[0].getBoundingClientRect();
- return {
- width: boundingClientRect.width || element.prop('offsetWidth'),
- height: boundingClientRect.height || element.prop('offsetHeight'),
- top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
- left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
- };
- },
-
- /**
- * Provides coordinates for the targetEl in relation to hostEl
- */
- positionElements: function (hostEl, targetEl, positionStr, appendToBody) {
-
- var positionStrParts = positionStr.split('-');
- var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center';
-
- var hostElPos,
- targetElWidth,
- targetElHeight,
- targetElPos;
-
- hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl);
-
- targetElWidth = targetEl.prop('offsetWidth');
- targetElHeight = targetEl.prop('offsetHeight');
-
- var shiftWidth = {
- center: function () {
- return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2;
- },
- left: function () {
- return hostElPos.left;
- },
- right: function () {
- return hostElPos.left + hostElPos.width;
- }
- };
-
- var shiftHeight = {
- center: function () {
- return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
- },
- top: function () {
- return hostElPos.top;
- },
- bottom: function () {
- return hostElPos.top + hostElPos.height;
- }
- };
-
- switch (pos0) {
- case 'right':
- targetElPos = {
- top: shiftHeight[pos1](),
- left: shiftWidth[pos0]()
- };
- break;
- case 'left':
- targetElPos = {
- top: shiftHeight[pos1](),
- left: hostElPos.left - targetElWidth
- };
- break;
- case 'bottom':
- targetElPos = {
- top: shiftHeight[pos0](),
- left: shiftWidth[pos1]()
- };
- break;
- default:
- targetElPos = {
- top: hostElPos.top - targetElHeight,
- left: shiftWidth[pos1]()
- };
- break;
- }
-
- return targetElPos;
+ data.watchFn(scope.$eval(compareWithExp));
}
- };
- }]);
-angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])
+ function removeScope(e) {
+ var removedScope = e.targetScope;
+ var index = linkedScopes.indexOf(removedScope);
+ linkedScopes.splice(index, 1);
+ instances.splice(index, 1);
+ if (linkedScopes.length) {
+ var newWatchScope = linkedScopes[0];
+ angular.forEach(expToData, function(data) {
+ if (data.scope === removedScope) {
+ data.watcher = newWatchScope.$watch(data.compareWithExp, data.watchFn);
+ data.scope = newWatchScope;
+ }
+ });
+ } else {
+ expToData = {};
+ }
+ }
+ }
+ };
+}]);
+angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.isClass'])
-.constant('datepickerConfig', {
+.value('$datepickerSuppressError', false)
+
+.value('$datepickerLiteralWarning', true)
+
+.constant('uibDatepickerConfig', {
+ datepickerMode: 'day',
formatDay: 'dd',
formatMonth: 'MMMM',
formatYear: 'yyyy',
formatDayHeader: 'EEE',
formatDayTitle: 'MMMM yyyy',
formatMonthTitle: 'yyyy',
- datepickerMode: 'day',
- minMode: 'day',
+ maxDate: null,
maxMode: 'year',
- showWeeks: true,
- startingDay: 0,
- yearRange: 20,
minDate: null,
- maxDate: null
+ minMode: 'day',
+ ngModelOptions: {},
+ shortcutPropagation: false,
+ showWeeks: true,
+ yearColumns: 5,
+ yearRows: 4
})
-.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) {
+.controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerLiteralWarning', '$datepickerSuppressError', 'uibDateParser',
+ function($scope, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerLiteralWarning, $datepickerSuppressError, dateParser) {
var self = this,
- ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
+ ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl;
+ ngModelOptions = {},
+ watchListeners = [],
+ optionsUsed = !!$attrs.datepickerOptions;
+
+ if (!$scope.datepickerOptions) {
+ $scope.datepickerOptions = {};
+ }
// Modes chain
this.modes = ['day', 'month', 'year'];
- // Configuration attributes
- angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
- 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) {
- self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
- });
+ [
+ 'customClass',
+ 'dateDisabled',
+ 'datepickerMode',
+ 'formatDay',
+ 'formatDayHeader',
+ 'formatDayTitle',
+ 'formatMonth',
+ 'formatMonthTitle',
+ 'formatYear',
+ 'maxDate',
+ 'maxMode',
+ 'minDate',
+ 'minMode',
+ 'showWeeks',
+ 'shortcutPropagation',
+ 'startingDay',
+ 'yearColumns',
+ 'yearRows'
+ ].forEach(function(key) {
+ switch (key) {
+ case 'customClass':
+ case 'dateDisabled':
+ $scope[key] = $scope.datepickerOptions[key] || angular.noop;
+ break;
+ case 'datepickerMode':
+ $scope.datepickerMode = angular.isDefined($scope.datepickerOptions.datepickerMode) ?
+ $scope.datepickerOptions.datepickerMode : datepickerConfig.datepickerMode;
+ break;
+ case 'formatDay':
+ case 'formatDayHeader':
+ case 'formatDayTitle':
+ case 'formatMonth':
+ case 'formatMonthTitle':
+ case 'formatYear':
+ self[key] = angular.isDefined($scope.datepickerOptions[key]) ?
+ $interpolate($scope.datepickerOptions[key])($scope.$parent) :
+ datepickerConfig[key];
+ break;
+ case 'showWeeks':
+ case 'shortcutPropagation':
+ case 'yearColumns':
+ case 'yearRows':
+ self[key] = angular.isDefined($scope.datepickerOptions[key]) ?
+ $scope.datepickerOptions[key] : datepickerConfig[key];
+ break;
+ case 'startingDay':
+ if (angular.isDefined($scope.datepickerOptions.startingDay)) {
+ self.startingDay = $scope.datepickerOptions.startingDay;
+ } else if (angular.isNumber(datepickerConfig.startingDay)) {
+ self.startingDay = datepickerConfig.startingDay;
+ } else {
+ self.startingDay = ($locale.DATETIME_FORMATS.FIRSTDAYOFWEEK + 8) % 7;
+ }
- // Watchable attributes
- angular.forEach(['minDate', 'maxDate'], function( key ) {
- if ( $attrs[key] ) {
- $scope.$parent.$watch($parse($attrs[key]), function(value) {
- self[key] = value ? new Date(value) : null;
- self.refreshView();
- });
- } else {
- self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
+ break;
+ case 'maxDate':
+ case 'minDate':
+ $scope.$watch('datepickerOptions.' + key, function(value) {
+ if (value) {
+ if (angular.isDate(value)) {
+ self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone);
+ } else {
+ if ($datepickerLiteralWarning) {
+ $log.warn('Literal date support has been deprecated, please switch to date object usage');
+ }
+
+ self[key] = new Date(dateFilter(value, 'medium'));
+ }
+ } else {
+ self[key] = datepickerConfig[key] ?
+ dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) :
+ null;
+ }
+
+ self.refreshView();
+ });
+
+ break;
+ case 'maxMode':
+ case 'minMode':
+ if ($scope.datepickerOptions[key]) {
+ $scope.$watch(function() { return $scope.datepickerOptions[key]; }, function(value) {
+ self[key] = $scope[key] = angular.isDefined(value) ? value : datepickerOptions[key];
+ if (key === 'minMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) < self.modes.indexOf(self[key]) ||
+ key === 'maxMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) > self.modes.indexOf(self[key])) {
+ $scope.datepickerMode = self[key];
+ $scope.datepickerOptions.datepickerMode = self[key];
+ }
+ });
+ } else {
+ self[key] = $scope[key] = datepickerConfig[key] || null;
+ }
+
+ break;
}
});
- $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
$scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
- this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date();
+
+ $scope.disabled = angular.isDefined($attrs.disabled) || false;
+ if (angular.isDefined($attrs.ngDisabled)) {
+ watchListeners.push($scope.$parent.$watch($attrs.ngDisabled, function(disabled) {
+ $scope.disabled = disabled;
+ self.refreshView();
+ }));
+ }
$scope.isActive = function(dateObject) {
if (self.compare(dateObject.date, self.activeDate) === 0) {
@@ -1032,8 +1456,24 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
return false;
};
- this.init = function( ngModelCtrl_ ) {
+ this.init = function(ngModelCtrl_) {
ngModelCtrl = ngModelCtrl_;
+ ngModelOptions = ngModelCtrl_.$options || datepickerConfig.ngModelOptions;
+ if ($scope.datepickerOptions.initDate) {
+ self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.timezone) || new Date();
+ $scope.$watch('datepickerOptions.initDate', function(initDate) {
+ if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
+ self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone);
+ self.refreshView();
+ }
+ });
+ } else {
+ self.activeDate = new Date();
+ }
+
+ this.activeDate = ngModelCtrl.$modelValue ?
+ dateParser.fromTimezone(new Date(ngModelCtrl.$modelValue), ngModelOptions.timezone) :
+ dateParser.fromTimezone(new Date(), ngModelOptions.timezone);
ngModelCtrl.$render = function() {
self.render();
@@ -1041,42 +1481,71 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
};
this.render = function() {
- if ( ngModelCtrl.$modelValue ) {
- var date = new Date( ngModelCtrl.$modelValue ),
+ if (ngModelCtrl.$viewValue) {
+ var date = new Date(ngModelCtrl.$viewValue),
isValid = !isNaN(date);
- if ( isValid ) {
- this.activeDate = date;
- } else {
- $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
+ if (isValid) {
+ this.activeDate = dateParser.fromTimezone(date, ngModelOptions.timezone);
+ } else if (!$datepickerSuppressError) {
+ $log.error('Datepicker directive: "ng-model" value must be a Date object');
}
- ngModelCtrl.$setValidity('date', isValid);
}
this.refreshView();
};
this.refreshView = function() {
- if ( this.element ) {
+ if (this.element) {
+ $scope.selectedDt = null;
this._refreshView();
+ if ($scope.activeDt) {
+ $scope.activeDateId = $scope.activeDt.uid;
+ }
- var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null;
- ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date)));
+ var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
+ date = dateParser.fromTimezone(date, ngModelOptions.timezone);
+ ngModelCtrl.$setValidity('dateDisabled', !date ||
+ this.element && !this.isDisabled(date));
}
};
this.createDateObject = function(date, format) {
- var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null;
- return {
+ var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
+ model = dateParser.fromTimezone(model, ngModelOptions.timezone);
+ var today = new Date();
+ today = dateParser.fromTimezone(today, ngModelOptions.timezone);
+ var time = this.compare(date, today);
+ var dt = {
date: date,
- label: dateFilter(date, format),
+ label: dateParser.filter(date, format),
selected: model && this.compare(date, model) === 0,
disabled: this.isDisabled(date),
- current: this.compare(date, new Date()) === 0
+ past: time < 0,
+ current: time === 0,
+ future: time > 0,
+ customClass: this.customClass(date) || null
};
+
+ if (model && this.compare(date, model) === 0) {
+ $scope.selectedDt = dt;
+ }
+
+ if (self.activeDate && this.compare(dt.date, self.activeDate) === 0) {
+ $scope.activeDt = dt;
+ }
+
+ return dt;
};
- this.isDisabled = function( date ) {
- return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode})));
+ this.isDisabled = function(date) {
+ return $scope.disabled ||
+ this.minDate && this.compare(date, this.minDate) < 0 ||
+ this.maxDate && this.compare(date, this.maxDate) > 0 ||
+ $scope.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode});
+ };
+
+ this.customClass = function(date) {
+ return $scope.customClass({date: date, mode: $scope.datepickerMode});
};
// Split array into smaller arrays
@@ -1088,579 +1557,1563 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
return arrays;
};
- $scope.select = function( date ) {
- if ( $scope.datepickerMode === self.minMode ) {
- var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0);
- dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() );
- ngModelCtrl.$setViewValue( dt );
+ $scope.select = function(date) {
+ if ($scope.datepickerMode === self.minMode) {
+ var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.timezone) : new Date(0, 0, 0, 0, 0, 0, 0);
+ dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
+ dt = dateParser.toTimezone(dt, ngModelOptions.timezone);
+ ngModelCtrl.$setViewValue(dt);
ngModelCtrl.$render();
} else {
self.activeDate = date;
- $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ];
+ setMode(self.modes[self.modes.indexOf($scope.datepickerMode) - 1]);
+
+ $scope.$emit('uib:datepicker.mode');
}
+
+ $scope.$broadcast('uib:datepicker.focus');
};
- $scope.move = function( direction ) {
+ $scope.move = function(direction) {
var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
month = self.activeDate.getMonth() + direction * (self.step.months || 0);
self.activeDate.setFullYear(year, month, 1);
self.refreshView();
};
- $scope.toggleMode = function( direction ) {
+ $scope.toggleMode = function(direction) {
direction = direction || 1;
- if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) {
+ if ($scope.datepickerMode === self.maxMode && direction === 1 ||
+ $scope.datepickerMode === self.minMode && direction === -1) {
return;
}
- $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ];
+ setMode(self.modes[self.modes.indexOf($scope.datepickerMode) + direction]);
+
+ $scope.$emit('uib:datepicker.mode');
};
// Key event mapper
- $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' };
+ $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' };
var focusElement = function() {
- $timeout(function() {
- self.element[0].focus();
- }, 0 , false);
+ self.element[0].focus();
};
// Listen for focus requests from popup directive
- $scope.$on('datepicker.focus', focusElement);
+ $scope.$on('uib:datepicker.focus', focusElement);
- $scope.keydown = function( evt ) {
+ $scope.keydown = function(evt) {
var key = $scope.keys[evt.which];
- if ( !key || evt.shiftKey || evt.altKey ) {
+ if (!key || evt.shiftKey || evt.altKey || $scope.disabled) {
return;
}
evt.preventDefault();
- evt.stopPropagation();
+ if (!self.shortcutPropagation) {
+ evt.stopPropagation();
+ }
if (key === 'enter' || key === 'space') {
- if ( self.isDisabled(self.activeDate)) {
+ if (self.isDisabled(self.activeDate)) {
return; // do nothing
}
$scope.select(self.activeDate);
- focusElement();
} else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
$scope.toggleMode(key === 'up' ? 1 : -1);
- focusElement();
} else {
self.handleKeyDown(key, evt);
self.refreshView();
}
};
+
+ $scope.$on('$destroy', function() {
+ //Clear all watch listeners on destroy
+ while (watchListeners.length) {
+ watchListeners.shift()();
+ }
+ });
+
+ function setMode(mode) {
+ $scope.datepickerMode = mode;
+ $scope.datepickerOptions.datepickerMode = mode;
+ }
}])
-.directive( 'datepicker', function () {
+.controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
+ var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+
+ this.step = { months: 1 };
+ this.element = $element;
+ function getDaysInMonth(year, month) {
+ return month === 1 && year % 4 === 0 &&
+ (year % 100 !== 0 || year % 400 === 0) ? 29 : DAYS_IN_MONTH[month];
+ }
+
+ this.init = function(ctrl) {
+ angular.extend(ctrl, this);
+ scope.showWeeks = ctrl.showWeeks;
+ ctrl.refreshView();
+ };
+
+ this.getDates = function(startDate, n) {
+ var dates = new Array(n), current = new Date(startDate), i = 0, date;
+ while (i < n) {
+ date = new Date(current);
+ dates[i++] = date;
+ current.setDate(current.getDate() + 1);
+ }
+ return dates;
+ };
+
+ this._refreshView = function() {
+ var year = this.activeDate.getFullYear(),
+ month = this.activeDate.getMonth(),
+ firstDayOfMonth = new Date(this.activeDate);
+
+ firstDayOfMonth.setFullYear(year, month, 1);
+
+ var difference = this.startingDay - firstDayOfMonth.getDay(),
+ numDisplayedFromPreviousMonth = difference > 0 ?
+ 7 - difference : - difference,
+ firstDate = new Date(firstDayOfMonth);
+
+ if (numDisplayedFromPreviousMonth > 0) {
+ firstDate.setDate(-numDisplayedFromPreviousMonth + 1);
+ }
+
+ // 42 is the number of days on a six-week calendar
+ var days = this.getDates(firstDate, 42);
+ for (var i = 0; i < 42; i ++) {
+ days[i] = angular.extend(this.createDateObject(days[i], this.formatDay), {
+ secondary: days[i].getMonth() !== month,
+ uid: scope.uniqueId + '-' + i
+ });
+ }
+
+ scope.labels = new Array(7);
+ for (var j = 0; j < 7; j++) {
+ scope.labels[j] = {
+ abbr: dateFilter(days[j].date, this.formatDayHeader),
+ full: dateFilter(days[j].date, 'EEEE')
+ };
+ }
+
+ scope.title = dateFilter(this.activeDate, this.formatDayTitle);
+ scope.rows = this.split(days, 7);
+
+ if (scope.showWeeks) {
+ scope.weekNumbers = [];
+ var thursdayIndex = (4 + 7 - this.startingDay) % 7,
+ numWeeks = scope.rows.length;
+ for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
+ scope.weekNumbers.push(
+ getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date));
+ }
+ }
+ };
+
+ this.compare = function(date1, date2) {
+ var _date1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate());
+ var _date2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
+ _date1.setFullYear(date1.getFullYear());
+ _date2.setFullYear(date2.getFullYear());
+ return _date1 - _date2;
+ };
+
+ function getISO8601WeekNumber(date) {
+ var checkDate = new Date(date);
+ checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
+ var time = checkDate.getTime();
+ checkDate.setMonth(0); // Compare with Jan 1
+ checkDate.setDate(1);
+ return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
+ }
+
+ this.handleKeyDown = function(key, evt) {
+ var date = this.activeDate.getDate();
+
+ if (key === 'left') {
+ date = date - 1;
+ } else if (key === 'up') {
+ date = date - 7;
+ } else if (key === 'right') {
+ date = date + 1;
+ } else if (key === 'down') {
+ date = date + 7;
+ } else if (key === 'pageup' || key === 'pagedown') {
+ var month = this.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1);
+ this.activeDate.setMonth(month, 1);
+ date = Math.min(getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()), date);
+ } else if (key === 'home') {
+ date = 1;
+ } else if (key === 'end') {
+ date = getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth());
+ }
+ this.activeDate.setDate(date);
+ };
+}])
+
+.controller('UibMonthpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
+ this.step = { years: 1 };
+ this.element = $element;
+
+ this.init = function(ctrl) {
+ angular.extend(ctrl, this);
+ ctrl.refreshView();
+ };
+
+ this._refreshView = function() {
+ var months = new Array(12),
+ year = this.activeDate.getFullYear(),
+ date;
+
+ for (var i = 0; i < 12; i++) {
+ date = new Date(this.activeDate);
+ date.setFullYear(year, i, 1);
+ months[i] = angular.extend(this.createDateObject(date, this.formatMonth), {
+ uid: scope.uniqueId + '-' + i
+ });
+ }
+
+ scope.title = dateFilter(this.activeDate, this.formatMonthTitle);
+ scope.rows = this.split(months, 3);
+ };
+
+ this.compare = function(date1, date2) {
+ var _date1 = new Date(date1.getFullYear(), date1.getMonth());
+ var _date2 = new Date(date2.getFullYear(), date2.getMonth());
+ _date1.setFullYear(date1.getFullYear());
+ _date2.setFullYear(date2.getFullYear());
+ return _date1 - _date2;
+ };
+
+ this.handleKeyDown = function(key, evt) {
+ var date = this.activeDate.getMonth();
+
+ if (key === 'left') {
+ date = date - 1;
+ } else if (key === 'up') {
+ date = date - 3;
+ } else if (key === 'right') {
+ date = date + 1;
+ } else if (key === 'down') {
+ date = date + 3;
+ } else if (key === 'pageup' || key === 'pagedown') {
+ var year = this.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1);
+ this.activeDate.setFullYear(year);
+ } else if (key === 'home') {
+ date = 0;
+ } else if (key === 'end') {
+ date = 11;
+ }
+ this.activeDate.setMonth(date);
+ };
+}])
+
+.controller('UibYearpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
+ var columns, range;
+ this.element = $element;
+
+ function getStartingYear(year) {
+ return parseInt((year - 1) / range, 10) * range + 1;
+ }
+
+ this.yearpickerInit = function() {
+ columns = this.yearColumns;
+ range = this.yearRows * columns;
+ this.step = { years: range };
+ };
+
+ this._refreshView = function() {
+ var years = new Array(range), date;
+
+ for (var i = 0, start = getStartingYear(this.activeDate.getFullYear()); i < range; i++) {
+ date = new Date(this.activeDate);
+ date.setFullYear(start + i, 0, 1);
+ years[i] = angular.extend(this.createDateObject(date, this.formatYear), {
+ uid: scope.uniqueId + '-' + i
+ });
+ }
+
+ scope.title = [years[0].label, years[range - 1].label].join(' - ');
+ scope.rows = this.split(years, columns);
+ scope.columns = columns;
+ };
+
+ this.compare = function(date1, date2) {
+ return date1.getFullYear() - date2.getFullYear();
+ };
+
+ this.handleKeyDown = function(key, evt) {
+ var date = this.activeDate.getFullYear();
+
+ if (key === 'left') {
+ date = date - 1;
+ } else if (key === 'up') {
+ date = date - columns;
+ } else if (key === 'right') {
+ date = date + 1;
+ } else if (key === 'down') {
+ date = date + columns;
+ } else if (key === 'pageup' || key === 'pagedown') {
+ date += (key === 'pageup' ? - 1 : 1) * range;
+ } else if (key === 'home') {
+ date = getStartingYear(this.activeDate.getFullYear());
+ } else if (key === 'end') {
+ date = getStartingYear(this.activeDate.getFullYear()) + range - 1;
+ }
+ this.activeDate.setFullYear(date);
+ };
+}])
+
+.directive('uibDatepicker', function() {
return {
- restrict: 'EA',
replace: true,
- templateUrl: 'template/datepicker/datepicker.html',
- scope: {
- datepickerMode: '=?',
- dateDisabled: '&'
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/datepicker/datepicker.html';
},
- require: ['datepicker', '?^ngModel'],
- controller: 'DatepickerController',
+ scope: {
+ datepickerOptions: '=?'
+ },
+ require: ['uibDatepicker', '^ngModel'],
+ controller: 'UibDatepickerController',
+ controllerAs: 'datepicker',
link: function(scope, element, attrs, ctrls) {
var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
- if ( ngModelCtrl ) {
- datepickerCtrl.init( ngModelCtrl );
- }
+ datepickerCtrl.init(ngModelCtrl);
}
};
})
-.directive('daypicker', ['dateFilter', function (dateFilter) {
+.directive('uibDaypicker', function() {
return {
- restrict: 'EA',
replace: true,
- templateUrl: 'template/datepicker/day.html',
- require: '^datepicker',
- link: function(scope, element, attrs, ctrl) {
- scope.showWeeks = ctrl.showWeeks;
-
- ctrl.step = { months: 1 };
- ctrl.element = element;
-
- var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
- function getDaysInMonth( year, month ) {
- return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month];
- }
-
- function getDates(startDate, n) {
- var dates = new Array(n), current = new Date(startDate), i = 0;
- current.setHours(12); // Prevent repeated dates because of timezone bug
- while ( i < n ) {
- dates[i++] = new Date(current);
- current.setDate( current.getDate() + 1 );
- }
- return dates;
- }
-
- ctrl._refreshView = function() {
- var year = ctrl.activeDate.getFullYear(),
- month = ctrl.activeDate.getMonth(),
- firstDayOfMonth = new Date(year, month, 1),
- difference = ctrl.startingDay - firstDayOfMonth.getDay(),
- numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference,
- firstDate = new Date(firstDayOfMonth);
-
- if ( numDisplayedFromPreviousMonth > 0 ) {
- firstDate.setDate( - numDisplayedFromPreviousMonth + 1 );
- }
-
- // 42 is the number of days on a six-month calendar
- var days = getDates(firstDate, 42);
- for (var i = 0; i < 42; i ++) {
- days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), {
- secondary: days[i].getMonth() !== month,
- uid: scope.uniqueId + '-' + i
- });
- }
-
- scope.labels = new Array(7);
- for (var j = 0; j < 7; j++) {
- scope.labels[j] = {
- abbr: dateFilter(days[j].date, ctrl.formatDayHeader),
- full: dateFilter(days[j].date, 'EEEE')
- };
- }
-
- scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle);
- scope.rows = ctrl.split(days, 7);
-
- if ( scope.showWeeks ) {
- scope.weekNumbers = [];
- var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ),
- numWeeks = scope.rows.length;
- while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {}
- }
- };
-
- ctrl.compare = function(date1, date2) {
- return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) );
- };
-
- function getISO8601WeekNumber(date) {
- var checkDate = new Date(date);
- checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
- var time = checkDate.getTime();
- checkDate.setMonth(0); // Compare with Jan 1
- checkDate.setDate(1);
- return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
- }
-
- ctrl.handleKeyDown = function( key, evt ) {
- var date = ctrl.activeDate.getDate();
-
- if (key === 'left') {
- date = date - 1; // up
- } else if (key === 'up') {
- date = date - 7; // down
- } else if (key === 'right') {
- date = date + 1; // down
- } else if (key === 'down') {
- date = date + 7;
- } else if (key === 'pageup' || key === 'pagedown') {
- var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1);
- ctrl.activeDate.setMonth(month, 1);
- date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date);
- } else if (key === 'home') {
- date = 1;
- } else if (key === 'end') {
- date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth());
- }
- ctrl.activeDate.setDate(date);
- };
-
- ctrl.refreshView();
- }
- };
-}])
-
-.directive('monthpicker', ['dateFilter', function (dateFilter) {
- return {
- restrict: 'EA',
- replace: true,
- templateUrl: 'template/datepicker/month.html',
- require: '^datepicker',
- link: function(scope, element, attrs, ctrl) {
- ctrl.step = { years: 1 };
- ctrl.element = element;
-
- ctrl._refreshView = function() {
- var months = new Array(12),
- year = ctrl.activeDate.getFullYear();
-
- for ( var i = 0; i < 12; i++ ) {
- months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), {
- uid: scope.uniqueId + '-' + i
- });
- }
-
- scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle);
- scope.rows = ctrl.split(months, 3);
- };
-
- ctrl.compare = function(date1, date2) {
- return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() );
- };
-
- ctrl.handleKeyDown = function( key, evt ) {
- var date = ctrl.activeDate.getMonth();
-
- if (key === 'left') {
- date = date - 1; // up
- } else if (key === 'up') {
- date = date - 3; // down
- } else if (key === 'right') {
- date = date + 1; // down
- } else if (key === 'down') {
- date = date + 3;
- } else if (key === 'pageup' || key === 'pagedown') {
- var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1);
- ctrl.activeDate.setFullYear(year);
- } else if (key === 'home') {
- date = 0;
- } else if (key === 'end') {
- date = 11;
- }
- ctrl.activeDate.setMonth(date);
- };
-
- ctrl.refreshView();
- }
- };
-}])
-
-.directive('yearpicker', ['dateFilter', function (dateFilter) {
- return {
- restrict: 'EA',
- replace: true,
- templateUrl: 'template/datepicker/year.html',
- require: '^datepicker',
- link: function(scope, element, attrs, ctrl) {
- var range = ctrl.yearRange;
-
- ctrl.step = { years: range };
- ctrl.element = element;
-
- function getStartingYear( year ) {
- return parseInt((year - 1) / range, 10) * range + 1;
- }
-
- ctrl._refreshView = function() {
- var years = new Array(range);
-
- for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) {
- years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), {
- uid: scope.uniqueId + '-' + i
- });
- }
-
- scope.title = [years[0].label, years[range - 1].label].join(' - ');
- scope.rows = ctrl.split(years, 5);
- };
-
- ctrl.compare = function(date1, date2) {
- return date1.getFullYear() - date2.getFullYear();
- };
-
- ctrl.handleKeyDown = function( key, evt ) {
- var date = ctrl.activeDate.getFullYear();
-
- if (key === 'left') {
- date = date - 1; // up
- } else if (key === 'up') {
- date = date - 5; // down
- } else if (key === 'right') {
- date = date + 1; // down
- } else if (key === 'down') {
- date = date + 5;
- } else if (key === 'pageup' || key === 'pagedown') {
- date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years;
- } else if (key === 'home') {
- date = getStartingYear( ctrl.activeDate.getFullYear() );
- } else if (key === 'end') {
- date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1;
- }
- ctrl.activeDate.setFullYear(date);
- };
-
- ctrl.refreshView();
- }
- };
-}])
-
-.constant('datepickerPopupConfig', {
- datepickerPopup: 'yyyy-MM-dd',
- currentText: 'Today',
- clearText: 'Clear',
- closeText: 'Done',
- closeOnDateSelection: true,
- appendToBody: false,
- showButtonBar: true
-})
-
-.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig',
-function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) {
- return {
- restrict: 'EA',
- require: 'ngModel',
- scope: {
- isOpen: '=?',
- currentText: '@',
- clearText: '@',
- closeText: '@',
- dateDisabled: '&'
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/datepicker/day.html';
},
- link: function(scope, element, attrs, ngModel) {
- var dateFormat,
- closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection,
- appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
+ require: ['^uibDatepicker', 'uibDaypicker'],
+ controller: 'UibDaypickerController',
+ link: function(scope, element, attrs, ctrls) {
+ var datepickerCtrl = ctrls[0],
+ daypickerCtrl = ctrls[1];
- scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;
-
- scope.getText = function( key ) {
- return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
- };
-
- attrs.$observe('datepickerPopup', function(value) {
- dateFormat = value || datepickerPopupConfig.datepickerPopup;
- ngModel.$render();
- });
-
- // popup element used to display calendar
- var popupEl = angular.element('
');
- popupEl.attr({
- 'ng-model': 'date',
- 'ng-change': 'dateSelection()'
- });
-
- function cameltoDash( string ){
- return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
- }
-
- // datepicker element
- var datepickerEl = angular.element(popupEl.children()[0]);
- if ( attrs.datepickerOptions ) {
- angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) {
- datepickerEl.attr( cameltoDash(option), value );
- });
- }
-
- angular.forEach(['minDate', 'maxDate'], function( key ) {
- if ( attrs[key] ) {
- scope.$parent.$watch($parse(attrs[key]), function(value){
- scope[key] = value;
- });
- datepickerEl.attr(cameltoDash(key), key);
- }
- });
- if (attrs.dateDisabled) {
- datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })');
- }
-
- function parseDate(viewValue) {
- if (!viewValue) {
- ngModel.$setValidity('date', true);
- return null;
- } else if (angular.isDate(viewValue) && !isNaN(viewValue)) {
- ngModel.$setValidity('date', true);
- return viewValue;
- } else if (angular.isString(viewValue)) {
- var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue);
- if (isNaN(date)) {
- ngModel.$setValidity('date', false);
- return undefined;
- } else {
- ngModel.$setValidity('date', true);
- return date;
- }
- } else {
- ngModel.$setValidity('date', false);
- return undefined;
- }
- }
- ngModel.$parsers.unshift(parseDate);
-
- // Inner change
- scope.dateSelection = function(dt) {
- if (angular.isDefined(dt)) {
- scope.date = dt;
- }
- ngModel.$setViewValue(scope.date);
- ngModel.$render();
-
- if ( closeOnDateSelection ) {
- scope.isOpen = false;
- element[0].focus();
- }
- };
-
- element.bind('input change keyup', function() {
- scope.$apply(function() {
- scope.date = ngModel.$modelValue;
- });
- });
-
- // Outter change
- ngModel.$render = function() {
- var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : '';
- element.val(date);
- scope.date = parseDate( ngModel.$modelValue );
- };
-
- var documentClickBind = function(event) {
- if (scope.isOpen && event.target !== element[0]) {
- scope.$apply(function() {
- scope.isOpen = false;
- });
- }
- };
-
- var keydown = function(evt, noApply) {
- scope.keydown(evt);
- };
- element.bind('keydown', keydown);
-
- scope.keydown = function(evt) {
- if (evt.which === 27) {
- evt.preventDefault();
- evt.stopPropagation();
- scope.close();
- } else if (evt.which === 40 && !scope.isOpen) {
- scope.isOpen = true;
- }
- };
-
- scope.$watch('isOpen', function(value) {
- if (value) {
- scope.$broadcast('datepicker.focus');
- scope.position = appendToBody ? $position.offset(element) : $position.position(element);
- scope.position.top = scope.position.top + element.prop('offsetHeight');
-
- $document.bind('click', documentClickBind);
- } else {
- $document.unbind('click', documentClickBind);
- }
- });
-
- scope.select = function( date ) {
- if (date === 'today') {
- var today = new Date();
- if (angular.isDate(ngModel.$modelValue)) {
- date = new Date(ngModel.$modelValue);
- date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
- } else {
- date = new Date(today.setHours(0, 0, 0, 0));
- }
- }
- scope.dateSelection( date );
- };
-
- scope.close = function() {
- scope.isOpen = false;
- element[0].focus();
- };
-
- var $popup = $compile(popupEl)(scope);
- if ( appendToBody ) {
- $document.find('body').append($popup);
- } else {
- element.after($popup);
- }
-
- scope.$on('$destroy', function() {
- $popup.remove();
- element.unbind('keydown', keydown);
- $document.unbind('click', documentClickBind);
- });
+ daypickerCtrl.init(datepickerCtrl);
}
};
-}])
+})
-.directive('datepickerPopupWrap', function() {
+.directive('uibMonthpicker', function() {
return {
- restrict:'EA',
replace: true,
- transclude: true,
- templateUrl: 'template/datepicker/popup.html',
- link:function (scope, element, attrs) {
- element.bind('click', function(event) {
- event.preventDefault();
- event.stopPropagation();
- });
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/datepicker/month.html';
+ },
+ require: ['^uibDatepicker', 'uibMonthpicker'],
+ controller: 'UibMonthpickerController',
+ link: function(scope, element, attrs, ctrls) {
+ var datepickerCtrl = ctrls[0],
+ monthpickerCtrl = ctrls[1];
+
+ monthpickerCtrl.init(datepickerCtrl);
+ }
+ };
+})
+
+.directive('uibYearpicker', function() {
+ return {
+ replace: true,
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/datepicker/year.html';
+ },
+ require: ['^uibDatepicker', 'uibYearpicker'],
+ controller: 'UibYearpickerController',
+ link: function(scope, element, attrs, ctrls) {
+ var ctrl = ctrls[0];
+ angular.extend(ctrl, ctrls[1]);
+ ctrl.yearpickerInit();
+
+ ctrl.refreshView();
}
};
});
-angular.module('ui.bootstrap.dropdown', [])
+angular.module('ui.bootstrap.position', [])
-.constant('dropdownConfig', {
+/**
+ * A set of utility methods for working with the DOM.
+ * It is meant to be used where we need to absolute-position elements in
+ * relation to another element (this is the case for tooltips, popovers,
+ * typeahead suggestions etc.).
+ */
+ .factory('$uibPosition', ['$document', '$window', function($document, $window) {
+ /**
+ * Used by scrollbarWidth() function to cache scrollbar's width.
+ * Do not access this variable directly, use scrollbarWidth() instead.
+ */
+ var SCROLLBAR_WIDTH;
+ /**
+ * scrollbar on body and html element in IE and Edge overlay
+ * content and should be considered 0 width.
+ */
+ var BODY_SCROLLBAR_WIDTH;
+ var OVERFLOW_REGEX = {
+ normal: /(auto|scroll)/,
+ hidden: /(auto|scroll|hidden)/
+ };
+ var PLACEMENT_REGEX = {
+ auto: /\s?auto?\s?/i,
+ primary: /^(top|bottom|left|right)$/,
+ secondary: /^(top|bottom|left|right|center)$/,
+ vertical: /^(top|bottom)$/
+ };
+ var BODY_REGEX = /(HTML|BODY)/;
+
+ return {
+
+ /**
+ * Provides a raw DOM element from a jQuery/jQLite element.
+ *
+ * @param {element} elem - The element to convert.
+ *
+ * @returns {element} A HTML element.
+ */
+ getRawNode: function(elem) {
+ return elem.nodeName ? elem : elem[0] || elem;
+ },
+
+ /**
+ * Provides a parsed number for a style property. Strips
+ * units and casts invalid numbers to 0.
+ *
+ * @param {string} value - The style value to parse.
+ *
+ * @returns {number} A valid number.
+ */
+ parseStyle: function(value) {
+ value = parseFloat(value);
+ return isFinite(value) ? value : 0;
+ },
+
+ /**
+ * Provides the closest positioned ancestor.
+ *
+ * @param {element} element - The element to get the offest parent for.
+ *
+ * @returns {element} The closest positioned ancestor.
+ */
+ offsetParent: function(elem) {
+ elem = this.getRawNode(elem);
+
+ var offsetParent = elem.offsetParent || $document[0].documentElement;
+
+ function isStaticPositioned(el) {
+ return ($window.getComputedStyle(el).position || 'static') === 'static';
+ }
+
+ while (offsetParent && offsetParent !== $document[0].documentElement && isStaticPositioned(offsetParent)) {
+ offsetParent = offsetParent.offsetParent;
+ }
+
+ return offsetParent || $document[0].documentElement;
+ },
+
+ /**
+ * Provides the scrollbar width, concept from TWBS measureScrollbar()
+ * function in https://github.com/twbs/bootstrap/blob/master/js/modal.js
+ * In IE and Edge, scollbar on body and html element overlay and should
+ * return a width of 0.
+ *
+ * @returns {number} The width of the browser scollbar.
+ */
+ scrollbarWidth: function(isBody) {
+ if (isBody) {
+ if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) {
+ var bodyElem = $document.find('body');
+ bodyElem.addClass('uib-position-body-scrollbar-measure');
+ BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth;
+ BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0;
+ bodyElem.removeClass('uib-position-body-scrollbar-measure');
+ }
+ return BODY_SCROLLBAR_WIDTH;
+ }
+
+ if (angular.isUndefined(SCROLLBAR_WIDTH)) {
+ var scrollElem = angular.element('');
+ $document.find('body').append(scrollElem);
+ SCROLLBAR_WIDTH = scrollElem[0].offsetWidth - scrollElem[0].clientWidth;
+ SCROLLBAR_WIDTH = isFinite(SCROLLBAR_WIDTH) ? SCROLLBAR_WIDTH : 0;
+ scrollElem.remove();
+ }
+
+ return SCROLLBAR_WIDTH;
+ },
+
+ /**
+ * Provides the padding required on an element to replace the scrollbar.
+ *
+ * @returns {object} An object with the following properties:
+ *
+ *
**scrollbarWidth**: the width of the scrollbar
+ *
**widthOverflow**: whether the the width is overflowing
+ *
**right**: the amount of right padding on the element needed to replace the scrollbar
+ *
**rightOriginal**: the amount of right padding currently on the element
+ *
**heightOverflow**: whether the the height is overflowing
+ *
**bottom**: the amount of bottom padding on the element needed to replace the scrollbar
+ *
**bottomOriginal**: the amount of bottom padding currently on the element
+ *
+ */
+ scrollbarPadding: function(elem) {
+ elem = this.getRawNode(elem);
+
+ var elemStyle = $window.getComputedStyle(elem);
+ var paddingRight = this.parseStyle(elemStyle.paddingRight);
+ var paddingBottom = this.parseStyle(elemStyle.paddingBottom);
+ var scrollParent = this.scrollParent(elem, false, true);
+ var scrollbarWidth = this.scrollbarWidth(scrollParent, BODY_REGEX.test(scrollParent.tagName));
+
+ return {
+ scrollbarWidth: scrollbarWidth,
+ widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth,
+ right: paddingRight + scrollbarWidth,
+ originalRight: paddingRight,
+ heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight,
+ bottom: paddingBottom + scrollbarWidth,
+ originalBottom: paddingBottom
+ };
+ },
+
+ /**
+ * Checks to see if the element is scrollable.
+ *
+ * @param {element} elem - The element to check.
+ * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered,
+ * default is false.
+ *
+ * @returns {boolean} Whether the element is scrollable.
+ */
+ isScrollable: function(elem, includeHidden) {
+ elem = this.getRawNode(elem);
+
+ var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal;
+ var elemStyle = $window.getComputedStyle(elem);
+ return overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX);
+ },
+
+ /**
+ * Provides the closest scrollable ancestor.
+ * A port of the jQuery UI scrollParent method:
+ * https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js
+ *
+ * @param {element} elem - The element to find the scroll parent of.
+ * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered,
+ * default is false.
+ * @param {boolean=} [includeSelf=false] - Should the element being passed be
+ * included in the scrollable llokup.
+ *
+ * @returns {element} A HTML element.
+ */
+ scrollParent: function(elem, includeHidden, includeSelf) {
+ elem = this.getRawNode(elem);
+
+ var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal;
+ var documentEl = $document[0].documentElement;
+ var elemStyle = $window.getComputedStyle(elem);
+ if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) {
+ return elem;
+ }
+ var excludeStatic = elemStyle.position === 'absolute';
+ var scrollParent = elem.parentElement || documentEl;
+
+ if (scrollParent === documentEl || elemStyle.position === 'fixed') {
+ return documentEl;
+ }
+
+ while (scrollParent.parentElement && scrollParent !== documentEl) {
+ var spStyle = $window.getComputedStyle(scrollParent);
+ if (excludeStatic && spStyle.position !== 'static') {
+ excludeStatic = false;
+ }
+
+ if (!excludeStatic && overflowRegex.test(spStyle.overflow + spStyle.overflowY + spStyle.overflowX)) {
+ break;
+ }
+ scrollParent = scrollParent.parentElement;
+ }
+
+ return scrollParent;
+ },
+
+ /**
+ * Provides read-only equivalent of jQuery's position function:
+ * http://api.jquery.com/position/ - distance to closest positioned
+ * ancestor. Does not account for margins by default like jQuery position.
+ *
+ * @param {element} elem - The element to caclulate the position on.
+ * @param {boolean=} [includeMargins=false] - Should margins be accounted
+ * for, default is false.
+ *
+ * @returns {object} An object with the following properties:
+ *
+ *
**width**: the width of the element
+ *
**height**: the height of the element
+ *
**top**: distance to top edge of offset parent
+ *
**left**: distance to left edge of offset parent
+ *
+ */
+ position: function(elem, includeMagins) {
+ elem = this.getRawNode(elem);
+
+ var elemOffset = this.offset(elem);
+ if (includeMagins) {
+ var elemStyle = $window.getComputedStyle(elem);
+ elemOffset.top -= this.parseStyle(elemStyle.marginTop);
+ elemOffset.left -= this.parseStyle(elemStyle.marginLeft);
+ }
+ var parent = this.offsetParent(elem);
+ var parentOffset = {top: 0, left: 0};
+
+ if (parent !== $document[0].documentElement) {
+ parentOffset = this.offset(parent);
+ parentOffset.top += parent.clientTop - parent.scrollTop;
+ parentOffset.left += parent.clientLeft - parent.scrollLeft;
+ }
+
+ return {
+ width: Math.round(angular.isNumber(elemOffset.width) ? elemOffset.width : elem.offsetWidth),
+ height: Math.round(angular.isNumber(elemOffset.height) ? elemOffset.height : elem.offsetHeight),
+ top: Math.round(elemOffset.top - parentOffset.top),
+ left: Math.round(elemOffset.left - parentOffset.left)
+ };
+ },
+
+ /**
+ * Provides read-only equivalent of jQuery's offset function:
+ * http://api.jquery.com/offset/ - distance to viewport. Does
+ * not account for borders, margins, or padding on the body
+ * element.
+ *
+ * @param {element} elem - The element to calculate the offset on.
+ *
+ * @returns {object} An object with the following properties:
+ *
+ *
**width**: the width of the element
+ *
**height**: the height of the element
+ *
**top**: distance to top edge of viewport
+ *
**right**: distance to bottom edge of viewport
+ *
+ */
+ offset: function(elem) {
+ elem = this.getRawNode(elem);
+
+ var elemBCR = elem.getBoundingClientRect();
+ return {
+ width: Math.round(angular.isNumber(elemBCR.width) ? elemBCR.width : elem.offsetWidth),
+ height: Math.round(angular.isNumber(elemBCR.height) ? elemBCR.height : elem.offsetHeight),
+ top: Math.round(elemBCR.top + ($window.pageYOffset || $document[0].documentElement.scrollTop)),
+ left: Math.round(elemBCR.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft))
+ };
+ },
+
+ /**
+ * Provides offset distance to the closest scrollable ancestor
+ * or viewport. Accounts for border and scrollbar width.
+ *
+ * Right and bottom dimensions represent the distance to the
+ * respective edge of the viewport element. If the element
+ * edge extends beyond the viewport, a negative value will be
+ * reported.
+ *
+ * @param {element} elem - The element to get the viewport offset for.
+ * @param {boolean=} [useDocument=false] - Should the viewport be the document element instead
+ * of the first scrollable element, default is false.
+ * @param {boolean=} [includePadding=true] - Should the padding on the offset parent element
+ * be accounted for, default is true.
+ *
+ * @returns {object} An object with the following properties:
+ *
+ *
**top**: distance to the top content edge of viewport element
+ *
**bottom**: distance to the bottom content edge of viewport element
+ *
**left**: distance to the left content edge of viewport element
+ *
**right**: distance to the right content edge of viewport element
top: element on top, horizontally centered on host element.
+ *
top-left: element on top, left edge aligned with host element left edge.
+ *
top-right: element on top, lerightft edge aligned with host element right edge.
+ *
bottom: element on bottom, horizontally centered on host element.
+ *
bottom-left: element on bottom, left edge aligned with host element left edge.
+ *
bottom-right: element on bottom, right edge aligned with host element right edge.
+ *
left: element on left, vertically centered on host element.
+ *
left-top: element on left, top edge aligned with host element top edge.
+ *
left-bottom: element on left, bottom edge aligned with host element bottom edge.
+ *
right: element on right, vertically centered on host element.
+ *
right-top: element on right, top edge aligned with host element top edge.
+ *
right-bottom: element on right, bottom edge aligned with host element bottom edge.
+ *
+ * A placement string with an 'auto' indicator is expected to be
+ * space separated from the placement, i.e: 'auto bottom-left' If
+ * the primary and secondary placement values do not match 'top,
+ * bottom, left, right' then 'top' will be the primary placement and
+ * 'center' will be the secondary placement. If 'auto' is passed, true
+ * will be returned as the 3rd value of the array.
+ *
+ * @param {string} placement - The placement string to parse.
+ *
+ * @returns {array} An array with the following values
+ *
+ *
**[0]**: The primary placement.
+ *
**[1]**: The secondary placement.
+ *
**[2]**: If auto is passed: true, else undefined.
+ *
+ */
+ parsePlacement: function(placement) {
+ var autoPlace = PLACEMENT_REGEX.auto.test(placement);
+ if (autoPlace) {
+ placement = placement.replace(PLACEMENT_REGEX.auto, '');
+ }
+
+ placement = placement.split('-');
+
+ placement[0] = placement[0] || 'top';
+ if (!PLACEMENT_REGEX.primary.test(placement[0])) {
+ placement[0] = 'top';
+ }
+
+ placement[1] = placement[1] || 'center';
+ if (!PLACEMENT_REGEX.secondary.test(placement[1])) {
+ placement[1] = 'center';
+ }
+
+ if (autoPlace) {
+ placement[2] = true;
+ } else {
+ placement[2] = false;
+ }
+
+ return placement;
+ },
+
+ /**
+ * Provides coordinates for an element to be positioned relative to
+ * another element. Passing 'auto' as part of the placement parameter
+ * will enable smart placement - where the element fits. i.e:
+ * 'auto left-top' will check to see if there is enough space to the left
+ * of the hostElem to fit the targetElem, if not place right (same for secondary
+ * top placement). Available space is calculated using the viewportOffset
+ * function.
+ *
+ * @param {element} hostElem - The element to position against.
+ * @param {element} targetElem - The element to position.
+ * @param {string=} [placement=top] - The placement for the targetElem,
+ * default is 'top'. 'center' is assumed as secondary placement for
+ * 'top', 'left', 'right', and 'bottom' placements. Available placements are:
+ *
+ *
top
+ *
top-right
+ *
top-left
+ *
bottom
+ *
bottom-left
+ *
bottom-right
+ *
left
+ *
left-top
+ *
left-bottom
+ *
right
+ *
right-top
+ *
right-bottom
+ *
+ * @param {boolean=} [appendToBody=false] - Should the top and left values returned
+ * be calculated from the body element, default is false.
+ *
+ * @returns {object} An object with the following properties:
+ *
');
+ self.dropdownMenu.replaceWith(newEl);
+ self.dropdownMenu = newEl;
+ }
+
+ uibDropdownService.close(scope, $element);
+ self.selectedOption = null;
}
- setIsOpen($scope, isOpen);
- if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
- toggleInvoker($scope, { open: !!isOpen });
+ if (angular.isFunction(setIsOpen)) {
+ setIsOpen($scope, isOpen);
}
});
-
- $scope.$on('$locationChangeSuccess', function() {
- scope.isOpen = false;
- });
-
- $scope.$on('$destroy', function() {
- scope.$destroy();
- });
}])
-.directive('dropdown', function() {
+.directive('uibDropdown', function() {
return {
- restrict: 'CA',
- controller: 'DropdownController',
+ controller: 'UibDropdownController',
link: function(scope, element, attrs, dropdownCtrl) {
- dropdownCtrl.init( element );
+ dropdownCtrl.init();
}
};
})
-.directive('dropdownToggle', function() {
+.directive('uibDropdownMenu', function() {
return {
- restrict: 'CA',
- require: '?^dropdown',
+ restrict: 'A',
+ require: '?^uibDropdown',
link: function(scope, element, attrs, dropdownCtrl) {
- if ( !dropdownCtrl ) {
+ if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) {
return;
}
+ element.addClass('dropdown-menu');
+
+ var tplUrl = attrs.templateUrl;
+ if (tplUrl) {
+ dropdownCtrl.dropdownMenuTemplateUrl = tplUrl;
+ }
+
+ if (!dropdownCtrl.dropdownMenu) {
+ dropdownCtrl.dropdownMenu = element;
+ }
+ }
+ };
+})
+
+.directive('uibDropdownToggle', function() {
+ return {
+ require: '?^uibDropdown',
+ link: function(scope, element, attrs, dropdownCtrl) {
+ if (!dropdownCtrl) {
+ return;
+ }
+
+ element.addClass('dropdown-toggle');
+
dropdownCtrl.toggleElement = element;
var toggleDropdown = function(event) {
event.preventDefault();
- if ( !element.hasClass('disabled') && !attrs.disabled ) {
+ if (!element.hasClass('disabled') && !attrs.disabled) {
scope.$apply(function() {
dropdownCtrl.toggle();
});
@@ -1745,7 +3355,7 @@ angular.module('ui.bootstrap.dropdown', [])
// WAI-ARIA
element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
- scope.$watch(dropdownCtrl.isOpen, function( isOpen ) {
+ scope.$watch(dropdownCtrl.isOpen, function(isOpen) {
element.attr('aria-expanded', !!isOpen);
});
@@ -1756,27 +3366,26 @@ angular.module('ui.bootstrap.dropdown', [])
};
});
-angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
-
+angular.module('ui.bootstrap.stackedMap', [])
/**
* A helper, internal data structure that acts as a map but also allows getting / removing
* elements in the LIFO order
*/
- .factory('$$stackedMap', function () {
+ .factory('$$stackedMap', function() {
return {
- createNew: function () {
+ createNew: function() {
var stack = [];
return {
- add: function (key, value) {
+ add: function(key, value) {
stack.push({
key: key,
value: value
});
},
- get: function (key) {
+ get: function(key) {
for (var i = 0; i < stack.length; i++) {
- if (key == stack[i].key) {
+ if (key === stack[i].key) {
return stack[i];
}
}
@@ -1788,93 +3397,302 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
}
return keys;
},
- top: function () {
+ top: function() {
return stack[stack.length - 1];
},
- remove: function (key) {
+ remove: function(key) {
var idx = -1;
for (var i = 0; i < stack.length; i++) {
- if (key == stack[i].key) {
+ if (key === stack[i].key) {
idx = i;
break;
}
}
return stack.splice(idx, 1)[0];
},
- removeTop: function () {
+ removeTop: function() {
return stack.splice(stack.length - 1, 1)[0];
},
- length: function () {
+ length: function() {
return stack.length;
}
};
}
};
+ });
+angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.position'])
+/**
+ * A helper, internal data structure that stores all references attached to key
+ */
+ .factory('$$multiMap', function() {
+ return {
+ createNew: function() {
+ var map = {};
+
+ return {
+ entries: function() {
+ return Object.keys(map).map(function(key) {
+ return {
+ key: key,
+ value: map[key]
+ };
+ });
+ },
+ get: function(key) {
+ return map[key];
+ },
+ hasKey: function(key) {
+ return !!map[key];
+ },
+ keys: function() {
+ return Object.keys(map);
+ },
+ put: function(key, value) {
+ if (!map[key]) {
+ map[key] = [];
+ }
+
+ map[key].push(value);
+ },
+ remove: function(key, value) {
+ var values = map[key];
+
+ if (!values) {
+ return;
+ }
+
+ var idx = values.indexOf(value);
+
+ if (idx !== -1) {
+ values.splice(idx, 1);
+ }
+
+ if (!values.length) {
+ delete map[key];
+ }
+ }
+ };
+ }
+ };
+ })
+
+/**
+ * Pluggable resolve mechanism for the modal resolve resolution
+ * Supports UI Router's $resolve service
+ */
+ .provider('$uibResolve', function() {
+ var resolve = this;
+ this.resolver = null;
+
+ this.setResolver = function(resolver) {
+ this.resolver = resolver;
+ };
+
+ this.$get = ['$injector', '$q', function($injector, $q) {
+ var resolver = resolve.resolver ? $injector.get(resolve.resolver) : null;
+ return {
+ resolve: function(invocables, locals, parent, self) {
+ if (resolver) {
+ return resolver.resolve(invocables, locals, parent, self);
+ }
+
+ var promises = [];
+
+ angular.forEach(invocables, function(value) {
+ if (angular.isFunction(value) || angular.isArray(value)) {
+ promises.push($q.resolve($injector.invoke(value)));
+ } else if (angular.isString(value)) {
+ promises.push($q.resolve($injector.get(value)));
+ } else {
+ promises.push($q.resolve(value));
+ }
+ });
+
+ return $q.all(promises).then(function(resolves) {
+ var resolveObj = {};
+ var resolveIter = 0;
+ angular.forEach(invocables, function(value, key) {
+ resolveObj[key] = resolves[resolveIter++];
+ });
+
+ return resolveObj;
+ });
+ }
+ };
+ }];
})
/**
* A helper directive for the $modal service. It creates a backdrop element.
*/
- .directive('modalBackdrop', ['$timeout', function ($timeout) {
+ .directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack',
+ function($animate, $injector, $modalStack) {
return {
- restrict: 'EA',
replace: true,
- templateUrl: 'template/modal/backdrop.html',
- link: function (scope) {
-
- scope.animate = false;
-
- //trigger CSS transitions
- $timeout(function () {
- scope.animate = true;
- });
+ templateUrl: 'uib/template/modal/backdrop.html',
+ compile: function(tElement, tAttrs) {
+ tElement.addClass(tAttrs.backdropClass);
+ return linkFn;
}
};
+
+ function linkFn(scope, element, attrs) {
+ if (attrs.modalInClass) {
+ $animate.addClass(element, attrs.modalInClass);
+
+ scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
+ var done = setIsAsync();
+ if (scope.modalOptions.animation) {
+ $animate.removeClass(element, attrs.modalInClass).then(done);
+ } else {
+ done();
+ }
+ });
+ }
+ }
}])
- .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) {
+ .directive('uibModalWindow', ['$uibModalStack', '$q', '$animateCss', '$document',
+ function($modalStack, $q, $animateCss, $document) {
return {
- restrict: 'EA',
scope: {
- index: '@',
- animate: '='
+ index: '@'
},
replace: true,
transclude: true,
templateUrl: function(tElement, tAttrs) {
- return tAttrs.templateUrl || 'template/modal/window.html';
+ return tAttrs.templateUrl || 'uib/template/modal/window.html';
},
- link: function (scope, element, attrs) {
+ link: function(scope, element, attrs) {
element.addClass(attrs.windowClass || '');
+ element.addClass(attrs.windowTopClass || '');
scope.size = attrs.size;
- $timeout(function () {
- // trigger CSS transitions
- scope.animate = true;
- // focus a freshly-opened modal
- element[0].focus();
- });
-
- scope.close = function (evt) {
+ scope.close = function(evt) {
var modal = $modalStack.getTop();
- if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) {
+ if (modal && modal.value.backdrop &&
+ modal.value.backdrop !== 'static' &&
+ evt.target === evt.currentTarget) {
evt.preventDefault();
evt.stopPropagation();
$modalStack.dismiss(modal.key, 'backdrop click');
}
};
+
+ // moved from template to fix issue #2280
+ element.on('click', scope.close);
+
+ // This property is only added to the scope for the purpose of detecting when this directive is rendered.
+ // We can detect that by using this property in the template associated with this directive and then use
+ // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}.
+ scope.$isRendered = true;
+
+ // Deferred object that will be resolved when this modal is render.
+ var modalRenderDeferObj = $q.defer();
+ // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready.
+ // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template.
+ attrs.$observe('modalRender', function(value) {
+ if (value === 'true') {
+ modalRenderDeferObj.resolve();
+ }
+ });
+
+ modalRenderDeferObj.promise.then(function() {
+ var animationPromise = null;
+
+ if (attrs.modalInClass) {
+ animationPromise = $animateCss(element, {
+ addClass: attrs.modalInClass
+ }).start();
+
+ scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
+ var done = setIsAsync();
+ $animateCss(element, {
+ removeClass: attrs.modalInClass
+ }).start().then(done);
+ });
+ }
+
+
+ $q.when(animationPromise).then(function() {
+ // Notify {@link $modalStack} that modal is rendered.
+ var modal = $modalStack.getTop();
+ if (modal) {
+ $modalStack.modalRendered(modal.key);
+ }
+
+ /**
+ * If something within the freshly-opened modal already has focus (perhaps via a
+ * directive that causes focus). then no need to try and focus anything.
+ */
+ if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) {
+ var inputWithAutofocus = element[0].querySelector('[autofocus]');
+ /**
+ * Auto-focusing of a freshly-opened modal element causes any child elements
+ * with the autofocus attribute to lose focus. This is an issue on touch
+ * based devices which will show and then hide the onscreen keyboard.
+ * Attempts to refocus the autofocus element via JavaScript will not reopen
+ * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
+ * the modal element if the modal does not contain an autofocus element.
+ */
+ if (inputWithAutofocus) {
+ inputWithAutofocus.focus();
+ } else {
+ element[0].focus();
+ }
+ }
+ });
+ });
}
};
}])
- .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap',
- function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) {
+ .directive('uibModalAnimationClass', function() {
+ return {
+ compile: function(tElement, tAttrs) {
+ if (tAttrs.modalAnimation) {
+ tElement.addClass(tAttrs.uibModalAnimationClass);
+ }
+ }
+ };
+ })
+ .directive('uibModalTransclude', function() {
+ return {
+ link: function(scope, element, attrs, controller, transclude) {
+ transclude(scope.$parent, function(clone) {
+ element.empty();
+ element.append(clone);
+ });
+ }
+ };
+ })
+
+ .factory('$uibModalStack', ['$animate', '$animateCss', '$document',
+ '$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition',
+ function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) {
var OPENED_MODAL_CLASS = 'modal-open';
var backdropDomEl, backdropScope;
var openedWindows = $$stackedMap.createNew();
- var $modalStack = {};
+ var openedClasses = $$multiMap.createNew();
+ var $modalStack = {
+ NOW_CLOSING_EVENT: 'modal.stack.now-closing'
+ };
+ var topModalIndex = 0;
+ var previousTopOpenedModal = null;
+
+ //Modal focus behavior
+ var tabableSelector = 'a[href], area[href], input:not([disabled]), ' +
+ 'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' +
+ 'iframe, object, embed, *[tabindex], *[contenteditable=true]';
+ var scrollbarPadding;
+
+ function isVisible(element) {
+ return !!(element.offsetWidth ||
+ element.offsetHeight ||
+ element.getClientRects().length);
+ }
function backdropIndex() {
var topBackdropIndex = -1;
@@ -1884,62 +3702,98 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
topBackdropIndex = i;
}
}
+
+ // If any backdrop exist, ensure that it's index is always
+ // right below the top modal
+ if (topBackdropIndex > -1 && topBackdropIndex < topModalIndex) {
+ topBackdropIndex = topModalIndex;
+ }
return topBackdropIndex;
}
- $rootScope.$watch(backdropIndex, function(newBackdropIndex){
+ $rootScope.$watch(backdropIndex, function(newBackdropIndex) {
if (backdropScope) {
backdropScope.index = newBackdropIndex;
}
});
- function removeModalWindow(modalInstance) {
-
- var body = $document.find('body').eq(0);
+ function removeModalWindow(modalInstance, elementToReceiveFocus) {
var modalWindow = openedWindows.get(modalInstance).value;
+ var appendToElement = modalWindow.appendTo;
//clean up the stack
openedWindows.remove(modalInstance);
+ previousTopOpenedModal = openedWindows.top();
+ if (previousTopOpenedModal) {
+ topModalIndex = parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10);
+ }
- //remove window DOM element
- removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() {
- modalWindow.modalScope.$destroy();
- body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0);
- checkRemoveBackdrop();
- });
+ removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() {
+ var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS;
+ openedClasses.remove(modalBodyClass, modalInstance);
+ var areAnyOpen = openedClasses.hasKey(modalBodyClass);
+ appendToElement.toggleClass(modalBodyClass, areAnyOpen);
+ if (!areAnyOpen && scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
+ if (scrollbarPadding.originalRight) {
+ appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'});
+ } else {
+ appendToElement.css({paddingRight: ''});
+ }
+ scrollbarPadding = null;
+ }
+ toggleTopWindowClass(true);
+ }, modalWindow.closedDeferred);
+ checkRemoveBackdrop();
+
+ //move focus to specified element if available, or else to body
+ if (elementToReceiveFocus && elementToReceiveFocus.focus) {
+ elementToReceiveFocus.focus();
+ } else if (appendToElement.focus) {
+ appendToElement.focus();
+ }
+ }
+
+ // Add or remove "windowTopClass" from the top window in the stack
+ function toggleTopWindowClass(toggleSwitch) {
+ var modalWindow;
+
+ if (openedWindows.length() > 0) {
+ modalWindow = openedWindows.top().value;
+ modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch);
+ }
}
function checkRemoveBackdrop() {
- //remove backdrop if no longer needed
- if (backdropDomEl && backdropIndex() == -1) {
- var backdropScopeRef = backdropScope;
- removeAfterAnimate(backdropDomEl, backdropScope, 150, function () {
- backdropScopeRef.$destroy();
- backdropScopeRef = null;
- });
- backdropDomEl = undefined;
- backdropScope = undefined;
- }
+ //remove backdrop if no longer needed
+ if (backdropDomEl && backdropIndex() === -1) {
+ var backdropScopeRef = backdropScope;
+ removeAfterAnimate(backdropDomEl, backdropScope, function() {
+ backdropScopeRef = null;
+ });
+ backdropDomEl = undefined;
+ backdropScope = undefined;
+ }
}
- function removeAfterAnimate(domEl, scope, emulateTime, done) {
- // Closing animation
- scope.animate = false;
+ function removeAfterAnimate(domEl, scope, done, closedDeferred) {
+ var asyncDeferred;
+ var asyncPromise = null;
+ var setIsAsync = function() {
+ if (!asyncDeferred) {
+ asyncDeferred = $q.defer();
+ asyncPromise = asyncDeferred.promise;
+ }
- var transitionEndEventName = $transition.transitionEndEventName;
- if (transitionEndEventName) {
- // transition out
- var timeout = $timeout(afterAnimating, emulateTime);
+ return function asyncDone() {
+ asyncDeferred.resolve();
+ };
+ };
+ scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync);
- domEl.bind(transitionEndEventName, function () {
- $timeout.cancel(timeout);
- afterAnimating();
- scope.$apply();
- });
- } else {
- // Ensure this call is async
- $timeout(afterAnimating, 0);
- }
+ // Note that it's intentional that asyncPromise might be null.
+ // That's when setIsAsync has not been called during the
+ // NOW_CLOSING_EVENT broadcast.
+ return $q.when(asyncPromise).then(afterAnimating);
function afterAnimating() {
if (afterAnimating.done) {
@@ -1947,141 +3801,284 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
}
afterAnimating.done = true;
- domEl.remove();
+ $animate.leave(domEl).then(function() {
+ domEl.remove();
+ if (closedDeferred) {
+ closedDeferred.resolve();
+ }
+ });
+
+ scope.$destroy();
if (done) {
done();
}
}
}
- $document.bind('keydown', function (evt) {
- var modal;
+ $document.on('keydown', keydownListener);
- if (evt.which === 27) {
- modal = openedWindows.top();
- if (modal && modal.value.keyboard) {
- evt.preventDefault();
- $rootScope.$apply(function () {
- $modalStack.dismiss(modal.key, 'escape key press');
- });
- }
- }
+ $rootScope.$on('$destroy', function() {
+ $document.off('keydown', keydownListener);
});
- $modalStack.open = function (modalInstance, modal) {
+ function keydownListener(evt) {
+ if (evt.isDefaultPrevented()) {
+ return evt;
+ }
+
+ var modal = openedWindows.top();
+ if (modal) {
+ switch (evt.which) {
+ case 27: {
+ if (modal.value.keyboard) {
+ evt.preventDefault();
+ $rootScope.$apply(function() {
+ $modalStack.dismiss(modal.key, 'escape key press');
+ });
+ }
+ break;
+ }
+ case 9: {
+ var list = $modalStack.loadFocusElementList(modal);
+ var focusChanged = false;
+ if (evt.shiftKey) {
+ if ($modalStack.isFocusInFirstItem(evt, list) || $modalStack.isModalFocused(evt, modal)) {
+ focusChanged = $modalStack.focusLastFocusableElement(list);
+ }
+ } else {
+ if ($modalStack.isFocusInLastItem(evt, list)) {
+ focusChanged = $modalStack.focusFirstFocusableElement(list);
+ }
+ }
+
+ if (focusChanged) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ $modalStack.open = function(modalInstance, modal) {
+ var modalOpener = $document[0].activeElement,
+ modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS;
+
+ toggleTopWindowClass(false);
+
+ // Store the current top first, to determine what index we ought to use
+ // for the current top modal
+ previousTopOpenedModal = openedWindows.top();
openedWindows.add(modalInstance, {
deferred: modal.deferred,
+ renderDeferred: modal.renderDeferred,
+ closedDeferred: modal.closedDeferred,
modalScope: modal.scope,
backdrop: modal.backdrop,
- keyboard: modal.keyboard
+ keyboard: modal.keyboard,
+ openedClass: modal.openedClass,
+ windowTopClass: modal.windowTopClass,
+ animation: modal.animation,
+ appendTo: modal.appendTo
});
- var body = $document.find('body').eq(0),
+ openedClasses.put(modalBodyClass, modalInstance);
+
+ var appendToElement = modal.appendTo,
currBackdropIndex = backdropIndex();
+ if (!appendToElement.length) {
+ throw new Error('appendTo element not found. Make sure that the element passed is in DOM.');
+ }
+
if (currBackdropIndex >= 0 && !backdropDomEl) {
backdropScope = $rootScope.$new(true);
+ backdropScope.modalOptions = modal;
backdropScope.index = currBackdropIndex;
- backdropDomEl = $compile('')(backdropScope);
- body.append(backdropDomEl);
+ backdropDomEl = angular.element('');
+ backdropDomEl.attr('backdrop-class', modal.backdropClass);
+ if (modal.animation) {
+ backdropDomEl.attr('modal-animation', 'true');
+ }
+ $compile(backdropDomEl)(backdropScope);
+ $animate.enter(backdropDomEl, appendToElement);
+ scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement);
+ if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
+ appendToElement.css({paddingRight: scrollbarPadding.right + 'px'});
+ }
}
- var angularDomEl = angular.element('');
+ // Set the top modal index based on the index of the previous top modal
+ topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0;
+ var angularDomEl = angular.element('');
angularDomEl.attr({
'template-url': modal.windowTemplateUrl,
'window-class': modal.windowClass,
+ 'window-top-class': modal.windowTopClass,
'size': modal.size,
- 'index': openedWindows.length() - 1,
+ 'index': topModalIndex,
'animate': 'animate'
}).html(modal.content);
-
- var modalDomEl = $compile(angularDomEl)(modal.scope);
- openedWindows.top().value.modalDomEl = modalDomEl;
- body.append(modalDomEl);
- body.addClass(OPENED_MODAL_CLASS);
- };
-
- $modalStack.close = function (modalInstance, result) {
- var modalWindow = openedWindows.get(modalInstance).value;
- if (modalWindow) {
- modalWindow.deferred.resolve(result);
- removeModalWindow(modalInstance);
+ if (modal.animation) {
+ angularDomEl.attr('modal-animation', 'true');
}
+
+ appendToElement.addClass(modalBodyClass);
+ $animate.enter($compile(angularDomEl)(modal.scope), appendToElement);
+
+ openedWindows.top().value.modalDomEl = angularDomEl;
+ openedWindows.top().value.modalOpener = modalOpener;
};
- $modalStack.dismiss = function (modalInstance, reason) {
- var modalWindow = openedWindows.get(modalInstance).value;
- if (modalWindow) {
- modalWindow.deferred.reject(reason);
- removeModalWindow(modalInstance);
+ function broadcastClosing(modalWindow, resultOrReason, closing) {
+ return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
+ }
+
+ $modalStack.close = function(modalInstance, result) {
+ var modalWindow = openedWindows.get(modalInstance);
+ if (modalWindow && broadcastClosing(modalWindow, result, true)) {
+ modalWindow.value.modalScope.$$uibDestructionScheduled = true;
+ modalWindow.value.deferred.resolve(result);
+ removeModalWindow(modalInstance, modalWindow.value.modalOpener);
+ return true;
}
+ return !modalWindow;
};
- $modalStack.dismissAll = function (reason) {
+ $modalStack.dismiss = function(modalInstance, reason) {
+ var modalWindow = openedWindows.get(modalInstance);
+ if (modalWindow && broadcastClosing(modalWindow, reason, false)) {
+ modalWindow.value.modalScope.$$uibDestructionScheduled = true;
+ modalWindow.value.deferred.reject(reason);
+ removeModalWindow(modalInstance, modalWindow.value.modalOpener);
+ return true;
+ }
+ return !modalWindow;
+ };
+
+ $modalStack.dismissAll = function(reason) {
var topModal = this.getTop();
- while (topModal) {
- this.dismiss(topModal.key, reason);
+ while (topModal && this.dismiss(topModal.key, reason)) {
topModal = this.getTop();
}
};
- $modalStack.getTop = function () {
+ $modalStack.getTop = function() {
return openedWindows.top();
};
+ $modalStack.modalRendered = function(modalInstance) {
+ var modalWindow = openedWindows.get(modalInstance);
+ if (modalWindow) {
+ modalWindow.value.renderDeferred.resolve();
+ }
+ };
+
+ $modalStack.focusFirstFocusableElement = function(list) {
+ if (list.length > 0) {
+ list[0].focus();
+ return true;
+ }
+ return false;
+ };
+
+ $modalStack.focusLastFocusableElement = function(list) {
+ if (list.length > 0) {
+ list[list.length - 1].focus();
+ return true;
+ }
+ return false;
+ };
+
+ $modalStack.isModalFocused = function(evt, modalWindow) {
+ if (evt && modalWindow) {
+ var modalDomEl = modalWindow.value.modalDomEl;
+ if (modalDomEl && modalDomEl.length) {
+ return (evt.target || evt.srcElement) === modalDomEl[0];
+ }
+ }
+ return false;
+ };
+
+ $modalStack.isFocusInFirstItem = function(evt, list) {
+ if (list.length > 0) {
+ return (evt.target || evt.srcElement) === list[0];
+ }
+ return false;
+ };
+
+ $modalStack.isFocusInLastItem = function(evt, list) {
+ if (list.length > 0) {
+ return (evt.target || evt.srcElement) === list[list.length - 1];
+ }
+ return false;
+ };
+
+ $modalStack.loadFocusElementList = function(modalWindow) {
+ if (modalWindow) {
+ var modalDomE1 = modalWindow.value.modalDomEl;
+ if (modalDomE1 && modalDomE1.length) {
+ var elements = modalDomE1[0].querySelectorAll(tabableSelector);
+ return elements ?
+ Array.prototype.filter.call(elements, function(element) {
+ return isVisible(element);
+ }) : elements;
+ }
+ }
+ };
+
return $modalStack;
}])
- .provider('$modal', function () {
-
+ .provider('$uibModal', function() {
var $modalProvider = {
options: {
- backdrop: true, //can be also false or 'static'
+ animation: true,
+ backdrop: true, //can also be false or 'static'
keyboard: true
},
- $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack',
- function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) {
-
+ $get: ['$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibResolve', '$uibModalStack',
+ function ($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $modalStack) {
var $modal = {};
function getTemplatePromise(options) {
return options.template ? $q.when(options.template) :
- $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) {
- return result.data;
- });
+ $templateRequest(angular.isFunction(options.templateUrl) ?
+ options.templateUrl() : options.templateUrl);
}
- function getResolvePromises(resolves) {
- var promisesArr = [];
- angular.forEach(resolves, function (value, key) {
- if (angular.isFunction(value) || angular.isArray(value)) {
- promisesArr.push($q.when($injector.invoke(value)));
- }
- });
- return promisesArr;
- }
-
- $modal.open = function (modalOptions) {
+ var promiseChain = null;
+ $modal.getPromiseChain = function() {
+ return promiseChain;
+ };
+ $modal.open = function(modalOptions) {
var modalResultDeferred = $q.defer();
var modalOpenedDeferred = $q.defer();
+ var modalClosedDeferred = $q.defer();
+ var modalRenderDeferred = $q.defer();
//prepare an instance of a modal to be injected into controllers and returned to a caller
var modalInstance = {
result: modalResultDeferred.promise,
opened: modalOpenedDeferred.promise,
+ closed: modalClosedDeferred.promise,
+ rendered: modalRenderDeferred.promise,
close: function (result) {
- $modalStack.close(modalInstance, result);
+ return $modalStack.close(modalInstance, result);
},
dismiss: function (reason) {
- $modalStack.dismiss(modalInstance, reason);
+ return $modalStack.dismiss(modalInstance, reason);
}
};
//merge and clean up options
modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
modalOptions.resolve = modalOptions.resolve || {};
+ modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0);
//verify options
if (!modalOptions.template && !modalOptions.templateUrl) {
@@ -2089,261 +4086,379 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
}
var templateAndResolvePromise =
- $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
+ $q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]);
+ function resolveWithTemplate() {
+ return templateAndResolvePromise;
+ }
- templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {
+ // Wait for the resolution of the existing promise chain.
+ // Then switch to our own combined promise dependency (regardless of how the previous modal fared).
+ // Then add to $modalStack and resolve opened.
+ // Finally clean up the chain variable if no subsequent modal has overwritten it.
+ var samePromise;
+ samePromise = promiseChain = $q.all([promiseChain])
+ .then(resolveWithTemplate, resolveWithTemplate)
+ .then(function resolveSuccess(tplAndVars) {
+ var providedScope = modalOptions.scope || $rootScope;
- var modalScope = (modalOptions.scope || $rootScope).$new();
- modalScope.$close = modalInstance.close;
- modalScope.$dismiss = modalInstance.dismiss;
+ var modalScope = providedScope.$new();
+ modalScope.$close = modalInstance.close;
+ modalScope.$dismiss = modalInstance.dismiss;
- var ctrlInstance, ctrlLocals = {};
- var resolveIter = 1;
-
- //controllers
- if (modalOptions.controller) {
- ctrlLocals.$scope = modalScope;
- ctrlLocals.$modalInstance = modalInstance;
- angular.forEach(modalOptions.resolve, function (value, key) {
- ctrlLocals[key] = tplAndVars[resolveIter++];
+ modalScope.$on('$destroy', function() {
+ if (!modalScope.$$uibDestructionScheduled) {
+ modalScope.$dismiss('$uibUnscheduledDestruction');
+ }
});
- ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
- }
+ var ctrlInstance, ctrlInstantiate, ctrlLocals = {};
- $modalStack.open(modalInstance, {
- scope: modalScope,
- deferred: modalResultDeferred,
- content: tplAndVars[0],
- backdrop: modalOptions.backdrop,
- keyboard: modalOptions.keyboard,
- windowClass: modalOptions.windowClass,
- windowTemplateUrl: modalOptions.windowTemplateUrl,
- size: modalOptions.size
- });
+ //controllers
+ if (modalOptions.controller) {
+ ctrlLocals.$scope = modalScope;
+ ctrlLocals.$uibModalInstance = modalInstance;
+ angular.forEach(tplAndVars[1], function(value, key) {
+ ctrlLocals[key] = value;
+ });
+
+ // the third param will make the controller instantiate later,private api
+ // @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126
+ ctrlInstantiate = $controller(modalOptions.controller, ctrlLocals, true);
+ if (modalOptions.controllerAs) {
+ ctrlInstance = ctrlInstantiate.instance;
+
+ if (modalOptions.bindToController) {
+ ctrlInstance.$close = modalScope.$close;
+ ctrlInstance.$dismiss = modalScope.$dismiss;
+ angular.extend(ctrlInstance, providedScope);
+ }
+
+ ctrlInstance = ctrlInstantiate();
+
+ modalScope[modalOptions.controllerAs] = ctrlInstance;
+ } else {
+ ctrlInstance = ctrlInstantiate();
+ }
+
+ if (angular.isFunction(ctrlInstance.$onInit)) {
+ ctrlInstance.$onInit();
+ }
+ }
+
+ $modalStack.open(modalInstance, {
+ scope: modalScope,
+ deferred: modalResultDeferred,
+ renderDeferred: modalRenderDeferred,
+ closedDeferred: modalClosedDeferred,
+ content: tplAndVars[0],
+ animation: modalOptions.animation,
+ backdrop: modalOptions.backdrop,
+ keyboard: modalOptions.keyboard,
+ backdropClass: modalOptions.backdropClass,
+ windowTopClass: modalOptions.windowTopClass,
+ windowClass: modalOptions.windowClass,
+ windowTemplateUrl: modalOptions.windowTemplateUrl,
+ size: modalOptions.size,
+ openedClass: modalOptions.openedClass,
+ appendTo: modalOptions.appendTo
+ });
+ modalOpenedDeferred.resolve(true);
}, function resolveError(reason) {
+ modalOpenedDeferred.reject(reason);
modalResultDeferred.reject(reason);
- });
-
- templateAndResolvePromise.then(function () {
- modalOpenedDeferred.resolve(true);
- }, function () {
- modalOpenedDeferred.reject(false);
+ })['finally'](function() {
+ if (promiseChain === samePromise) {
+ promiseChain = null;
+ }
});
return modalInstance;
};
return $modal;
- }]
+ }
+ ]
};
return $modalProvider;
});
-angular.module('ui.bootstrap.pagination', [])
-
-.controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) {
- var self = this,
- ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
- setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
-
- this.init = function(ngModelCtrl_, config) {
- ngModelCtrl = ngModelCtrl_;
- this.config = config;
-
- ngModelCtrl.$render = function() {
- self.render();
- };
-
- if ($attrs.itemsPerPage) {
- $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) {
- self.itemsPerPage = parseInt(value, 10);
- $scope.totalPages = self.calculateTotalPages();
- });
- } else {
- this.itemsPerPage = config.itemsPerPage;
- }
- };
-
- this.calculateTotalPages = function() {
- var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage);
- return Math.max(totalPages || 0, 1);
- };
-
- this.render = function() {
- $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1;
- };
-
- $scope.selectPage = function(page) {
- if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) {
- ngModelCtrl.$setViewValue(page);
- ngModelCtrl.$render();
- }
- };
-
- $scope.getText = function( key ) {
- return $scope[key + 'Text'] || self.config[key + 'Text'];
- };
- $scope.noPrevious = function() {
- return $scope.page === 1;
- };
- $scope.noNext = function() {
- return $scope.page === $scope.totalPages;
- };
-
- $scope.$watch('totalItems', function() {
- $scope.totalPages = self.calculateTotalPages();
- });
-
- $scope.$watch('totalPages', function(value) {
- setNumPages($scope.$parent, value); // Readonly variable
-
- if ( $scope.page > value ) {
- $scope.selectPage(value);
- } else {
- ngModelCtrl.$render();
- }
- });
-}])
-
-.constant('paginationConfig', {
- itemsPerPage: 10,
- boundaryLinks: false,
- directionLinks: true,
- firstText: 'First',
- previousText: 'Previous',
- nextText: 'Next',
- lastText: 'Last',
- rotate: true
-})
-
-.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) {
+angular.module('ui.bootstrap.paging', [])
+/**
+ * Helper internal service for generating common controller code between the
+ * pager and pagination components
+ */
+.factory('uibPaging', ['$parse', function($parse) {
return {
- restrict: 'EA',
- scope: {
- totalItems: '=',
- firstText: '@',
- previousText: '@',
- nextText: '@',
- lastText: '@'
- },
- require: ['pagination', '?ngModel'],
- controller: 'PaginationController',
- templateUrl: 'template/pagination/pagination.html',
- replace: true,
- link: function(scope, element, attrs, ctrls) {
- var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+ create: function(ctrl, $scope, $attrs) {
+ ctrl.setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
+ ctrl.ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl
+ ctrl._watchers = [];
- if (!ngModelCtrl) {
- return; // do nothing if no ng-model
- }
+ ctrl.init = function(ngModelCtrl, config) {
+ ctrl.ngModelCtrl = ngModelCtrl;
+ ctrl.config = config;
- // Setup configuration parameters
- var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize,
- rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate;
- scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks;
- scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks;
-
- paginationCtrl.init(ngModelCtrl, paginationConfig);
-
- if (attrs.maxSize) {
- scope.$parent.$watch($parse(attrs.maxSize), function(value) {
- maxSize = parseInt(value, 10);
- paginationCtrl.render();
- });
- }
-
- // Create page object used in template
- function makePage(number, text, isActive) {
- return {
- number: number,
- text: text,
- active: isActive
+ ngModelCtrl.$render = function() {
+ ctrl.render();
};
- }
- function getPages(currentPage, totalPages) {
- var pages = [];
-
- // Default page limits
- var startPage = 1, endPage = totalPages;
- var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages );
-
- // recompute if maxSize
- if ( isMaxSized ) {
- if ( rotate ) {
- // Current page is displayed in the middle of the visible ones
- startPage = Math.max(currentPage - Math.floor(maxSize/2), 1);
- endPage = startPage + maxSize - 1;
-
- // Adjust if limit is exceeded
- if (endPage > totalPages) {
- endPage = totalPages;
- startPage = endPage - maxSize + 1;
- }
- } else {
- // Visible pages are paginated with maxSize
- startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1;
-
- // Adjust last page if limit is exceeded
- endPage = Math.min(startPage + maxSize - 1, totalPages);
- }
+ if ($attrs.itemsPerPage) {
+ ctrl._watchers.push($scope.$parent.$watch($attrs.itemsPerPage, function(value) {
+ ctrl.itemsPerPage = parseInt(value, 10);
+ $scope.totalPages = ctrl.calculateTotalPages();
+ ctrl.updatePage();
+ }));
+ } else {
+ ctrl.itemsPerPage = config.itemsPerPage;
}
- // Add page number links
- for (var number = startPage; number <= endPage; number++) {
- var page = makePage(number, number, number === currentPage);
- pages.push(page);
+ $scope.$watch('totalItems', function(newTotal, oldTotal) {
+ if (angular.isDefined(newTotal) || newTotal !== oldTotal) {
+ $scope.totalPages = ctrl.calculateTotalPages();
+ ctrl.updatePage();
+ }
+ });
+ };
+
+ ctrl.calculateTotalPages = function() {
+ var totalPages = ctrl.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / ctrl.itemsPerPage);
+ return Math.max(totalPages || 0, 1);
+ };
+
+ ctrl.render = function() {
+ $scope.page = parseInt(ctrl.ngModelCtrl.$viewValue, 10) || 1;
+ };
+
+ $scope.selectPage = function(page, evt) {
+ if (evt) {
+ evt.preventDefault();
}
- // Add links to move between page sets
- if ( isMaxSized && ! rotate ) {
- if ( startPage > 1 ) {
- var previousPageSet = makePage(startPage - 1, '...', false);
- pages.unshift(previousPageSet);
+ var clickAllowed = !$scope.ngDisabled || !evt;
+ if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) {
+ if (evt && evt.target) {
+ evt.target.blur();
}
-
- if ( endPage < totalPages ) {
- var nextPageSet = makePage(endPage + 1, '...', false);
- pages.push(nextPageSet);
- }
- }
-
- return pages;
- }
-
- var originalRender = paginationCtrl.render;
- paginationCtrl.render = function() {
- originalRender();
- if (scope.page > 0 && scope.page <= scope.totalPages) {
- scope.pages = getPages(scope.page, scope.totalPages);
+ ctrl.ngModelCtrl.$setViewValue(page);
+ ctrl.ngModelCtrl.$render();
}
};
+
+ $scope.getText = function(key) {
+ return $scope[key + 'Text'] || ctrl.config[key + 'Text'];
+ };
+
+ $scope.noPrevious = function() {
+ return $scope.page === 1;
+ };
+
+ $scope.noNext = function() {
+ return $scope.page === $scope.totalPages;
+ };
+
+ ctrl.updatePage = function() {
+ ctrl.setNumPages($scope.$parent, $scope.totalPages); // Readonly variable
+
+ if ($scope.page > $scope.totalPages) {
+ $scope.selectPage($scope.totalPages);
+ } else {
+ ctrl.ngModelCtrl.$render();
+ }
+ };
+
+ $scope.$on('$destroy', function() {
+ while (ctrl._watchers.length) {
+ ctrl._watchers.shift()();
+ }
+ });
}
};
+}]);
+
+angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging'])
+
+.controller('UibPagerController', ['$scope', '$attrs', 'uibPaging', 'uibPagerConfig', function($scope, $attrs, uibPaging, uibPagerConfig) {
+ $scope.align = angular.isDefined($attrs.align) ? $scope.$parent.$eval($attrs.align) : uibPagerConfig.align;
+
+ uibPaging.create(this, $scope, $attrs);
}])
-.constant('pagerConfig', {
+.constant('uibPagerConfig', {
itemsPerPage: 10,
previousText: '« Previous',
nextText: 'Next »',
align: true
})
-.directive('pager', ['pagerConfig', function(pagerConfig) {
+.directive('uibPager', ['uibPagerConfig', function(uibPagerConfig) {
return {
- restrict: 'EA',
scope: {
totalItems: '=',
previousText: '@',
- nextText: '@'
+ nextText: '@',
+ ngDisabled: '='
+ },
+ require: ['uibPager', '?ngModel'],
+ controller: 'UibPagerController',
+ controllerAs: 'pager',
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/pager/pager.html';
+ },
+ replace: true,
+ link: function(scope, element, attrs, ctrls) {
+ var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+ if (!ngModelCtrl) {
+ return; // do nothing if no ng-model
+ }
+
+ paginationCtrl.init(ngModelCtrl, uibPagerConfig);
+ }
+ };
+}]);
+
+angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging'])
+.controller('UibPaginationController', ['$scope', '$attrs', '$parse', 'uibPaging', 'uibPaginationConfig', function($scope, $attrs, $parse, uibPaging, uibPaginationConfig) {
+ var ctrl = this;
+ // Setup configuration parameters
+ var maxSize = angular.isDefined($attrs.maxSize) ? $scope.$parent.$eval($attrs.maxSize) : uibPaginationConfig.maxSize,
+ rotate = angular.isDefined($attrs.rotate) ? $scope.$parent.$eval($attrs.rotate) : uibPaginationConfig.rotate,
+ forceEllipses = angular.isDefined($attrs.forceEllipses) ? $scope.$parent.$eval($attrs.forceEllipses) : uibPaginationConfig.forceEllipses,
+ boundaryLinkNumbers = angular.isDefined($attrs.boundaryLinkNumbers) ? $scope.$parent.$eval($attrs.boundaryLinkNumbers) : uibPaginationConfig.boundaryLinkNumbers,
+ pageLabel = angular.isDefined($attrs.pageLabel) ? function(idx) { return $scope.$parent.$eval($attrs.pageLabel, {$page: idx}); } : angular.identity;
+ $scope.boundaryLinks = angular.isDefined($attrs.boundaryLinks) ? $scope.$parent.$eval($attrs.boundaryLinks) : uibPaginationConfig.boundaryLinks;
+ $scope.directionLinks = angular.isDefined($attrs.directionLinks) ? $scope.$parent.$eval($attrs.directionLinks) : uibPaginationConfig.directionLinks;
+
+ uibPaging.create(this, $scope, $attrs);
+
+ if ($attrs.maxSize) {
+ ctrl._watchers.push($scope.$parent.$watch($parse($attrs.maxSize), function(value) {
+ maxSize = parseInt(value, 10);
+ ctrl.render();
+ }));
+ }
+
+ // Create page object used in template
+ function makePage(number, text, isActive) {
+ return {
+ number: number,
+ text: text,
+ active: isActive
+ };
+ }
+
+ function getPages(currentPage, totalPages) {
+ var pages = [];
+
+ // Default page limits
+ var startPage = 1, endPage = totalPages;
+ var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages;
+
+ // recompute if maxSize
+ if (isMaxSized) {
+ if (rotate) {
+ // Current page is displayed in the middle of the visible ones
+ startPage = Math.max(currentPage - Math.floor(maxSize / 2), 1);
+ endPage = startPage + maxSize - 1;
+
+ // Adjust if limit is exceeded
+ if (endPage > totalPages) {
+ endPage = totalPages;
+ startPage = endPage - maxSize + 1;
+ }
+ } else {
+ // Visible pages are paginated with maxSize
+ startPage = (Math.ceil(currentPage / maxSize) - 1) * maxSize + 1;
+
+ // Adjust last page if limit is exceeded
+ endPage = Math.min(startPage + maxSize - 1, totalPages);
+ }
+ }
+
+ // Add page number links
+ for (var number = startPage; number <= endPage; number++) {
+ var page = makePage(number, pageLabel(number), number === currentPage);
+ pages.push(page);
+ }
+
+ // Add links to move between page sets
+ if (isMaxSized && maxSize > 0 && (!rotate || forceEllipses || boundaryLinkNumbers)) {
+ if (startPage > 1) {
+ if (!boundaryLinkNumbers || startPage > 3) { //need ellipsis for all options unless range is too close to beginning
+ var previousPageSet = makePage(startPage - 1, '...', false);
+ pages.unshift(previousPageSet);
+ }
+ if (boundaryLinkNumbers) {
+ if (startPage === 3) { //need to replace ellipsis when the buttons would be sequential
+ var secondPageLink = makePage(2, '2', false);
+ pages.unshift(secondPageLink);
+ }
+ //add the first page
+ var firstPageLink = makePage(1, '1', false);
+ pages.unshift(firstPageLink);
+ }
+ }
+
+ if (endPage < totalPages) {
+ if (!boundaryLinkNumbers || endPage < totalPages - 2) { //need ellipsis for all options unless range is too close to end
+ var nextPageSet = makePage(endPage + 1, '...', false);
+ pages.push(nextPageSet);
+ }
+ if (boundaryLinkNumbers) {
+ if (endPage === totalPages - 2) { //need to replace ellipsis when the buttons would be sequential
+ var secondToLastPageLink = makePage(totalPages - 1, totalPages - 1, false);
+ pages.push(secondToLastPageLink);
+ }
+ //add the last page
+ var lastPageLink = makePage(totalPages, totalPages, false);
+ pages.push(lastPageLink);
+ }
+ }
+ }
+ return pages;
+ }
+
+ var originalRender = this.render;
+ this.render = function() {
+ originalRender();
+ if ($scope.page > 0 && $scope.page <= $scope.totalPages) {
+ $scope.pages = getPages($scope.page, $scope.totalPages);
+ }
+ };
+}])
+
+.constant('uibPaginationConfig', {
+ itemsPerPage: 10,
+ boundaryLinks: false,
+ boundaryLinkNumbers: false,
+ directionLinks: true,
+ firstText: 'First',
+ previousText: 'Previous',
+ nextText: 'Next',
+ lastText: 'Last',
+ rotate: true,
+ forceEllipses: false
+})
+
+.directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, uibPaginationConfig) {
+ return {
+ scope: {
+ totalItems: '=',
+ firstText: '@',
+ previousText: '@',
+ nextText: '@',
+ lastText: '@',
+ ngDisabled:'='
+ },
+ require: ['uibPagination', '?ngModel'],
+ controller: 'UibPaginationController',
+ controllerAs: 'pagination',
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/pagination/pagination.html';
},
- require: ['pager', '?ngModel'],
- controller: 'PaginationController',
- templateUrl: 'template/pagination/pager.html',
replace: true,
link: function(scope, element, attrs, ctrls) {
var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
@@ -2352,8 +4467,7 @@ angular.module('ui.bootstrap.pagination', [])
return; // do nothing if no ng-model
}
- scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align;
- paginationCtrl.init(ngModelCtrl, pagerConfig);
+ paginationCtrl.init(ngModelCtrl, uibPaginationConfig);
}
};
}]);
@@ -2363,25 +4477,30 @@ angular.module('ui.bootstrap.pagination', [])
* function, placement as a function, inside, support for more triggers than
* just mouse enter/leave, html tooltips, and selector delegation.
*/
-angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] )
+angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap'])
/**
* The $tooltip service creates tooltip- and popover-like directives as well as
* houses global options for them.
*/
-.provider( '$tooltip', function () {
+.provider('$uibTooltip', function() {
// The default options tooltip and popover.
var defaultOptions = {
placement: 'top',
+ placementClassPrefix: '',
animation: true,
- popupDelay: 0
+ popupDelay: 0,
+ popupCloseDelay: 0,
+ useContentExp: false
};
// Default hide triggers for each show trigger
var triggerMap = {
'mouseenter': 'mouseleave',
'click': 'click',
- 'focus': 'blur'
+ 'outsideClick': 'outsideClick',
+ 'focus': 'blur',
+ 'none': ''
};
// The options specified to the provider globally.
@@ -2396,23 +4515,23 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
* $tooltipProvider.options( { placement: 'left' } );
* });
*/
- this.options = function( value ) {
- angular.extend( globalOptions, value );
+ this.options = function(value) {
+ angular.extend(globalOptions, value);
};
/**
* This allows you to extend the set of trigger mappings available. E.g.:
*
- * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' );
+ * $tooltipProvider.setTriggers( { 'openTrigger': 'closeTrigger' } );
*/
- this.setTriggers = function setTriggers ( triggers ) {
- angular.extend( triggerMap, triggers );
+ this.setTriggers = function setTriggers(triggers) {
+ angular.extend(triggerMap, triggers);
};
/**
- * This is a helper function for translating camel-case to snake-case.
+ * This is a helper function for translating camel-case to snake_case.
*/
- function snake_case(name){
+ function snake_case(name) {
var regexp = /[A-Z]/g;
var separator = '-';
return name.replace(regexp, function(letter, pos) {
@@ -2424,9 +4543,27 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
* Returns the actual instance of the $tooltip service.
* TODO support multiple triggers
*/
- this.$get = [ '$window', '$compile', '$timeout', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) {
- return function $tooltip ( type, prefix, defaultTriggerShow ) {
- var options = angular.extend( {}, defaultOptions, globalOptions );
+ this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) {
+ var openedTooltips = $$stackedMap.createNew();
+ $document.on('keypress', keypressListener);
+
+ $rootScope.$on('$destroy', function() {
+ $document.off('keypress', keypressListener);
+ });
+
+ function keypressListener(e) {
+ if (e.which === 27) {
+ var last = openedTooltips.top();
+ if (last) {
+ last.value.close();
+ openedTooltips.removeTop();
+ last = null;
+ }
+ }
+ }
+
+ return function $tooltip(ttType, prefix, defaultTriggerShow, options) {
+ options = angular.extend({}, defaultOptions, globalOptions, options);
/**
* Returns an object of show and hide triggers.
@@ -2442,59 +4579,104 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
* undefined; otherwise, it uses the `triggerMap` value of the show
* trigger; else it will just use the show trigger.
*/
- function getTriggers ( trigger ) {
- var show = trigger || options.trigger || defaultTriggerShow;
- var hide = triggerMap[show] || show;
+ function getTriggers(trigger) {
+ var show = (trigger || options.trigger || defaultTriggerShow).split(' ');
+ var hide = show.map(function(trigger) {
+ return triggerMap[trigger] || trigger;
+ });
return {
show: show,
hide: hide
};
}
- var directiveName = snake_case( type );
+ var directiveName = snake_case(ttType);
var startSym = $interpolate.startSymbol();
var endSym = $interpolate.endSymbol();
var template =
- '
'+
+ '
' +
'
';
return {
- restrict: 'EA',
- scope: true,
- compile: function (tElem, tAttrs) {
- var tooltipLinker = $compile( template );
+ compile: function(tElem, tAttrs) {
+ var tooltipLinker = $compile(template);
- return function link ( scope, element, attrs ) {
+ return function link(scope, element, attrs, tooltipCtrl) {
var tooltip;
+ var tooltipLinkedScope;
var transitionTimeout;
- var popupTimeout;
- var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false;
- var triggers = getTriggers( undefined );
- var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']);
+ var showTimeout;
+ var hideTimeout;
+ var positionTimeout;
+ var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false;
+ var triggers = getTriggers(undefined);
+ var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']);
+ var ttScope = scope.$new(true);
+ var repositionScheduled = false;
+ var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false;
+ var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false;
+ var observers = [];
+ var lastPlacement;
- var positionTooltip = function () {
+ var positionTooltip = function() {
+ // check if tooltip exists and is not empty
+ if (!tooltip || !tooltip.html()) { return; }
- var ttPosition = $position.positionElements(element, tooltip, scope.tt_placement, appendToBody);
- ttPosition.top += 'px';
- ttPosition.left += 'px';
+ if (!positionTimeout) {
+ positionTimeout = $timeout(function() {
+ var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
+ tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' });
- // Now set the calculated positioning.
- tooltip.css( ttPosition );
+ if (!tooltip.hasClass(ttPosition.placement.split('-')[0])) {
+ tooltip.removeClass(lastPlacement.split('-')[0]);
+ tooltip.addClass(ttPosition.placement.split('-')[0]);
+ }
+
+ if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) {
+ tooltip.removeClass(options.placementClassPrefix + lastPlacement);
+ tooltip.addClass(options.placementClassPrefix + ttPosition.placement);
+ }
+
+ // first time through tt element will have the
+ // uib-position-measure class or if the placement
+ // has changed we need to position the arrow.
+ if (tooltip.hasClass('uib-position-measure')) {
+ $position.positionArrow(tooltip, ttPosition.placement);
+ tooltip.removeClass('uib-position-measure');
+ } else if (lastPlacement !== ttPosition.placement) {
+ $position.positionArrow(tooltip, ttPosition.placement);
+ }
+ lastPlacement = ttPosition.placement;
+
+ positionTimeout = null;
+ }, 0, false);
+ }
};
+ // Set up the correct scope to allow transclusion later
+ ttScope.origScope = scope;
+
// By default, the tooltip is not open.
// TODO add ability to start tooltip opened
- scope.tt_isOpen = false;
+ ttScope.isOpen = false;
+ openedTooltips.add(ttScope, {
+ close: hide
+ });
- function toggleTooltipBind () {
- if ( ! scope.tt_isOpen ) {
+ function toggleTooltipBind() {
+ if (!ttScope.isOpen) {
showTooltipBind();
} else {
hideTooltipBind();
@@ -2503,174 +4685,338 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
// Show the tooltip with delay if specified, otherwise show it immediately
function showTooltipBind() {
- if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) {
+ if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) {
return;
}
- if ( scope.tt_popupDelay ) {
+
+ cancelHide();
+ prepareTooltip();
+
+ if (ttScope.popupDelay) {
// Do nothing if the tooltip was already scheduled to pop-up.
// This happens if show is triggered multiple times before any hide is triggered.
- if (!popupTimeout) {
- popupTimeout = $timeout( show, scope.tt_popupDelay, false );
- popupTimeout.then(function(reposition){reposition();});
+ if (!showTimeout) {
+ showTimeout = $timeout(show, ttScope.popupDelay, false);
}
} else {
- show()();
+ show();
}
}
- function hideTooltipBind () {
- scope.$apply(function () {
+ function hideTooltipBind() {
+ cancelShow();
+
+ if (ttScope.popupCloseDelay) {
+ if (!hideTimeout) {
+ hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false);
+ }
+ } else {
hide();
- });
+ }
}
// Show the tooltip popup element.
function show() {
-
- popupTimeout = null;
-
- // If there is a pending remove transition, we must cancel it, lest the
- // tooltip be mysteriously removed.
- if ( transitionTimeout ) {
- $timeout.cancel( transitionTimeout );
- transitionTimeout = null;
- }
+ cancelShow();
+ cancelHide();
// Don't show empty tooltips.
- if ( ! scope.tt_content ) {
+ if (!ttScope.content) {
return angular.noop;
}
createTooltip();
- // Set the initial positioning.
- tooltip.css({ top: 0, left: 0, display: 'block' });
+ // And show the tooltip.
+ ttScope.$evalAsync(function() {
+ ttScope.isOpen = true;
+ assignIsOpen(true);
+ positionTooltip();
+ });
+ }
- // Now we add it to the DOM because need some info about it. But it's not
- // visible yet anyway.
- if ( appendToBody ) {
- $document.find( 'body' ).append( tooltip );
- } else {
- element.after( tooltip );
+ function cancelShow() {
+ if (showTimeout) {
+ $timeout.cancel(showTimeout);
+ showTimeout = null;
}
- positionTooltip();
-
- // And show the tooltip.
- scope.tt_isOpen = true;
- scope.$digest(); // digest required as $apply is not called
-
- // Return positioning function as promise callback for correct
- // positioning after draw.
- return positionTooltip;
+ if (positionTimeout) {
+ $timeout.cancel(positionTimeout);
+ positionTimeout = null;
+ }
}
// Hide the tooltip popup element.
function hide() {
+ if (!ttScope) {
+ return;
+ }
+
// First things first: we don't show it anymore.
- scope.tt_isOpen = false;
-
- //if tooltip is going to be shown after delay, we must cancel this
- $timeout.cancel( popupTimeout );
- popupTimeout = null;
-
- // And now we remove it from the DOM. However, if we have animation, we
- // need to wait for it to expire beforehand.
- // FIXME: this is a placeholder for a port of the transitions library.
- if ( scope.tt_animation ) {
- if (!transitionTimeout) {
- transitionTimeout = $timeout(removeTooltip, 500);
+ ttScope.$evalAsync(function() {
+ if (ttScope) {
+ ttScope.isOpen = false;
+ assignIsOpen(false);
+ // And now we remove it from the DOM. However, if we have animation, we
+ // need to wait for it to expire beforehand.
+ // FIXME: this is a placeholder for a port of the transitions library.
+ // The fade transition in TWBS is 150ms.
+ if (ttScope.animation) {
+ if (!transitionTimeout) {
+ transitionTimeout = $timeout(removeTooltip, 150, false);
+ }
+ } else {
+ removeTooltip();
+ }
}
- } else {
- removeTooltip();
+ });
+ }
+
+ function cancelHide() {
+ if (hideTimeout) {
+ $timeout.cancel(hideTimeout);
+ hideTimeout = null;
+ }
+
+ if (transitionTimeout) {
+ $timeout.cancel(transitionTimeout);
+ transitionTimeout = null;
}
}
function createTooltip() {
// There can only be one tooltip element per directive shown at once.
if (tooltip) {
- removeTooltip();
+ return;
}
- tooltip = tooltipLinker(scope, function () {});
- // Get contents rendered into the tooltip
- scope.$digest();
+ tooltipLinkedScope = ttScope.$new();
+ tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) {
+ if (appendToBody) {
+ $document.find('body').append(tooltip);
+ } else {
+ element.after(tooltip);
+ }
+ });
+
+ prepObservers();
}
function removeTooltip() {
- transitionTimeout = null;
+ cancelShow();
+ cancelHide();
+ unregisterObservers();
+
if (tooltip) {
tooltip.remove();
tooltip = null;
}
+ if (tooltipLinkedScope) {
+ tooltipLinkedScope.$destroy();
+ tooltipLinkedScope = null;
+ }
}
+ /**
+ * Set the initial scope values. Once
+ * the tooltip is created, the observers
+ * will be added to keep things in sync.
+ */
+ function prepareTooltip() {
+ ttScope.title = attrs[prefix + 'Title'];
+ if (contentParse) {
+ ttScope.content = contentParse(scope);
+ } else {
+ ttScope.content = attrs[ttType];
+ }
+
+ ttScope.popupClass = attrs[prefix + 'Class'];
+ ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement;
+ var placement = $position.parsePlacement(ttScope.placement);
+ lastPlacement = placement[1] ? placement[0] + '-' + placement[1] : placement[0];
+
+ var delay = parseInt(attrs[prefix + 'PopupDelay'], 10);
+ var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10);
+ ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay;
+ ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay;
+ }
+
+ function assignIsOpen(isOpen) {
+ if (isOpenParse && angular.isFunction(isOpenParse.assign)) {
+ isOpenParse.assign(scope, isOpen);
+ }
+ }
+
+ ttScope.contentExp = function() {
+ return ttScope.content;
+ };
+
/**
* Observe the relevant attributes.
*/
- attrs.$observe( type, function ( val ) {
- scope.tt_content = val;
+ attrs.$observe('disabled', function(val) {
+ if (val) {
+ cancelShow();
+ }
- if (!val && scope.tt_isOpen ) {
+ if (val && ttScope.isOpen) {
hide();
}
});
- attrs.$observe( prefix+'Title', function ( val ) {
- scope.tt_title = val;
- });
+ if (isOpenParse) {
+ scope.$watch(isOpenParse, function(val) {
+ if (ttScope && !val === ttScope.isOpen) {
+ toggleTooltipBind();
+ }
+ });
+ }
- attrs.$observe( prefix+'Placement', function ( val ) {
- scope.tt_placement = angular.isDefined( val ) ? val : options.placement;
- });
+ function prepObservers() {
+ observers.length = 0;
- attrs.$observe( prefix+'PopupDelay', function ( val ) {
- var delay = parseInt( val, 10 );
- scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay;
- });
+ if (contentParse) {
+ observers.push(
+ scope.$watch(contentParse, function(val) {
+ ttScope.content = val;
+ if (!val && ttScope.isOpen) {
+ hide();
+ }
+ })
+ );
- var unregisterTriggers = function () {
- element.unbind(triggers.show, showTooltipBind);
- element.unbind(triggers.hide, hideTooltipBind);
+ observers.push(
+ tooltipLinkedScope.$watch(function() {
+ if (!repositionScheduled) {
+ repositionScheduled = true;
+ tooltipLinkedScope.$$postDigest(function() {
+ repositionScheduled = false;
+ if (ttScope && ttScope.isOpen) {
+ positionTooltip();
+ }
+ });
+ }
+ })
+ );
+ } else {
+ observers.push(
+ attrs.$observe(ttType, function(val) {
+ ttScope.content = val;
+ if (!val && ttScope.isOpen) {
+ hide();
+ } else {
+ positionTooltip();
+ }
+ })
+ );
+ }
+
+ observers.push(
+ attrs.$observe(prefix + 'Title', function(val) {
+ ttScope.title = val;
+ if (ttScope.isOpen) {
+ positionTooltip();
+ }
+ })
+ );
+
+ observers.push(
+ attrs.$observe(prefix + 'Placement', function(val) {
+ ttScope.placement = val ? val : options.placement;
+ if (ttScope.isOpen) {
+ positionTooltip();
+ }
+ })
+ );
+ }
+
+ function unregisterObservers() {
+ if (observers.length) {
+ angular.forEach(observers, function(observer) {
+ observer();
+ });
+ observers.length = 0;
+ }
+ }
+
+ // hide tooltips/popovers for outsideClick trigger
+ function bodyHideTooltipBind(e) {
+ if (!ttScope || !ttScope.isOpen || !tooltip) {
+ return;
+ }
+ // make sure the tooltip/popover link or tool tooltip/popover itself were not clicked
+ if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) {
+ hideTooltipBind();
+ }
+ }
+
+ var unregisterTriggers = function() {
+ triggers.show.forEach(function(trigger) {
+ if (trigger === 'outsideClick') {
+ element.off('click', toggleTooltipBind);
+ } else {
+ element.off(trigger, showTooltipBind);
+ element.off(trigger, toggleTooltipBind);
+ }
+ });
+ triggers.hide.forEach(function(trigger) {
+ if (trigger === 'outsideClick') {
+ $document.off('click', bodyHideTooltipBind);
+ } else {
+ element.off(trigger, hideTooltipBind);
+ }
+ });
};
- attrs.$observe( prefix+'Trigger', function ( val ) {
+ function prepTriggers() {
+ var val = attrs[prefix + 'Trigger'];
unregisterTriggers();
- triggers = getTriggers( val );
+ triggers = getTriggers(val);
- if ( triggers.show === triggers.hide ) {
- element.bind( triggers.show, toggleTooltipBind );
- } else {
- element.bind( triggers.show, showTooltipBind );
- element.bind( triggers.hide, hideTooltipBind );
+ if (triggers.show !== 'none') {
+ triggers.show.forEach(function(trigger, idx) {
+ if (trigger === 'outsideClick') {
+ element.on('click', toggleTooltipBind);
+ $document.on('click', bodyHideTooltipBind);
+ } else if (trigger === triggers.hide[idx]) {
+ element.on(trigger, toggleTooltipBind);
+ } else if (trigger) {
+ element.on(trigger, showTooltipBind);
+ element.on(triggers.hide[idx], hideTooltipBind);
+ }
+
+ element.on('keypress', function(e) {
+ if (e.which === 27) {
+ hideTooltipBind();
+ }
+ });
+ });
}
- });
+ }
+
+ prepTriggers();
var animation = scope.$eval(attrs[prefix + 'Animation']);
- scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation;
+ ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation;
- attrs.$observe( prefix+'AppendToBody', function ( val ) {
- appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody;
- });
-
- // if a tooltip is attached to we need to remove it on
- // location change as its parent scope will probably not be destroyed
- // by the change.
- if ( appendToBody ) {
- scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () {
- if ( scope.tt_isOpen ) {
- hide();
- }
- });
+ var appendToBodyVal;
+ var appendKey = prefix + 'AppendToBody';
+ if (appendKey in attrs && attrs[appendKey] === undefined) {
+ appendToBodyVal = true;
+ } else {
+ appendToBodyVal = scope.$eval(attrs[appendKey]);
}
+ appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody;
+
// Make sure tooltip is destroyed and removed.
scope.$on('$destroy', function onDestroyTooltip() {
- $timeout.cancel( transitionTimeout );
- $timeout.cancel( popupTimeout );
unregisterTriggers();
removeTooltip();
+ openedTooltips.remove(ttScope);
+ ttScope = null;
});
};
}
@@ -2679,172 +5025,371 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
}];
})
-.directive( 'tooltipPopup', function () {
+// This is mostly ngInclude code but with a custom scope
+.directive('uibTooltipTemplateTransclude', [
+ '$animate', '$sce', '$compile', '$templateRequest',
+function ($animate, $sce, $compile, $templateRequest) {
return {
- restrict: 'EA',
- replace: true,
- scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },
- templateUrl: 'template/tooltip/tooltip-popup.html'
- };
-})
+ link: function(scope, elem, attrs) {
+ var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);
-.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) {
- return $tooltip( 'tooltip', 'tooltip', 'mouseenter' );
+ var changeCounter = 0,
+ currentScope,
+ previousElement,
+ currentElement;
+
+ var cleanupLastIncludeContent = function() {
+ if (previousElement) {
+ previousElement.remove();
+ previousElement = null;
+ }
+
+ if (currentScope) {
+ currentScope.$destroy();
+ currentScope = null;
+ }
+
+ if (currentElement) {
+ $animate.leave(currentElement).then(function() {
+ previousElement = null;
+ });
+ previousElement = currentElement;
+ currentElement = null;
+ }
+ };
+
+ scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) {
+ var thisChangeId = ++changeCounter;
+
+ if (src) {
+ //set the 2nd param to true to ignore the template request error so that the inner
+ //contents and scope can be cleaned up.
+ $templateRequest(src, true).then(function(response) {
+ if (thisChangeId !== changeCounter) { return; }
+ var newScope = origScope.$new();
+ var template = response;
+
+ var clone = $compile(template)(newScope, function(clone) {
+ cleanupLastIncludeContent();
+ $animate.enter(clone, elem);
+ });
+
+ currentScope = newScope;
+ currentElement = clone;
+
+ currentScope.$emit('$includeContentLoaded', src);
+ }, function() {
+ if (thisChangeId === changeCounter) {
+ cleanupLastIncludeContent();
+ scope.$emit('$includeContentError', src);
+ }
+ });
+ scope.$emit('$includeContentRequested', src);
+ } else {
+ cleanupLastIncludeContent();
+ }
+ });
+
+ scope.$on('$destroy', cleanupLastIncludeContent);
+ }
+ };
}])
-.directive( 'tooltipHtmlUnsafePopup', function () {
+/**
+ * Note that it's intentional that these classes are *not* applied through $animate.
+ * They must not be animated as they're expected to be present on the tooltip on
+ * initialization.
+ */
+.directive('uibTooltipClasses', ['$uibPosition', function($uibPosition) {
+ return {
+ restrict: 'A',
+ link: function(scope, element, attrs) {
+ // need to set the primary position so the
+ // arrow has space during position measure.
+ // tooltip.positionTooltip()
+ if (scope.placement) {
+ // // There are no top-left etc... classes
+ // // in TWBS, so we need the primary position.
+ var position = $uibPosition.parsePlacement(scope.placement);
+ element.addClass(position[0]);
+ }
+
+ if (scope.popupClass) {
+ element.addClass(scope.popupClass);
+ }
+
+ if (scope.animation()) {
+ element.addClass(attrs.tooltipAnimationClass);
+ }
+ }
+ };
+}])
+
+.directive('uibTooltipPopup', function() {
return {
- restrict: 'EA',
replace: true,
- scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },
- templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html'
+ scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+ templateUrl: 'uib/template/tooltip/tooltip-popup.html'
};
})
-.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) {
- return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' );
+.directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter');
+}])
+
+.directive('uibTooltipTemplatePopup', function() {
+ return {
+ replace: true,
+ scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
+ originScope: '&' },
+ templateUrl: 'uib/template/tooltip/tooltip-template-popup.html'
+ };
+})
+
+.directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', {
+ useContentExp: true
+ });
+}])
+
+.directive('uibTooltipHtmlPopup', function() {
+ return {
+ replace: true,
+ scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+ templateUrl: 'uib/template/tooltip/tooltip-html-popup.html'
+ };
+})
+
+.directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', {
+ useContentExp: true
+ });
}]);
/**
* The following features are still outstanding: popup delay, animation as a
* function, placement as a function, inside, support for more triggers than
- * just mouse enter/leave, html popovers, and selector delegatation.
+ * just mouse enter/leave, and selector delegatation.
*/
-angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
+angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip'])
-.directive( 'popoverPopup', function () {
+.directive('uibPopoverTemplatePopup', function() {
return {
- restrict: 'EA',
replace: true,
- scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' },
- templateUrl: 'template/popover/popover.html'
+ scope: { uibTitle: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
+ originScope: '&' },
+ templateUrl: 'uib/template/popover/popover-template.html'
};
})
-.directive( 'popover', [ '$tooltip', function ( $tooltip ) {
- return $tooltip( 'popover', 'popover', 'click' );
+.directive('uibPopoverTemplate', ['$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibPopoverTemplate', 'popover', 'click', {
+ useContentExp: true
+ });
+}])
+
+.directive('uibPopoverHtmlPopup', function() {
+ return {
+ replace: true,
+ scope: { contentExp: '&', uibTitle: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+ templateUrl: 'uib/template/popover/popover-html.html'
+ };
+})
+
+.directive('uibPopoverHtml', ['$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibPopoverHtml', 'popover', 'click', {
+ useContentExp: true
+ });
+}])
+
+.directive('uibPopoverPopup', function() {
+ return {
+ replace: true,
+ scope: { uibTitle: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+ templateUrl: 'uib/template/popover/popover.html'
+ };
+})
+
+.directive('uibPopover', ['$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibPopover', 'popover', 'click');
}]);
angular.module('ui.bootstrap.progressbar', [])
-.constant('progressConfig', {
+.constant('uibProgressConfig', {
animate: true,
max: 100
})
-.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) {
- var self = this,
- animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
+.controller('UibProgressController', ['$scope', '$attrs', 'uibProgressConfig', function($scope, $attrs, progressConfig) {
+ var self = this,
+ animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
- this.bars = [];
- $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max;
+ this.bars = [];
+ $scope.max = getMaxOrDefault();
- this.addBar = function(bar, element) {
- if ( !animate ) {
- element.css({'transition': 'none'});
- }
+ this.addBar = function(bar, element, attrs) {
+ if (!animate) {
+ element.css({'transition': 'none'});
+ }
- this.bars.push(bar);
+ this.bars.push(bar);
- bar.$watch('value', function( value ) {
- bar.percent = +(100 * value / $scope.max).toFixed(2);
- });
+ bar.max = getMaxOrDefault();
+ bar.title = attrs && angular.isDefined(attrs.title) ? attrs.title : 'progressbar';
- bar.$on('$destroy', function() {
- element = null;
- self.removeBar(bar);
- });
+ bar.$watch('value', function(value) {
+ bar.recalculatePercentage();
+ });
+
+ bar.recalculatePercentage = function() {
+ var totalPercentage = self.bars.reduce(function(total, bar) {
+ bar.percent = +(100 * bar.value / bar.max).toFixed(2);
+ return total + bar.percent;
+ }, 0);
+
+ if (totalPercentage > 100) {
+ bar.percent -= totalPercentage - 100;
+ }
};
- this.removeBar = function(bar) {
- this.bars.splice(this.bars.indexOf(bar), 1);
- };
+ bar.$on('$destroy', function() {
+ element = null;
+ self.removeBar(bar);
+ });
+ };
+
+ this.removeBar = function(bar) {
+ this.bars.splice(this.bars.indexOf(bar), 1);
+ this.bars.forEach(function (bar) {
+ bar.recalculatePercentage();
+ });
+ };
+
+ //$attrs.$observe('maxParam', function(maxParam) {
+ $scope.$watch('maxParam', function(maxParam) {
+ self.bars.forEach(function(bar) {
+ bar.max = getMaxOrDefault();
+ bar.recalculatePercentage();
+ });
+ });
+
+ function getMaxOrDefault () {
+ return angular.isDefined($scope.maxParam) ? $scope.maxParam : progressConfig.max;
+ }
}])
-.directive('progress', function() {
- return {
- restrict: 'EA',
- replace: true,
- transclude: true,
- controller: 'ProgressController',
- require: 'progress',
- scope: {},
- templateUrl: 'template/progressbar/progress.html'
- };
+.directive('uibProgress', function() {
+ return {
+ replace: true,
+ transclude: true,
+ controller: 'UibProgressController',
+ require: 'uibProgress',
+ scope: {
+ maxParam: '=?max'
+ },
+ templateUrl: 'uib/template/progressbar/progress.html'
+ };
})
-.directive('bar', function() {
- return {
- restrict: 'EA',
- replace: true,
- transclude: true,
- require: '^progress',
- scope: {
- value: '=',
- type: '@'
- },
- templateUrl: 'template/progressbar/bar.html',
- link: function(scope, element, attrs, progressCtrl) {
- progressCtrl.addBar(scope, element);
- }
- };
+.directive('uibBar', function() {
+ return {
+ replace: true,
+ transclude: true,
+ require: '^uibProgress',
+ scope: {
+ value: '=',
+ type: '@'
+ },
+ templateUrl: 'uib/template/progressbar/bar.html',
+ link: function(scope, element, attrs, progressCtrl) {
+ progressCtrl.addBar(scope, element, attrs);
+ }
+ };
})
-.directive('progressbar', function() {
- return {
- restrict: 'EA',
- replace: true,
- transclude: true,
- controller: 'ProgressController',
- scope: {
- value: '=',
- type: '@'
- },
- templateUrl: 'template/progressbar/progressbar.html',
- link: function(scope, element, attrs, progressCtrl) {
- progressCtrl.addBar(scope, angular.element(element.children()[0]));
- }
- };
+.directive('uibProgressbar', function() {
+ return {
+ replace: true,
+ transclude: true,
+ controller: 'UibProgressController',
+ scope: {
+ value: '=',
+ maxParam: '=?max',
+ type: '@'
+ },
+ templateUrl: 'uib/template/progressbar/progressbar.html',
+ link: function(scope, element, attrs, progressCtrl) {
+ progressCtrl.addBar(scope, angular.element(element.children()[0]), {title: attrs.title});
+ }
+ };
});
+
angular.module('ui.bootstrap.rating', [])
-.constant('ratingConfig', {
+.constant('uibRatingConfig', {
max: 5,
stateOn: null,
- stateOff: null
+ stateOff: null,
+ enableReset: true,
+ titles : ['one', 'two', 'three', 'four', 'five']
})
-.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) {
- var ngModelCtrl = { $setViewValue: angular.noop };
+.controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) {
+ var ngModelCtrl = { $setViewValue: angular.noop },
+ self = this;
this.init = function(ngModelCtrl_) {
ngModelCtrl = ngModelCtrl_;
ngModelCtrl.$render = this.render;
+ ngModelCtrl.$formatters.push(function(value) {
+ if (angular.isNumber(value) && value << 0 !== value) {
+ value = Math.round(value);
+ }
+
+ return value;
+ });
+
this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
+ this.enableReset = angular.isDefined($attrs.enableReset) ?
+ $scope.$parent.$eval($attrs.enableReset) : ratingConfig.enableReset;
+ var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles;
+ this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ?
+ tmpTitles : ratingConfig.titles;
- var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) :
- new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max );
+ var ratingStates = angular.isDefined($attrs.ratingStates) ?
+ $scope.$parent.$eval($attrs.ratingStates) :
+ new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max);
$scope.range = this.buildTemplateObjects(ratingStates);
};
this.buildTemplateObjects = function(states) {
for (var i = 0, n = states.length; i < n; i++) {
- states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]);
+ states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]);
}
return states;
};
+ this.getTitle = function(index) {
+ if (index >= this.titles.length) {
+ return index + 1;
+ }
+
+ return this.titles[index];
+ };
+
$scope.rate = function(value) {
- if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) {
- ngModelCtrl.$setViewValue(value);
+ if (!$scope.readonly && value >= 0 && value <= $scope.range.length) {
+ var newViewValue = self.enableReset && ngModelCtrl.$viewValue === value ? 0 : value;
+ ngModelCtrl.$setViewValue(newViewValue);
ngModelCtrl.$render();
}
};
$scope.enter = function(value) {
- if ( !$scope.readonly ) {
+ if (!$scope.readonly) {
$scope.value = value;
}
$scope.onHover({value: value});
@@ -2859,222 +5404,169 @@ angular.module('ui.bootstrap.rating', [])
if (/(37|38|39|40)/.test(evt.which)) {
evt.preventDefault();
evt.stopPropagation();
- $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) );
+ $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1));
}
};
this.render = function() {
$scope.value = ngModelCtrl.$viewValue;
+ $scope.title = self.getTitle($scope.value - 1);
};
}])
-.directive('rating', function() {
+.directive('uibRating', function() {
return {
- restrict: 'EA',
- require: ['rating', 'ngModel'],
+ require: ['uibRating', 'ngModel'],
scope: {
- readonly: '=?',
+ readonly: '=?readOnly',
onHover: '&',
onLeave: '&'
},
- controller: 'RatingController',
- templateUrl: 'template/rating/rating.html',
+ controller: 'UibRatingController',
+ templateUrl: 'uib/template/rating/rating.html',
replace: true,
link: function(scope, element, attrs, ctrls) {
var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
-
- if ( ngModelCtrl ) {
- ratingCtrl.init( ngModelCtrl );
- }
+ ratingCtrl.init(ngModelCtrl);
}
};
});
-/**
- * @ngdoc overview
- * @name ui.bootstrap.tabs
- *
- * @description
- * AngularJS version of the tabs directive.
- */
-
angular.module('ui.bootstrap.tabs', [])
-.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
+.controller('UibTabsetController', ['$scope', function ($scope) {
var ctrl = this,
- tabs = ctrl.tabs = $scope.tabs = [];
+ oldIndex;
+ ctrl.tabs = [];
- ctrl.select = function(selectedTab) {
- angular.forEach(tabs, function(tab) {
- if (tab.active && tab !== selectedTab) {
- tab.active = false;
- tab.onDeselect();
+ ctrl.select = function(index, evt) {
+ if (!destroyed) {
+ var previousIndex = findTabIndex(oldIndex);
+ var previousSelected = ctrl.tabs[previousIndex];
+ if (previousSelected) {
+ previousSelected.tab.onDeselect({
+ $event: evt
+ });
+ if (evt && evt.isDefaultPrevented()) {
+ return;
+ }
+ previousSelected.tab.active = false;
}
- });
- selectedTab.active = true;
- selectedTab.onSelect();
+
+ var selected = ctrl.tabs[index];
+ if (selected) {
+ selected.tab.onSelect({
+ $event: evt
+ });
+ selected.tab.active = true;
+ ctrl.active = selected.index;
+ oldIndex = selected.index;
+ } else if (!selected && angular.isNumber(oldIndex)) {
+ ctrl.active = null;
+ oldIndex = null;
+ }
+ }
};
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 = true;
- } else if (tab.active) {
- ctrl.select(tab);
+ 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
- if (tab.active && tabs.length > 1) {
- //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;
+ for (var i = 0; i < ctrl.tabs.length; i++) {
+ if (ctrl.tabs[i].tab === tab) {
+ index = i;
+ break;
+ }
}
- tabs.splice(index, 1);
+
+ if (ctrl.tabs[index].index === ctrl.active) {
+ var newActiveTabIndex = index === ctrl.tabs.length - 1 ?
+ index - 1 : index + 1 % ctrl.tabs.length;
+ ctrl.select(newActiveTabIndex);
+ }
+
+ 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;
+ }
+ }
+ }
}])
-/**
- * @ngdoc directive
- * @name ui.bootstrap.tabs.directive:tabset
- * @restrict EA
- *
- * @description
- * Tabset is the outer container for the tabs directive
- *
- * @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
- * @param {boolean=} justified Whether or not to use justified styling for the tabs.
- *
- * @example
-
-
-
- First Content!
- Second Content!
-
-
-
- First Vertical Content!
- Second Vertical Content!
-
-
- First Justified Content!
- Second Justified Content!
-
-
-
- */
-.directive('tabset', function() {
+.directive('uibTabset', function() {
return {
- restrict: 'EA',
transclude: true,
replace: true,
- scope: {
+ scope: {},
+ bindToController: {
+ active: '=?',
type: '@'
},
- controller: 'TabsetController',
- templateUrl: 'template/tabs/tabset.html',
+ controller: 'UibTabsetController',
+ controllerAs: 'tabset',
+ templateUrl: function(element, attrs) {
+ return attrs.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;
+ if (angular.isUndefined(attrs.active)) {
+ scope.active = 0;
+ }
}
};
})
-/**
- * @ngdoc directive
- * @name ui.bootstrap.tabs.directive:tab
- * @restrict EA
- *
- * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
- * @param {string=} select An expression to evaluate when the tab is selected.
- * @param {boolean=} active A binding, telling whether or not this tab is selected.
- * @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
- *
- * @description
- * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
- *
- * @example
-
-
-
-
-
-
-
- First Tab
-
- Alert me!
- Second Tab, with alert callback and html heading!
-
-
- {{item.content}}
-
-
-
-
-
- function TabsDemoCtrl($scope) {
- $scope.items = [
- { title:"Dynamic Title 1", content:"Dynamic Item 0" },
- { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
- ];
-
- $scope.alertMe = function() {
- setTimeout(function() {
- alert("You've selected the alert tab!");
- });
- };
- };
-
-
- */
-
-/**
- * @ngdoc directive
- * @name ui.bootstrap.tabs.directive:tabHeading
- * @restrict EA
- *
- * @description
- * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
- *
- * @example
-
-
-
-
- HTML in my titles?!
- And some content, too!
-
-
- Icon heading?!?
- That's right.
-
-
-
-
- */
-.directive('tab', ['$parse', function($parse) {
+.directive('uibTab', ['$parse', function($parse) {
return {
- require: '^tabset',
- restrict: 'EA',
+ require: '^uibTabset',
replace: true,
- templateUrl: 'template/tabs/tab.html',
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/tabs/tab.html';
+ },
transclude: true,
scope: {
- active: '=?',
heading: '@',
+ index: '=?',
+ classes: '@?',
onSelect: '&select', //This callback is called in contentHeadingTransclude
//once it inserts the tab's content into the dom
onDeselect: '&deselect'
@@ -3082,45 +5574,58 @@ angular.module('ui.bootstrap.tabs', [])
controller: function() {
//Empty controller so other directives can require being 'under' a tab
},
- compile: function(elm, attrs, transclude) {
- return function postLink(scope, elm, attrs, tabsetCtrl) {
- scope.$watch('active', function(active) {
- if (active) {
- tabsetCtrl.select(scope);
- }
+ controllerAs: 'tab',
+ link: function(scope, elm, attrs, tabsetCtrl, transclude) {
+ scope.disabled = false;
+ if (attrs.disable) {
+ scope.$parent.$watch($parse(attrs.disable), function(value) {
+ scope.disabled = !! value;
});
+ }
- scope.disabled = false;
- if ( attrs.disabled ) {
- scope.$parent.$watch($parse(attrs.disabled), function(value) {
- scope.disabled = !! value;
- });
+ if (angular.isUndefined(attrs.index)) {
+ if (tabsetCtrl.tabs && tabsetCtrl.tabs.length) {
+ scope.index = Math.max.apply(null, tabsetCtrl.tabs.map(function(t) { return t.index; })) + 1;
+ } else {
+ scope.index = 0;
}
+ }
- scope.select = function() {
- if ( !scope.disabled ) {
- scope.active = true;
+ if (angular.isUndefined(attrs.classes)) {
+ scope.classes = '';
+ }
+
+ scope.select = function(evt) {
+ if (!scope.disabled) {
+ var index;
+ for (var i = 0; i < tabsetCtrl.tabs.length; i++) {
+ if (tabsetCtrl.tabs[i].tab === scope) {
+ index = i;
+ break;
+ }
}
- };
- tabsetCtrl.addTab(scope);
- scope.$on('$destroy', function() {
- tabsetCtrl.removeTab(scope);
- });
-
- //We need to transclude later, once the content container is ready.
- //when this link happens, we're inside a tab heading.
- scope.$transcludeFn = transclude;
+ tabsetCtrl.select(index, evt);
+ }
};
+
+ tabsetCtrl.addTab(scope);
+ scope.$on('$destroy', function() {
+ tabsetCtrl.removeTab(scope);
+ });
+
+ //We need to transclude later, once the content container is ready.
+ //when this link happens, we're inside a tab heading.
+ scope.$transcludeFn = transclude;
}
};
}])
-.directive('tabHeadingTransclude', [function() {
+.directive('uibTabHeadingTransclude', function() {
return {
restrict: 'A',
- require: '^tab',
- link: function(scope, elm, attrs, tabCtrl) {
+ require: '^uibTab',
+ link: function(scope, elm) {
scope.$watch('headingElement', function updateHeadingElement(heading) {
if (heading) {
elm.html('');
@@ -3129,14 +5634,14 @@ angular.module('ui.bootstrap.tabs', [])
});
}
};
-}])
+})
-.directive('tabContentTransclude', function() {
+.directive('uibTabContentTransclude', function() {
return {
restrict: 'A',
- require: '^tabset',
+ require: '^uibTabset',
link: function(scope, elm, attrs) {
- var tab = scope.$eval(attrs.tabContentTransclude);
+ 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.
@@ -3152,96 +5657,197 @@ angular.module('ui.bootstrap.tabs', [])
});
}
};
+
function isTabHeading(node) {
- return node.tagName && (
- node.hasAttribute('tab-heading') ||
- node.hasAttribute('data-tab-heading') ||
- node.tagName.toLowerCase() === 'tab-heading' ||
- node.tagName.toLowerCase() === 'data-tab-heading'
+ return node.tagName && (
+ node.hasAttribute('uib-tab-heading') ||
+ node.hasAttribute('data-uib-tab-heading') ||
+ node.hasAttribute('x-uib-tab-heading') ||
+ node.tagName.toLowerCase() === 'uib-tab-heading' ||
+ node.tagName.toLowerCase() === 'data-uib-tab-heading' ||
+ node.tagName.toLowerCase() === 'x-uib-tab-heading' ||
+ node.tagName.toLowerCase() === 'uib:tab-heading'
);
}
-})
-
-;
+});
angular.module('ui.bootstrap.timepicker', [])
-.constant('timepickerConfig', {
+.constant('uibTimepickerConfig', {
hourStep: 1,
minuteStep: 1,
+ secondStep: 1,
showMeridian: true,
+ showSeconds: false,
meridians: null,
readonlyInput: false,
- mousewheel: true
+ mousewheel: true,
+ arrowkeys: true,
+ showSpinners: true,
+ templateUrl: 'uib/template/timepicker/timepicker.html'
})
-.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) {
+.controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) {
var selected = new Date(),
- ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
- meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS;
+ watchers = [],
+ ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
+ meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS,
+ padHours = angular.isDefined($attrs.padHours) ? $scope.$parent.$eval($attrs.padHours) : true;
- this.init = function( ngModelCtrl_, inputs ) {
+ $scope.tabindex = angular.isDefined($attrs.tabindex) ? $attrs.tabindex : 0;
+ $element.removeAttr('tabindex');
+
+ this.init = function(ngModelCtrl_, inputs) {
ngModelCtrl = ngModelCtrl_;
ngModelCtrl.$render = this.render;
+ ngModelCtrl.$formatters.unshift(function(modelValue) {
+ return modelValue ? new Date(modelValue) : null;
+ });
+
var hoursInputEl = inputs.eq(0),
- minutesInputEl = inputs.eq(1);
+ minutesInputEl = inputs.eq(1),
+ secondsInputEl = inputs.eq(2);
var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel;
- if ( mousewheel ) {
- this.setupMousewheelEvents( hoursInputEl, minutesInputEl );
+
+ if (mousewheel) {
+ this.setupMousewheelEvents(hoursInputEl, minutesInputEl, secondsInputEl);
+ }
+
+ var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys;
+ if (arrowkeys) {
+ this.setupArrowkeyEvents(hoursInputEl, minutesInputEl, secondsInputEl);
}
$scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput;
- this.setupInputEvents( hoursInputEl, minutesInputEl );
+ this.setupInputEvents(hoursInputEl, minutesInputEl, secondsInputEl);
};
var hourStep = timepickerConfig.hourStep;
if ($attrs.hourStep) {
- $scope.$parent.$watch($parse($attrs.hourStep), function(value) {
- hourStep = parseInt(value, 10);
- });
+ watchers.push($scope.$parent.$watch($parse($attrs.hourStep), function(value) {
+ hourStep = +value;
+ }));
}
var minuteStep = timepickerConfig.minuteStep;
if ($attrs.minuteStep) {
- $scope.$parent.$watch($parse($attrs.minuteStep), function(value) {
- minuteStep = parseInt(value, 10);
- });
+ watchers.push($scope.$parent.$watch($parse($attrs.minuteStep), function(value) {
+ minuteStep = +value;
+ }));
+ }
+
+ var min;
+ watchers.push($scope.$parent.$watch($parse($attrs.min), function(value) {
+ var dt = new Date(value);
+ min = isNaN(dt) ? undefined : dt;
+ }));
+
+ var max;
+ watchers.push($scope.$parent.$watch($parse($attrs.max), function(value) {
+ var dt = new Date(value);
+ max = isNaN(dt) ? undefined : dt;
+ }));
+
+ var disabled = false;
+ if ($attrs.ngDisabled) {
+ watchers.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(value) {
+ disabled = value;
+ }));
+ }
+
+ $scope.noIncrementHours = function() {
+ var incrementedSelected = addMinutes(selected, hourStep * 60);
+ return disabled || incrementedSelected > max ||
+ incrementedSelected < selected && incrementedSelected < min;
+ };
+
+ $scope.noDecrementHours = function() {
+ var decrementedSelected = addMinutes(selected, -hourStep * 60);
+ return disabled || decrementedSelected < min ||
+ decrementedSelected > selected && decrementedSelected > max;
+ };
+
+ $scope.noIncrementMinutes = function() {
+ var incrementedSelected = addMinutes(selected, minuteStep);
+ return disabled || incrementedSelected > max ||
+ incrementedSelected < selected && incrementedSelected < min;
+ };
+
+ $scope.noDecrementMinutes = function() {
+ var decrementedSelected = addMinutes(selected, -minuteStep);
+ return disabled || decrementedSelected < min ||
+ decrementedSelected > selected && decrementedSelected > max;
+ };
+
+ $scope.noIncrementSeconds = function() {
+ var incrementedSelected = addSeconds(selected, secondStep);
+ return disabled || incrementedSelected > max ||
+ incrementedSelected < selected && incrementedSelected < min;
+ };
+
+ $scope.noDecrementSeconds = function() {
+ var decrementedSelected = addSeconds(selected, -secondStep);
+ return disabled || decrementedSelected < min ||
+ decrementedSelected > selected && decrementedSelected > max;
+ };
+
+ $scope.noToggleMeridian = function() {
+ if (selected.getHours() < 12) {
+ return disabled || addMinutes(selected, 12 * 60) > max;
+ }
+
+ return disabled || addMinutes(selected, -12 * 60) < min;
+ };
+
+ var secondStep = timepickerConfig.secondStep;
+ if ($attrs.secondStep) {
+ watchers.push($scope.$parent.$watch($parse($attrs.secondStep), function(value) {
+ secondStep = +value;
+ }));
+ }
+
+ $scope.showSeconds = timepickerConfig.showSeconds;
+ if ($attrs.showSeconds) {
+ watchers.push($scope.$parent.$watch($parse($attrs.showSeconds), function(value) {
+ $scope.showSeconds = !!value;
+ }));
}
// 12H / 24H mode
$scope.showMeridian = timepickerConfig.showMeridian;
if ($attrs.showMeridian) {
- $scope.$parent.$watch($parse($attrs.showMeridian), function(value) {
+ watchers.push($scope.$parent.$watch($parse($attrs.showMeridian), function(value) {
$scope.showMeridian = !!value;
- if ( ngModelCtrl.$error.time ) {
+ if (ngModelCtrl.$error.time) {
// Evaluate from template
var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
- if (angular.isDefined( hours ) && angular.isDefined( minutes )) {
- selected.setHours( hours );
+ if (angular.isDefined(hours) && angular.isDefined(minutes)) {
+ selected.setHours(hours);
refresh();
}
} else {
updateTemplate();
}
- });
+ }));
}
// Get $scope.hours in 24H mode if valid
- function getHoursFromTemplate ( ) {
- var hours = parseInt( $scope.hours, 10 );
- var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
- if ( !valid ) {
+ function getHoursFromTemplate() {
+ var hours = +$scope.hours;
+ var valid = $scope.showMeridian ? hours > 0 && hours < 13 :
+ hours >= 0 && hours < 24;
+ if (!valid || $scope.hours === '') {
return undefined;
}
- if ( $scope.showMeridian ) {
- if ( hours === 12 ) {
+ if ($scope.showMeridian) {
+ if (hours === 12) {
hours = 0;
}
- if ( $scope.meridian === meridians[1] ) {
+ if ($scope.meridian === meridians[1]) {
hours = hours + 12;
}
}
@@ -3249,89 +5855,213 @@ angular.module('ui.bootstrap.timepicker', [])
}
function getMinutesFromTemplate() {
- var minutes = parseInt($scope.minutes, 10);
- return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined;
+ var minutes = +$scope.minutes;
+ var valid = minutes >= 0 && minutes < 60;
+ if (!valid || $scope.minutes === '') {
+ return undefined;
+ }
+ return minutes;
}
- function pad( value ) {
- return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value;
+ function getSecondsFromTemplate() {
+ var seconds = +$scope.seconds;
+ return seconds >= 0 && seconds < 60 ? seconds : undefined;
+ }
+
+ function pad(value, noPad) {
+ if (value === null) {
+ return '';
+ }
+
+ return angular.isDefined(value) && value.toString().length < 2 && !noPad ?
+ '0' + value : value.toString();
}
// Respond on mousewheel spin
- this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) {
+ this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) {
var isScrollingUp = function(e) {
if (e.originalEvent) {
e = e.originalEvent;
}
//pick correct delta variable depending on event
- var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
- return (e.detail || delta > 0);
+ var delta = e.wheelDelta ? e.wheelDelta : -e.deltaY;
+ return e.detail || delta > 0;
};
hoursInputEl.bind('mousewheel wheel', function(e) {
- $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() );
+ if (!disabled) {
+ $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours());
+ }
e.preventDefault();
});
minutesInputEl.bind('mousewheel wheel', function(e) {
- $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() );
+ if (!disabled) {
+ $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes());
+ }
e.preventDefault();
});
+ secondsInputEl.bind('mousewheel wheel', function(e) {
+ if (!disabled) {
+ $scope.$apply(isScrollingUp(e) ? $scope.incrementSeconds() : $scope.decrementSeconds());
+ }
+ e.preventDefault();
+ });
};
- this.setupInputEvents = function( hoursInputEl, minutesInputEl ) {
- if ( $scope.readonlyInput ) {
+ // Respond on up/down arrowkeys
+ this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) {
+ hoursInputEl.bind('keydown', function(e) {
+ if (!disabled) {
+ if (e.which === 38) { // up
+ e.preventDefault();
+ $scope.incrementHours();
+ $scope.$apply();
+ } else if (e.which === 40) { // down
+ e.preventDefault();
+ $scope.decrementHours();
+ $scope.$apply();
+ }
+ }
+ });
+
+ minutesInputEl.bind('keydown', function(e) {
+ if (!disabled) {
+ if (e.which === 38) { // up
+ e.preventDefault();
+ $scope.incrementMinutes();
+ $scope.$apply();
+ } else if (e.which === 40) { // down
+ e.preventDefault();
+ $scope.decrementMinutes();
+ $scope.$apply();
+ }
+ }
+ });
+
+ secondsInputEl.bind('keydown', function(e) {
+ if (!disabled) {
+ if (e.which === 38) { // up
+ e.preventDefault();
+ $scope.incrementSeconds();
+ $scope.$apply();
+ } else if (e.which === 40) { // down
+ e.preventDefault();
+ $scope.decrementSeconds();
+ $scope.$apply();
+ }
+ }
+ });
+ };
+
+ this.setupInputEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) {
+ if ($scope.readonlyInput) {
$scope.updateHours = angular.noop;
$scope.updateMinutes = angular.noop;
+ $scope.updateSeconds = angular.noop;
return;
}
- var invalidate = function(invalidHours, invalidMinutes) {
- ngModelCtrl.$setViewValue( null );
+ var invalidate = function(invalidHours, invalidMinutes, invalidSeconds) {
+ ngModelCtrl.$setViewValue(null);
ngModelCtrl.$setValidity('time', false);
if (angular.isDefined(invalidHours)) {
$scope.invalidHours = invalidHours;
}
+
if (angular.isDefined(invalidMinutes)) {
$scope.invalidMinutes = invalidMinutes;
}
+
+ if (angular.isDefined(invalidSeconds)) {
+ $scope.invalidSeconds = invalidSeconds;
+ }
};
$scope.updateHours = function() {
- var hours = getHoursFromTemplate();
+ var hours = getHoursFromTemplate(),
+ minutes = getMinutesFromTemplate();
- if ( angular.isDefined(hours) ) {
- selected.setHours( hours );
- refresh( 'h' );
+ ngModelCtrl.$setDirty();
+
+ if (angular.isDefined(hours) && angular.isDefined(minutes)) {
+ selected.setHours(hours);
+ selected.setMinutes(minutes);
+ if (selected < min || selected > max) {
+ invalidate(true);
+ } else {
+ refresh('h');
+ }
} else {
invalidate(true);
}
};
hoursInputEl.bind('blur', function(e) {
- if ( !$scope.invalidHours && $scope.hours < 10) {
- $scope.$apply( function() {
- $scope.hours = pad( $scope.hours );
+ ngModelCtrl.$setTouched();
+ if (modelIsEmpty()) {
+ makeValid();
+ } else if ($scope.hours === null || $scope.hours === '') {
+ invalidate(true);
+ } else if (!$scope.invalidHours && $scope.hours < 10) {
+ $scope.$apply(function() {
+ $scope.hours = pad($scope.hours, !padHours);
});
}
});
$scope.updateMinutes = function() {
- var minutes = getMinutesFromTemplate();
+ var minutes = getMinutesFromTemplate(),
+ hours = getHoursFromTemplate();
- if ( angular.isDefined(minutes) ) {
- selected.setMinutes( minutes );
- refresh( 'm' );
+ ngModelCtrl.$setDirty();
+
+ if (angular.isDefined(minutes) && angular.isDefined(hours)) {
+ selected.setHours(hours);
+ selected.setMinutes(minutes);
+ if (selected < min || selected > max) {
+ invalidate(undefined, true);
+ } else {
+ refresh('m');
+ }
} else {
invalidate(undefined, true);
}
};
minutesInputEl.bind('blur', function(e) {
- if ( !$scope.invalidMinutes && $scope.minutes < 10 ) {
+ ngModelCtrl.$setTouched();
+ if (modelIsEmpty()) {
+ makeValid();
+ } else if ($scope.minutes === null) {
+ invalidate(undefined, true);
+ } else if (!$scope.invalidMinutes && $scope.minutes < 10) {
+ $scope.$apply(function() {
+ $scope.minutes = pad($scope.minutes);
+ });
+ }
+ });
+
+ $scope.updateSeconds = function() {
+ var seconds = getSecondsFromTemplate();
+
+ ngModelCtrl.$setDirty();
+
+ if (angular.isDefined(seconds)) {
+ selected.setSeconds(seconds);
+ refresh('s');
+ } else {
+ invalidate(undefined, undefined, true);
+ }
+ };
+
+ secondsInputEl.bind('blur', function(e) {
+ if (modelIsEmpty()) {
+ makeValid();
+ } else if (!$scope.invalidSeconds && $scope.seconds < 10) {
$scope.$apply( function() {
- $scope.minutes = pad( $scope.minutes );
+ $scope.seconds = pad($scope.seconds);
});
}
});
@@ -3339,489 +6069,860 @@ angular.module('ui.bootstrap.timepicker', [])
};
this.render = function() {
- var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null;
+ var date = ngModelCtrl.$viewValue;
- if ( isNaN(date) ) {
+ if (isNaN(date)) {
ngModelCtrl.$setValidity('time', false);
$log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
} else {
- if ( date ) {
+ if (date) {
selected = date;
}
- makeValid();
+
+ if (selected < min || selected > max) {
+ ngModelCtrl.$setValidity('time', false);
+ $scope.invalidHours = true;
+ $scope.invalidMinutes = true;
+ } else {
+ makeValid();
+ }
updateTemplate();
}
};
// Call internally when we know that model is valid.
- function refresh( keyboardChange ) {
+ function refresh(keyboardChange) {
makeValid();
- ngModelCtrl.$setViewValue( new Date(selected) );
- updateTemplate( keyboardChange );
+ ngModelCtrl.$setViewValue(new Date(selected));
+ updateTemplate(keyboardChange);
}
function makeValid() {
ngModelCtrl.$setValidity('time', true);
$scope.invalidHours = false;
$scope.invalidMinutes = false;
+ $scope.invalidSeconds = false;
}
- function updateTemplate( keyboardChange ) {
- var hours = selected.getHours(), minutes = selected.getMinutes();
+ function updateTemplate(keyboardChange) {
+ if (!ngModelCtrl.$modelValue) {
+ $scope.hours = null;
+ $scope.minutes = null;
+ $scope.seconds = null;
+ $scope.meridian = meridians[0];
+ } else {
+ var hours = selected.getHours(),
+ minutes = selected.getMinutes(),
+ seconds = selected.getSeconds();
- if ( $scope.showMeridian ) {
- hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system
+ if ($scope.showMeridian) {
+ hours = hours === 0 || hours === 12 ? 12 : hours % 12; // Convert 24 to 12 hour system
+ }
+
+ $scope.hours = keyboardChange === 'h' ? hours : pad(hours, !padHours);
+ if (keyboardChange !== 'm') {
+ $scope.minutes = pad(minutes);
+ }
+ $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
+
+ if (keyboardChange !== 's') {
+ $scope.seconds = pad(seconds);
+ }
+ $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
}
-
- $scope.hours = keyboardChange === 'h' ? hours : pad(hours);
- $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes);
- $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
}
- function addMinutes( minutes ) {
- var dt = new Date( selected.getTime() + minutes * 60000 );
- selected.setHours( dt.getHours(), dt.getMinutes() );
+ function addSecondsToSelected(seconds) {
+ selected = addSeconds(selected, seconds);
refresh();
}
+ function addMinutes(selected, minutes) {
+ return addSeconds(selected, minutes*60);
+ }
+
+ function addSeconds(date, seconds) {
+ var dt = new Date(date.getTime() + seconds * 1000);
+ var newDate = new Date(date);
+ newDate.setHours(dt.getHours(), dt.getMinutes(), dt.getSeconds());
+ return newDate;
+ }
+
+ function modelIsEmpty() {
+ return ($scope.hours === null || $scope.hours === '') &&
+ ($scope.minutes === null || $scope.minutes === '') &&
+ (!$scope.showSeconds || $scope.showSeconds && ($scope.seconds === null || $scope.seconds === ''));
+ }
+
+ $scope.showSpinners = angular.isDefined($attrs.showSpinners) ?
+ $scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners;
+
$scope.incrementHours = function() {
- addMinutes( hourStep * 60 );
+ if (!$scope.noIncrementHours()) {
+ addSecondsToSelected(hourStep * 60 * 60);
+ }
};
+
$scope.decrementHours = function() {
- addMinutes( - hourStep * 60 );
+ if (!$scope.noDecrementHours()) {
+ addSecondsToSelected(-hourStep * 60 * 60);
+ }
};
+
$scope.incrementMinutes = function() {
- addMinutes( minuteStep );
+ if (!$scope.noIncrementMinutes()) {
+ addSecondsToSelected(minuteStep * 60);
+ }
};
+
$scope.decrementMinutes = function() {
- addMinutes( - minuteStep );
+ if (!$scope.noDecrementMinutes()) {
+ addSecondsToSelected(-minuteStep * 60);
+ }
};
+
+ $scope.incrementSeconds = function() {
+ if (!$scope.noIncrementSeconds()) {
+ addSecondsToSelected(secondStep);
+ }
+ };
+
+ $scope.decrementSeconds = function() {
+ if (!$scope.noDecrementSeconds()) {
+ addSecondsToSelected(-secondStep);
+ }
+ };
+
$scope.toggleMeridian = function() {
- addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) );
- };
-}])
+ var minutes = getMinutesFromTemplate(),
+ hours = getHoursFromTemplate();
-.directive('timepicker', function () {
- return {
- restrict: 'EA',
- require: ['timepicker', '?^ngModel'],
- controller:'TimepickerController',
- replace: true,
- scope: {},
- templateUrl: 'template/timepicker/timepicker.html',
- link: function(scope, element, attrs, ctrls) {
- var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
-
- if ( ngModelCtrl ) {
- timepickerCtrl.init( ngModelCtrl, element.find('input') );
+ if (!$scope.noToggleMeridian()) {
+ if (angular.isDefined(minutes) && angular.isDefined(hours)) {
+ addSecondsToSelected(12 * 60 * (selected.getHours() < 12 ? 60 : -60));
+ } else {
+ $scope.meridian = $scope.meridian === meridians[0] ? meridians[1] : meridians[0];
}
}
};
-});
-angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])
+ $scope.blur = function() {
+ ngModelCtrl.$setTouched();
+ };
+
+ $scope.$on('$destroy', function() {
+ while (watchers.length) {
+ watchers.shift()();
+ }
+ });
+}])
+
+.directive('uibTimepicker', ['uibTimepickerConfig', function(uibTimepickerConfig) {
+ return {
+ require: ['uibTimepicker', '?^ngModel'],
+ controller: 'UibTimepickerController',
+ controllerAs: 'timepicker',
+ replace: true,
+ scope: {},
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || uibTimepickerConfig.templateUrl;
+ },
+ link: function(scope, element, attrs, ctrls) {
+ var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+ if (ngModelCtrl) {
+ timepickerCtrl.init(ngModelCtrl, element.find('input'));
+ }
+ }
+ };
+}]);
+
+angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap.position'])
/**
* A helper service that can parse typeahead's syntax (string provided by users)
* Extracted to a separate service for ease of unit testing
*/
- .factory('typeaheadParser', ['$parse', function ($parse) {
-
- // 00000111000000000000022200000000000000003333333333333330000000000044000
- var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;
-
- return {
- parse:function (input) {
-
- var match = input.match(TYPEAHEAD_REGEXP);
- if (!match) {
- throw new Error(
- 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
- ' but got "' + input + '".');
- }
-
- return {
- itemName:match[3],
- source:$parse(match[4]),
- viewMapper:$parse(match[2] || match[1]),
- modelMapper:$parse(match[1])
- };
- }
- };
-}])
-
- .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser',
- function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) {
-
- var HOT_KEYS = [9, 13, 27, 38, 40];
-
- return {
- require:'ngModel',
- link:function (originalScope, element, attrs, modelCtrl) {
-
- //SUPPORTED ATTRIBUTES (OPTIONS)
-
- //minimal no of characters that needs to be entered before typeahead kicks-in
- var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;
-
- //minimal wait time after last character typed before typehead kicks-in
- var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
-
- //should it restrict model values to the ones selected from the popup only?
- var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
-
- //binding to a variable that indicates if matches are being retrieved asynchronously
- var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
-
- //a callback executed when a match is selected
- var onSelectCallback = $parse(attrs.typeaheadOnSelect);
-
- var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
-
- var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
-
- //INTERNAL VARIABLES
-
- //model setter executed upon match selection
- var $setModelValue = $parse(attrs.ngModel).assign;
-
- //expressions used by typeahead
- var parserResult = typeaheadParser.parse(attrs.typeahead);
-
- var hasFocus;
-
- //create a child scope for the typeahead directive so we are not polluting original scope
- //with typeahead-specific data (matches, query etc.)
- var scope = originalScope.$new();
- originalScope.$on('$destroy', function(){
- scope.$destroy();
- });
-
- // WAI-ARIA
- var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
- element.attr({
- 'aria-autocomplete': 'list',
- 'aria-expanded': false,
- 'aria-owns': popupId
- });
-
- //pop-up element used to display matches
- var popUpEl = angular.element('');
- popUpEl.attr({
- id: popupId,
- matches: 'matches',
- active: 'activeIdx',
- select: 'select(activeIdx)',
- query: 'query',
- position: 'position'
- });
- //custom item template
- if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
- popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
- }
-
- var resetMatches = function() {
- scope.matches = [];
- scope.activeIdx = -1;
- element.attr('aria-expanded', false);
- };
-
- var getMatchId = function(index) {
- return popupId + '-option-' + index;
- };
-
- // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
- // This attribute is added or removed automatically when the `activeIdx` changes.
- scope.$watch('activeIdx', function(index) {
- if (index < 0) {
- element.removeAttr('aria-activedescendant');
- } else {
- element.attr('aria-activedescendant', getMatchId(index));
+ .factory('uibTypeaheadParser', ['$parse', function($parse) {
+ // 00000111000000000000022200000000000000003333333333333330000000000044000
+ var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
+ return {
+ parse: function(input) {
+ var match = input.match(TYPEAHEAD_REGEXP);
+ if (!match) {
+ throw new Error(
+ 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
+ ' but got "' + input + '".');
}
+
+ return {
+ itemName: match[3],
+ source: $parse(match[4]),
+ viewMapper: $parse(match[2] || match[1]),
+ modelMapper: $parse(match[1])
+ };
+ }
+ };
+ }])
+
+ .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser',
+ function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) {
+ var HOT_KEYS = [9, 13, 27, 38, 40];
+ var eventDebounceTime = 200;
+ var modelCtrl, ngModelOptions;
+ //SUPPORTED ATTRIBUTES (OPTIONS)
+
+ //minimal no of characters that needs to be entered before typeahead kicks-in
+ var minLength = originalScope.$eval(attrs.typeaheadMinLength);
+ if (!minLength && minLength !== 0) {
+ minLength = 1;
+ }
+
+ originalScope.$watch(attrs.typeaheadMinLength, function (newVal) {
+ minLength = !newVal && newVal !== 0 ? 1 : newVal;
+ });
+
+ //minimal wait time after last character typed before typeahead kicks-in
+ var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
+
+ //should it restrict model values to the ones selected from the popup only?
+ var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
+ originalScope.$watch(attrs.typeaheadEditable, function (newVal) {
+ isEditable = newVal !== false;
+ });
+
+ //binding to a variable that indicates if matches are being retrieved asynchronously
+ var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
+
+ //a callback executed when a match is selected
+ var onSelectCallback = $parse(attrs.typeaheadOnSelect);
+
+ //should it select highlighted popup value when losing focus?
+ var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
+
+ //binding to a variable that indicates if there were no results after the query is completed
+ var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
+
+ var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
+
+ var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
+
+ var appendTo = attrs.typeaheadAppendTo ?
+ originalScope.$eval(attrs.typeaheadAppendTo) : null;
+
+ var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
+
+ //If input matches an item of the list exactly, select it automatically
+ var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
+
+ //binding to a variable that indicates if dropdown is open
+ var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop;
+
+ var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false;
+
+ //INTERNAL VARIABLES
+
+ //model setter executed upon match selection
+ var parsedModel = $parse(attrs.ngModel);
+ var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
+ var $setModelValue = function(scope, newValue) {
+ if (angular.isFunction(parsedModel(originalScope)) &&
+ ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) {
+ return invokeModelSetter(scope, {$$$p: newValue});
+ }
+
+ return parsedModel.assign(scope, newValue);
+ };
+
+ //expressions used by typeahead
+ var parserResult = typeaheadParser.parse(attrs.uibTypeahead);
+
+ var hasFocus;
+
+ //Used to avoid bug in iOS webview where iOS keyboard does not fire
+ //mousedown & mouseup events
+ //Issue #3699
+ var selected;
+
+ //create a child scope for the typeahead directive so we are not polluting original scope
+ //with typeahead-specific data (matches, query etc.)
+ var scope = originalScope.$new();
+ var offDestroy = originalScope.$on('$destroy', function() {
+ scope.$destroy();
+ });
+ scope.$on('$destroy', offDestroy);
+
+ // WAI-ARIA
+ var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
+ element.attr({
+ 'aria-autocomplete': 'list',
+ 'aria-expanded': false,
+ 'aria-owns': popupId
+ });
+
+ var inputsContainer, hintInputElem;
+ //add read-only input to show hint
+ if (showHint) {
+ inputsContainer = angular.element('');
+ inputsContainer.css('position', 'relative');
+ element.after(inputsContainer);
+ hintInputElem = element.clone();
+ hintInputElem.attr('placeholder', '');
+ hintInputElem.attr('tabindex', '-1');
+ hintInputElem.val('');
+ hintInputElem.css({
+ 'position': 'absolute',
+ 'top': '0px',
+ 'left': '0px',
+ 'border-color': 'transparent',
+ 'box-shadow': 'none',
+ 'opacity': 1,
+ 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)',
+ 'color': '#999'
});
+ element.css({
+ 'position': 'relative',
+ 'vertical-align': 'top',
+ 'background-color': 'transparent'
+ });
+ inputsContainer.append(hintInputElem);
+ hintInputElem.after(element);
+ }
- var getMatchesAsync = function(inputValue) {
+ //pop-up element used to display matches
+ var popUpEl = angular.element('');
+ popUpEl.attr({
+ id: popupId,
+ matches: 'matches',
+ active: 'activeIdx',
+ select: 'select(activeIdx, evt)',
+ 'move-in-progress': 'moveInProgress',
+ query: 'query',
+ position: 'position',
+ 'assign-is-open': 'assignIsOpen(isOpen)',
+ debounce: 'debounceUpdate'
+ });
+ //custom item template
+ if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
+ popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
+ }
- var locals = {$viewValue: inputValue};
- isLoadingSetter(originalScope, true);
- $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
+ if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
+ popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
+ }
- //it might happen that several async queries were in progress if a user were typing fast
- //but we are interested only in responses that correspond to the current view value
- var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
- if (onCurrentRequest && hasFocus) {
- if (matches.length > 0) {
+ var resetHint = function() {
+ if (showHint) {
+ hintInputElem.val('');
+ }
+ };
- scope.activeIdx = 0;
- scope.matches.length = 0;
+ var resetMatches = function() {
+ scope.matches = [];
+ scope.activeIdx = -1;
+ element.attr('aria-expanded', false);
+ resetHint();
+ };
- //transform labels
- for(var i=0; i index && inputValue) {
+ return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
+ }
+
+ return false;
+ };
+
+ var getMatchesAsync = function(inputValue, evt) {
+ var locals = {$viewValue: inputValue};
+ isLoadingSetter(originalScope, true);
+ isNoResultsSetter(originalScope, false);
+ $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
+ //it might happen that several async queries were in progress if a user were typing fast
+ //but we are interested only in responses that correspond to the current view value
+ var onCurrentRequest = inputValue === modelCtrl.$viewValue;
+ if (onCurrentRequest && hasFocus) {
+ if (matches && matches.length > 0) {
+ scope.activeIdx = focusFirst ? 0 : -1;
+ isNoResultsSetter(originalScope, false);
+ scope.matches.length = 0;
+
+ //transform labels
+ for (var i = 0; i < matches.length; i++) {
+ locals[parserResult.itemName] = matches[i];
+ scope.matches.push({
+ id: getMatchId(i),
+ label: parserResult.viewMapper(scope, locals),
+ model: matches[i]
+ });
}
+
+ scope.query = inputValue;
+ //position pop-up with matches - we need to re-calculate its position each time we are opening a window
+ //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
+ //due to other elements being rendered
+ recalculatePosition();
+
+ element.attr('aria-expanded', true);
+
+ //Select the single remaining option if user input matches
+ if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
+ if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
+ $$debounce(function() {
+ scope.select(0, evt);
+ }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
+ } else {
+ scope.select(0, evt);
+ }
+ }
+
+ if (showHint) {
+ var firstLabel = scope.matches[0].label;
+ if (angular.isString(inputValue) &&
+ inputValue.length > 0 &&
+ firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) {
+ hintInputElem.val(inputValue + firstLabel.slice(inputValue.length));
+ } else {
+ hintInputElem.val('');
+ }
+ }
+ } else {
+ resetMatches();
+ isNoResultsSetter(originalScope, true);
}
- if (onCurrentRequest) {
- isLoadingSetter(originalScope, false);
- }
- }, function(){
- resetMatches();
+ }
+ if (onCurrentRequest) {
isLoadingSetter(originalScope, false);
- });
- };
+ }
+ }, function() {
+ resetMatches();
+ isLoadingSetter(originalScope, false);
+ isNoResultsSetter(originalScope, true);
+ });
+ };
+
+ // bind events only if appendToBody params exist - performance feature
+ if (appendToBody) {
+ angular.element($window).on('resize', fireRecalculating);
+ $document.find('body').on('scroll', fireRecalculating);
+ }
+
+ // Declare the debounced function outside recalculating for
+ // proper debouncing
+ var debouncedRecalculate = $$debounce(function() {
+ // if popup is visible
+ if (scope.matches.length) {
+ recalculatePosition();
+ }
+
+ scope.moveInProgress = false;
+ }, eventDebounceTime);
+
+ // Default progress type
+ scope.moveInProgress = false;
+
+ function fireRecalculating() {
+ if (!scope.moveInProgress) {
+ scope.moveInProgress = true;
+ scope.$digest();
+ }
+
+ debouncedRecalculate();
+ }
+
+ // recalculate actual position and set new values to scope
+ // after digest loop is popup in right position
+ function recalculatePosition() {
+ scope.position = appendToBody ? $position.offset(element) : $position.position(element);
+ scope.position.top += element.prop('offsetHeight');
+ }
+
+ //we need to propagate user's query so we can higlight matches
+ scope.query = undefined;
+
+ //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
+ var timeoutPromise;
+
+ var scheduleSearchWithTimeout = function(inputValue) {
+ timeoutPromise = $timeout(function() {
+ getMatchesAsync(inputValue);
+ }, waitTime);
+ };
+
+ var cancelPreviousTimeout = function() {
+ if (timeoutPromise) {
+ $timeout.cancel(timeoutPromise);
+ }
+ };
+
+ resetMatches();
+
+ scope.assignIsOpen = function (isOpen) {
+ isOpenSetter(originalScope, isOpen);
+ };
+
+ scope.select = function(activeIdx, evt) {
+ //called from within the $digest() cycle
+ var locals = {};
+ var model, item;
+
+ selected = true;
+ locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
+ model = parserResult.modelMapper(originalScope, locals);
+ $setModelValue(originalScope, model);
+ modelCtrl.$setValidity('editable', true);
+ modelCtrl.$setValidity('parse', true);
+
+ onSelectCallback(originalScope, {
+ $item: item,
+ $model: model,
+ $label: parserResult.viewMapper(originalScope, locals),
+ $event: evt
+ });
resetMatches();
- //we need to propagate user's query so we can higlight matches
- scope.query = undefined;
+ //return focus to the input element if a match was selected via a mouse click event
+ // use timeout to avoid $rootScope:inprog error
+ if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
+ $timeout(function() { element[0].focus(); }, 0, false);
+ }
+ };
- //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
- var timeoutPromise;
+ //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
+ element.on('keydown', function(evt) {
+ //typeahead is open and an "interesting" key was pressed
+ if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
+ return;
+ }
+
+ /**
+ * if there's nothing selected (i.e. focusFirst) and enter or tab is hit
+ * or
+ * shift + tab is pressed to bring focus to the previous element
+ * then clear the results
+ */
+ if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13) || evt.which === 9 && !!evt.shiftKey) {
+ resetMatches();
+ scope.$digest();
+ return;
+ }
+
+ evt.preventDefault();
+ var target;
+ switch (evt.which) {
+ case 9:
+ case 13:
+ scope.$apply(function () {
+ if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
+ $$debounce(function() {
+ scope.select(scope.activeIdx, evt);
+ }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
+ } else {
+ scope.select(scope.activeIdx, evt);
+ }
+ });
+ break;
+ case 27:
+ evt.stopPropagation();
+
+ resetMatches();
+ originalScope.$digest();
+ break;
+ case 38:
+ scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
+ scope.$digest();
+ target = popUpEl.find('li')[scope.activeIdx];
+ target.parentNode.scrollTop = target.offsetTop;
+ break;
+ case 40:
+ scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
+ scope.$digest();
+ target = popUpEl.find('li')[scope.activeIdx];
+ target.parentNode.scrollTop = target.offsetTop;
+ break;
+ }
+ });
+
+ element.bind('focus', function (evt) {
+ hasFocus = true;
+ if (minLength === 0 && !modelCtrl.$viewValue) {
+ $timeout(function() {
+ getMatchesAsync(modelCtrl.$viewValue, evt);
+ }, 0);
+ }
+ });
+
+ element.bind('blur', function(evt) {
+ if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
+ selected = true;
+ scope.$apply(function() {
+ if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) {
+ $$debounce(function() {
+ scope.select(scope.activeIdx, evt);
+ }, scope.debounceUpdate.blur);
+ } else {
+ scope.select(scope.activeIdx, evt);
+ }
+ });
+ }
+ if (!isEditable && modelCtrl.$error.editable) {
+ modelCtrl.$setViewValue();
+ // Reset validity as we are clearing
+ modelCtrl.$setValidity('editable', true);
+ modelCtrl.$setValidity('parse', true);
+ element.val('');
+ }
+ hasFocus = false;
+ selected = false;
+ });
+
+ // Keep reference to click handler to unbind it.
+ var dismissClickHandler = function(evt) {
+ // Issue #3973
+ // Firefox treats right click as a click on document
+ if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
+ resetMatches();
+ if (!$rootScope.$$phase) {
+ originalScope.$digest();
+ }
+ }
+ };
+
+ $document.on('click', dismissClickHandler);
+
+ originalScope.$on('$destroy', function() {
+ $document.off('click', dismissClickHandler);
+ if (appendToBody || appendTo) {
+ $popup.remove();
+ }
+
+ if (appendToBody) {
+ angular.element($window).off('resize', fireRecalculating);
+ $document.find('body').off('scroll', fireRecalculating);
+ }
+ // Prevent jQuery cache memory leak
+ popUpEl.remove();
+
+ if (showHint) {
+ inputsContainer.remove();
+ }
+ });
+
+ var $popup = $compile(popUpEl)(scope);
+
+ if (appendToBody) {
+ $document.find('body').append($popup);
+ } else if (appendTo) {
+ angular.element(appendTo).eq(0).append($popup);
+ } else {
+ element.after($popup);
+ }
+
+ this.init = function(_modelCtrl, _ngModelOptions) {
+ modelCtrl = _modelCtrl;
+ ngModelOptions = _ngModelOptions;
+
+ scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope);
//plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
//$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
- modelCtrl.$parsers.unshift(function (inputValue) {
-
+ modelCtrl.$parsers.unshift(function(inputValue) {
hasFocus = true;
- if (inputValue && inputValue.length >= minSearch) {
+ if (minLength === 0 || inputValue && inputValue.length >= minLength) {
if (waitTime > 0) {
- if (timeoutPromise) {
- $timeout.cancel(timeoutPromise);//cancel previous timeout
- }
- timeoutPromise = $timeout(function () {
- getMatchesAsync(inputValue);
- }, waitTime);
+ cancelPreviousTimeout();
+ scheduleSearchWithTimeout(inputValue);
} else {
getMatchesAsync(inputValue);
}
} else {
isLoadingSetter(originalScope, false);
+ cancelPreviousTimeout();
resetMatches();
}
if (isEditable) {
return inputValue;
- } else {
- if (!inputValue) {
- // Reset in case user had typed something previously.
- modelCtrl.$setValidity('editable', true);
- return inputValue;
- } else {
- modelCtrl.$setValidity('editable', false);
- return undefined;
- }
}
+
+ if (!inputValue) {
+ // Reset in case user had typed something previously.
+ modelCtrl.$setValidity('editable', true);
+ return null;
+ }
+
+ modelCtrl.$setValidity('editable', false);
+ return undefined;
});
- modelCtrl.$formatters.push(function (modelValue) {
-
+ modelCtrl.$formatters.push(function(modelValue) {
var candidateViewValue, emptyViewValue;
var locals = {};
+ // The validity may be set to false via $parsers (see above) if
+ // the model is restricted to selected values. If the model
+ // is set manually it is considered to be valid.
+ if (!isEditable) {
+ modelCtrl.$setValidity('editable', true);
+ }
+
if (inputFormatter) {
-
- locals['$model'] = modelValue;
+ locals.$model = modelValue;
return inputFormatter(originalScope, locals);
-
- } else {
-
- //it might happen that we don't have enough info to properly render input value
- //we need to check for this situation and simply return model value if we can't apply custom formatting
- locals[parserResult.itemName] = modelValue;
- candidateViewValue = parserResult.viewMapper(originalScope, locals);
- locals[parserResult.itemName] = undefined;
- emptyViewValue = parserResult.viewMapper(originalScope, locals);
-
- return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue;
- }
- });
-
- scope.select = function (activeIdx) {
- //called from within the $digest() cycle
- var locals = {};
- var model, item;
-
- locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
- model = parserResult.modelMapper(originalScope, locals);
- $setModelValue(originalScope, model);
- modelCtrl.$setValidity('editable', true);
-
- onSelectCallback(originalScope, {
- $item: item,
- $model: model,
- $label: parserResult.viewMapper(originalScope, locals)
- });
-
- resetMatches();
-
- //return focus to the input element if a match was selected via a mouse click event
- // use timeout to avoid $rootScope:inprog error
- $timeout(function() { element[0].focus(); }, 0, false);
- };
-
- //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
- element.bind('keydown', function (evt) {
-
- //typeahead is open and an "interesting" key was pressed
- if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
- return;
}
- evt.preventDefault();
+ //it might happen that we don't have enough info to properly render input value
+ //we need to check for this situation and simply return model value if we can't apply custom formatting
+ locals[parserResult.itemName] = modelValue;
+ candidateViewValue = parserResult.viewMapper(originalScope, locals);
+ locals[parserResult.itemName] = undefined;
+ emptyViewValue = parserResult.viewMapper(originalScope, locals);
- if (evt.which === 40) {
- scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
- scope.$digest();
-
- } else if (evt.which === 38) {
- scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1;
- scope.$digest();
-
- } else if (evt.which === 13 || evt.which === 9) {
- scope.$apply(function () {
- scope.select(scope.activeIdx);
- });
-
- } else if (evt.which === 27) {
- evt.stopPropagation();
-
- resetMatches();
- scope.$digest();
- }
+ return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue;
});
+ };
+ }])
- element.bind('blur', function (evt) {
- hasFocus = false;
- });
-
- // Keep reference to click handler to unbind it.
- var dismissClickHandler = function (evt) {
- if (element[0] !== evt.target) {
- resetMatches();
- scope.$digest();
- }
- };
-
- $document.bind('click', dismissClickHandler);
-
- originalScope.$on('$destroy', function(){
- $document.unbind('click', dismissClickHandler);
- });
-
- var $popup = $compile(popUpEl)(scope);
- if ( appendToBody ) {
- $document.find('body').append($popup);
- } else {
- element.after($popup);
- }
- }
- };
-
-}])
-
- .directive('typeaheadPopup', function () {
+ .directive('uibTypeahead', function() {
return {
- restrict:'EA',
- scope:{
- matches:'=',
- query:'=',
- active:'=',
- position:'=',
- select:'&'
- },
- replace:true,
- templateUrl:'template/typeahead/typeahead-popup.html',
- link:function (scope, element, attrs) {
-
- scope.templateUrl = attrs.templateUrl;
-
- scope.isOpen = function () {
- return scope.matches.length > 0;
- };
-
- scope.isActive = function (matchIdx) {
- return scope.active == matchIdx;
- };
-
- scope.selectActive = function (matchIdx) {
- scope.active = matchIdx;
- };
-
- scope.selectMatch = function (activeIdx) {
- scope.select({activeIdx:activeIdx});
- };
+ controller: 'UibTypeaheadController',
+ require: ['ngModel', '^?ngModelOptions', 'uibTypeahead'],
+ link: function(originalScope, element, attrs, ctrls) {
+ ctrls[2].init(ctrls[0], ctrls[1]);
}
};
})
- .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) {
+ .directive('uibTypeaheadPopup', ['$$debounce', function($$debounce) {
return {
- restrict:'EA',
- scope:{
- index:'=',
- match:'=',
- query:'='
+ scope: {
+ matches: '=',
+ query: '=',
+ active: '=',
+ position: '&',
+ moveInProgress: '=',
+ select: '&',
+ assignIsOpen: '&',
+ debounce: '&'
},
- link:function (scope, element, attrs) {
- var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
- $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){
- element.replaceWith($compile(tplContent.trim())(scope));
+ replace: true,
+ templateUrl: function(element, attrs) {
+ return attrs.popupTemplateUrl || 'uib/template/typeahead/typeahead-popup.html';
+ },
+ link: function(scope, element, attrs) {
+ scope.templateUrl = attrs.templateUrl;
+
+ scope.isOpen = function() {
+ var isDropdownOpen = scope.matches.length > 0;
+ scope.assignIsOpen({ isOpen: isDropdownOpen });
+ return isDropdownOpen;
+ };
+
+ scope.isActive = function(matchIdx) {
+ return scope.active === matchIdx;
+ };
+
+ scope.selectActive = function(matchIdx) {
+ scope.active = matchIdx;
+ };
+
+ scope.selectMatch = function(activeIdx, evt) {
+ var debounce = scope.debounce();
+ if (angular.isNumber(debounce) || angular.isObject(debounce)) {
+ $$debounce(function() {
+ scope.select({activeIdx: activeIdx, evt: evt});
+ }, angular.isNumber(debounce) ? debounce : debounce['default']);
+ } else {
+ scope.select({activeIdx: activeIdx, evt: evt});
+ }
+ };
+ }
+ };
+ }])
+
+ .directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
+ return {
+ scope: {
+ index: '=',
+ match: '=',
+ query: '='
+ },
+ link: function(scope, element, attrs) {
+ var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'uib/template/typeahead/typeahead-match.html';
+ $templateRequest(tplUrl).then(function(tplContent) {
+ var tplEl = angular.element(tplContent.trim());
+ element.replaceWith(tplEl);
+ $compile(tplEl)(scope);
});
}
};
}])
- .filter('typeaheadHighlight', function() {
+ .filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) {
+ var isSanitizePresent;
+ isSanitizePresent = $injector.has('$sanitize');
function escapeRegexp(queryToEscape) {
+ // Regex: capture the whole query string and replace it with the string that will be used to match
+ // the results, for example if the capture is "a" the result will be \a
return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
}
- return function(matchItem, query) {
- return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem;
- };
- });
+ function containsHtml(matchItem) {
+ return /<.*>/g.test(matchItem);
+ }
-angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) {
- $templateCache.put("template/accordion/accordion-group.html",
- "
\n" +
- "
\n" +
+ return function(matchItem, query) {
+ if (!isSanitizePresent && containsHtml(matchItem)) {
+ $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger
+ }
+ matchItem = query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag
+ if (!isSanitizePresent) {
+ matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive
+ }
+ return matchItem;
+ };
+ }]);
+
+angular.module("uib/template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("uib/template/accordion/accordion-group.html",
+ "