UNPKG

angular-ui-bootstrap

Version:

Native AngularJS (Angular) directives for Bootstrap

1,635 lines (1,437 loc) 249 kB
/* * angular-ui-bootstrap * http://angular-ui.github.io/bootstrap/ * Version: 2.5.6 - 2017-10-14 * License: MIT */angular.module("ui.bootstrap", ["ui.bootstrap.collapse","ui.bootstrap.tabindex","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.multiMap","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); angular.module('ui.bootstrap.collapse', []) .directive('uibCollapse', ['$animate', '$q', '$parse', '$injector', function($animate, $q, $parse, $injector) { var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null; return { link: function(scope, element, attrs) { var expandingExpr = $parse(attrs.expanding), expandedExpr = $parse(attrs.expanded), collapsingExpr = $parse(attrs.collapsing), collapsedExpr = $parse(attrs.collapsed), horizontal = false, css = {}, cssTo = {}; init(); function init() { horizontal = !!('horizontal' in attrs); if (horizontal) { css = { width: '' }; cssTo = {width: '0'}; } else { css = { height: '' }; cssTo = {height: '0'}; } if (!scope.$eval(attrs.uibCollapse)) { element.addClass('in') .addClass('collapse') .attr('aria-expanded', true) .attr('aria-hidden', false) .css(css); } } function getScrollFromElement(element) { if (horizontal) { return {width: element.scrollWidth + 'px'}; } return {height: element.scrollHeight + 'px'}; } function expand() { if (element.hasClass('collapse') && element.hasClass('in')) { return; } $q.resolve(expandingExpr(scope)) .then(function() { element.removeClass('collapse') .addClass('collapsing') .attr('aria-expanded', true) .attr('aria-hidden', false); if ($animateCss) { $animateCss(element, { addClass: 'in', easing: 'ease', css: { overflow: 'hidden' }, to: getScrollFromElement(element[0]) }).start()['finally'](expandDone); } else { $animate.addClass(element, 'in', { css: { overflow: 'hidden' }, to: getScrollFromElement(element[0]) }).then(expandDone); } }, angular.noop); } function expandDone() { element.removeClass('collapsing') .addClass('collapse') .css(css); expandedExpr(scope); } function collapse() { if (!element.hasClass('collapse') && !element.hasClass('in')) { return collapseDone(); } $q.resolve(collapsingExpr(scope)) .then(function() { element // IMPORTANT: The width must be set before adding "collapsing" class. // Otherwise, the browser attempts to animate from width 0 (in // collapsing class) to the given width here. .css(getScrollFromElement(element[0])) // initially all panel collapse have the collapse class, this removal // prevents the animation from jumping to collapsed state .removeClass('collapse') .addClass('collapsing') .attr('aria-expanded', false) .attr('aria-hidden', true); if ($animateCss) { $animateCss(element, { removeClass: 'in', to: cssTo }).start()['finally'](collapseDone); } else { $animate.removeClass(element, 'in', { to: cssTo }).then(collapseDone); } }, angular.noop); } function collapseDone() { element.css(cssTo); // Required so that collapse works when animation is disabled element.removeClass('collapsing') .addClass('collapse'); collapsedExpr(scope); } scope.$watch(attrs.uibCollapse, function(shouldCollapse) { if (shouldCollapse) { collapse(); } else { expand(); } }); } }; }]); angular.module('ui.bootstrap.tabindex', []) .directive('uibTabindexToggle', function() { return { restrict: 'A', link: function(scope, elem, attrs) { attrs.$observe('disabled', function(disabled) { attrs.$set('tabindex', disabled ? -1 : null); }); } }; }); angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse', 'ui.bootstrap.tabindex']) .constant('uibAccordionConfig', { closeOthers: true }) .controller('UibAccordionController', ['$scope', '$attrs', 'uibAccordionConfig', function($scope, $attrs, accordionConfig) { // This array keeps track of the accordion groups this.groups = []; // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to this.closeOthers = function(openGroup) { var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; if (closeOthers) { angular.forEach(this.groups, function(group) { if (group !== openGroup) { group.isOpen = false; } }); } }; // This is called from the accordion-group directive to add itself to the accordion this.addGroup = function(groupScope) { var that = this; this.groups.push(groupScope); groupScope.$on('$destroy', function(event) { that.removeGroup(groupScope); }); }; // This is called from the accordion-group directive when to remove itself this.removeGroup = function(group) { var index = this.groups.indexOf(group); if (index !== -1) { this.groups.splice(index, 1); } }; }]) // The accordion directive simply sets up the directive controller // and adds an accordion CSS class to itself element. .directive('uibAccordion', function() { return { controller: 'UibAccordionController', controllerAs: 'accordion', transclude: true, templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/accordion/accordion.html'; } }; }) // The accordion-group directive indicates a block of html that will expand and collapse in an accordion .directive('uibAccordionGroup', function() { return { require: '^uibAccordion', // We need this directive to be inside an accordion transclude: true, // It transcludes the contents of the directive into the template restrict: 'A', templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/accordion/accordion-group.html'; }, scope: { heading: '@', // Interpolate the heading attribute onto this scope panelClass: '@?', // Ditto with panelClass isOpen: '=?', isDisabled: '=?' }, controller: function() { this.setHeading = function(element) { this.heading = element; }; }, link: function(scope, element, attrs, accordionCtrl) { element.addClass('panel'); accordionCtrl.addGroup(scope); scope.openClass = attrs.openClass || 'panel-open'; scope.panelClass = attrs.panelClass || 'panel-default'; scope.$watch('isOpen', function(value) { element.toggleClass(scope.openClass, !!value); if (value) { accordionCtrl.closeOthers(scope); } }); scope.toggleOpen = function($event) { if (!scope.isDisabled) { if (!$event || $event.which === 32) { scope.isOpen = !scope.isOpen; } } }; var id = 'accordiongroup-' + scope.$id + '-' + Math.floor(Math.random() * 10000); scope.headingId = id + '-tab'; scope.panelId = id + '-panel'; } }; }) // Use accordion-heading below an accordion-group to provide a heading containing HTML .directive('uibAccordionHeading', function() { return { transclude: true, // Grab the contents to be used as the heading template: '', // In effect remove this element! replace: true, require: '^uibAccordionGroup', link: function(scope, element, attrs, accordionGroupCtrl, transclude) { // Pass the heading to the accordion-group controller // so that it can be transcluded into the right place in the template // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] accordionGroupCtrl.setHeading(transclude(scope, angular.noop)); } }; }) // Use in the accordion-group template to indicate where you want the heading to be transcluded // You must provide the property on the accordion-group controller that will hold the transcluded element .directive('uibAccordionTransclude', function() { return { 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(getHeaderSelectors())); elem.html(''); elem.append(heading); } }); } }; function getHeaderSelectors() { return 'uib-accordion-header,' + 'data-uib-accordion-header,' + 'x-uib-accordion-header,' + 'uib\\:accordion-header,' + '[uib-accordion-header],' + '[data-uib-accordion-header],' + '[x-uib-accordion-header]'; } }); angular.module('ui.bootstrap.alert', []) .controller('UibAlertController', ['$scope', '$element', '$attrs', '$interpolate', '$timeout', function($scope, $element, $attrs, $interpolate, $timeout) { $scope.closeable = !!$attrs.close; $element.addClass('alert'); $attrs.$set('role', 'alert'); if ($scope.closeable) { $element.addClass('alert-dismissible'); } var dismissOnTimeout = angular.isDefined($attrs.dismissOnTimeout) ? $interpolate($attrs.dismissOnTimeout)($scope.$parent) : null; if (dismissOnTimeout) { $timeout(function() { $scope.close(); }, parseInt(dismissOnTimeout, 10)); } }]) .directive('uibAlert', function() { return { controller: 'UibAlertController', controllerAs: 'alert', restrict: 'A', templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/alert/alert.html'; }, transclude: true, scope: { close: '&' } }; }); angular.module('ui.bootstrap.buttons', []) .constant('uibButtonConfig', { activeClass: 'active', toggleEvent: 'click' }) .controller('UibButtonsController', ['uibButtonConfig', function(buttonConfig) { this.activeClass = buttonConfig.activeClass || 'active'; this.toggleEvent = buttonConfig.toggleEvent || 'click'; }]) .directive('uibBtnRadio', ['$parse', function($parse) { return { 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.uibBtnRadio))); }; //ui->model 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.uibBtnRadio)); ngModelCtrl.$render(); }); } }); if (attrs.uibUncheckable) { scope.$watch(uncheckableExpr, function(uncheckable) { attrs.$set('uncheckable', uncheckable ? '' : undefined); }); } } }; }]) .directive('uibBtnCheckbox', function() { return { 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); } function getFalseValue() { return getCheckboxValue(attrs.btnCheckboxFalse, false); } function getCheckboxValue(attribute, defaultValue) { return angular.isDefined(attribute) ? scope.$eval(attribute) : defaultValue; } //model -> UI ngModelCtrl.$render = function() { element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); }; //ui->model element.on(buttonsCtrl.toggleEvent, function() { if (attrs.disabled) { return; } scope.$apply(function() { ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); ngModelCtrl.$render(); }); }); } }; }); 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 = [], SLIDE_DIRECTION = 'uib-slideDirection', currentIndex = $scope.active, currentInterval, isPlaying; var destroyed = false; $element.addClass('carousel'); 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); //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; } }; /* direction: "prev" or "next" */ self.select = $scope.select = function(nextSlide, direction) { var nextIndex = findSlideIndex(nextSlide.slide); //Decide direction if it's not given if (direction === undefined) { direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev'; } //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); } }; /* Allow outside people to call indexOf on slides array */ $scope.indexOfSlide = function(slide) { return +slide.slide.index; }; $scope.isActive = function(slide) { return $scope.active === slide.slide.index; }; $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; resetTimer(); } }; $scope.play = function() { if (!isPlaying) { isPlaying = true; restartTimer(); } }; $element.on('mouseenter', $scope.pause); $element.on('mouseleave', $scope.play); $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; } } }); 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); } }); } $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; } } 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(); } } }]) .directive('uibCarousel', function() { return { transclude: true, controller: 'UibCarouselController', controllerAs: 'carousel', restrict: 'A', templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/carousel/carousel.html'; }, scope: { active: '=', interval: '=', noTransition: '=', noPause: '=', noWrap: '&' } }; }) .directive('uibSlide', ['$animate', function($animate) { return { require: '^uibCarousel', restrict: 'A', transclude: true, templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/carousel/slide.html'; }, scope: { actual: '=?', index: '=?' }, link: function (scope, element, attrs, carouselCtrl) { element.addClass('item'); carouselCtrl.addSlide(scope, element); //when the scope is destroyed then remove the slide from the current slides array scope.$on('$destroy', function() { carouselCtrl.removeSlide(scope); }); scope.$watch('active', function(active) { $animate[active ? 'addClass' : 'removeClass'](element, 'active'); }); } }; }]) .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('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', 'filterFilter', function($log, $locale, dateFilter, orderByFilter, filterFilter) { // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; var localeId; var formatCodeToRegex; 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'); } } ]; if (angular.version.major >= 1 && angular.version.minor > 4) { formatCodeToRegex.push({ key: 'LLLL', regex: $locale.DATETIME_FORMATS.STANDALONEMONTH.join('|'), apply: function(value) { this.month = $locale.DATETIME_FORMATS.STANDALONEMONTH.indexOf(value); }, formatter: function(date) { return dateFilter(date, 'LLLL'); } }); } }; this.init(); function getFormatCodeToRegex(key) { return filterFilter(formatCodeToRegex, {key: key}, true)[0]; } this.getParser = function (key) { var f = getFormatCodeToRegex(key); return f && f.apply || null; }; this.overrideParser = function (key, parser) { var f = getFormatCodeToRegex(key); if (f && angular.isFunction(parser)) { this.parsers = {}; f.apply = parser; } }.bind(this); function createParser(format) { var map = [], regex = format.split(''); // 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 + data.key.length; i < n; i++) { regex[i] = ''; format[i] = '$'; } format = format.join(''); map.push({ index: index, key: data.key, apply: data.apply, matcher: data.regex }); } }); return { regex: new RegExp('^' + regex.join('') + '$'), map: orderByFilter(map, 'index') }; } function createFormatter(format) { var formatters = []; var i = 0; var formatter, literalIdx; while (i < format.length) { if (angular.isNumber(literalIdx)) { if (format.charAt(i) === '\'') { if (i + 1 >= format.length || format.charAt(i + 1) !== '\'') { formatters.push(constructLiteralFormatter(format, literalIdx, i)); literalIdx = null; } } else if (i === format.length) { while (literalIdx < format.length) { formatter = constructFormatterFromIdx(format, literalIdx); formatters.push(formatter); literalIdx = formatter.endIdx; } } i++; continue; } if (format.charAt(i) === '\'') { literalIdx = i; i++; continue; } formatter = constructFormatterFromIdx(format, i); formatters.push(formatter.parser); i = formatter.endIdx; } return formatters; } function constructLiteralFormatter(format, literalIdx, endIdx) { return function() { return format.substr(literalIdx + 1, endIdx - literalIdx - 1); }; } function constructFormatterFromIdx(format, i) { var currentPosStr = format.substr(i); for (var j = 0; j < formatCodeToRegex.length; j++) { if (new RegExp('^' + formatCodeToRegex[j].key).test(currentPosStr)) { var data = formatCodeToRegex[j]; return { endIdx: i + data.key.length, parser: data.formatter }; } } return { endIdx: i + 1, parser: function() { return currentPosStr.charAt(0); } }; } this.filter = function(date, format) { if (!angular.isDate(date) || isNaN(date) || !format) { return ''; } format = $locale.DATETIME_FORMATS[format] || format; if ($locale.id !== localeId) { this.init(); } if (!this.formatters[format]) { this.formatters[format] = createFormatter(format); } var formatters = this.formatters[format]; return formatters.reduce(function(str, formatter) { return str + formatter(date); }, ''); }; 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), 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 }; } for (var i = 1, n = results.length; i < n; i++) { var mapper = map[i - 1]; if (mapper.matcher === 'Z') { tzOffset = true; } if (mapper.apply) { mapper.apply.call(fields, results[i]); } } 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; } }; // Check if date is valid for specific month (and year for February). // Month: 0 = Jan, 1 = Feb, etc function isValid(year, month, date) { if (date < 1) { return false; } 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/622c42169699ec07fc6daaa19fe6d224e5d2f70e/src/Angular.js#L1207 function timezoneToOffset(timezone, fallback) { timezone = timezone.replace(/:/g, ''); 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 dateTimezoneOffset = date.getTimezoneOffset(); var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset)); } }]); // 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*$/; var dataPerTracked = {}; 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); } 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) }; } data.watchFn(scope.$eval(compareWithExp)); } 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']) .value('$datepickerSuppressError', false) .value('$datepickerLiteralWarning', true) .constant('uibDatepickerConfig', { datepickerMode: 'day', formatDay: 'dd', formatMonth: 'MMMM', formatYear: 'yyyy', formatDayHeader: 'EEE', formatDayTitle: 'MMMM yyyy', formatMonthTitle: 'yyyy', maxDate: null, maxMode: 'year', minDate: null, minMode: 'day', monthColumns: 3, ngModelOptions: {}, shortcutPropagation: false, showWeeks: true, yearColumns: 5, yearRows: 4 }) .controller('UibDatepickerController', ['$scope', '$element', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerLiteralWarning', '$datepickerSuppressError', 'uibDateParser', function($scope, $element, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerLiteralWarning, $datepickerSuppressError, dateParser) { var self = this, ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl; ngModelOptions = {}, watchListeners = []; $element.addClass('uib-datepicker'); $attrs.$set('role', 'application'); if (!$scope.datepickerOptions) { $scope.datepickerOptions = {}; } // Modes chain this.modes = ['day', 'month', 'year']; [ 'customClass', 'dateDisabled', 'datepickerMode', 'formatDay', 'formatDayHeader', 'formatDayTitle', 'formatMonth', 'formatMonthTitle', 'formatYear', 'maxDate', 'maxMode', 'minDate', 'minMode', 'monthColumns', '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 'monthColumns': 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; } 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.getOption('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.getOption('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 : $scope.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.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); $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) { $scope.activeDateId = dateObject.uid; return true; } return false; }; this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; ngModelOptions = extractOptions(ngModelCtrl); if ($scope.datepickerOptions.initDate) { self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.getOption('timezone')) || new Date(); $scope.$watch('datepickerOptions.initDate', function(initDate) { if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) { self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.getOption('timezone')); self.refreshView(); } }); } else { self.activeDate = new Date(); } var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : new Date(); this.activeDate = !isNaN(date) ? dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')) : dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone')); ngModelCtrl.$render = function() { self.render(); }; }; this.render = function() { if (ngModelCtrl.$viewValue) { var date = new Date(ngModelCtrl.$viewValue), isValid = !isNaN(date); if (isValid) { this.activeDate = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')); } else if (!$datepickerSuppressError) { $log.error('Datepicker directive: "ng-model" value must be a Date object'); } } this.refreshView(); }; this.refreshView = function() { if (this.element) { $scope.selectedDt = null; this._refreshView(); if ($scope.activeDt) { $scope.activeDateId = $scope.activeDt.uid; } var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;