/* * angular-ui-bootstrap * http://angular-ui.github.io/bootstrap/ * Version: 0.11.0-SNAPSHOT - 2014-03-17 * License: MIT */ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position', 'ui.bootstrap.dateparser']) .constant('datepickerConfig', { formatDay: 'dd', formatMonth: 'MMMM', formatYear: 'yyyy', formatDayHeader: 'EEE', formatDayTitle: 'MMMM yyyy', formatMonthTitle: 'yyyy', datepickerMode: 'day', minMode: 'day', maxMode: 'year', showWeeks: true, startingDay: 0, yearRange: 20, minDate: null, maxDate: null }) .controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig) { var self = this, ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; // 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]; }); // 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; } }); $scope.datepickerMode = $scope.datepickerMode || (angular.isDefined($attrs['datepickerMode']) ? $interpolate($attrs['datepickerMode'])($scope.$parent) : datepickerConfig.datepickerMode); this.currentCalendarDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); this.init = function( ngModelCtrl_ ) { ngModelCtrl = ngModelCtrl_; ngModelCtrl.$render = function() { self.render(); }; }; this.render = function() { if ( ngModelCtrl.$modelValue ) { var date = new Date( ngModelCtrl.$modelValue ), isValid = !isNaN(date); if ( isValid ) { this.currentCalendarDate = 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.'); } ngModelCtrl.$setValidity('date', isValid); } this.refreshView(); }; this.refreshView = function() { if ( this.mode ) { this._refreshView(); var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; ngModelCtrl.$setValidity('date-disabled', !date || (this.mode && !this.isDisabled(date))); } }; this.createDateObject = function(date, format) { var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; return { date: date, label: dateFilter(date, format), selected: model && this.compare(date, model) === 0, disabled: this.isDisabled(date), current: this.compare(date, new Date()) === 0 }; }; this.isDisabled = function( date ) { return ((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}))); }; // Split array into smaller arrays this.split = function(arr, size) { var arrays = []; while (arr.length > 0) { arrays.push(arr.splice(0, size)); } 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 ); ngModelCtrl.$render(); } else { self.currentCalendarDate = date; $scope.datepickerMode = self.mode.previous; } }; $scope.move = function( direction ) { var year = self.currentCalendarDate.getFullYear() + direction * (self.mode.step.years || 0), month = self.currentCalendarDate.getMonth() + direction * (self.mode.step.months || 0); self.currentCalendarDate.setFullYear(year, month, 1); self.refreshView(); }; $scope.toggleMode = function() { $scope.datepickerMode = $scope.datepickerMode === self.maxMode ? self.minMode : self.mode.next; }; }]) .directive('daypicker', ['dateFilter', function (dateFilter) { return { restrict: 'EA', replace: true, templateUrl: 'template/datepicker/day.html', require: '^datepicker', link: function(scope, element, attrs, ctrl) { scope.showWeeks = ctrl.showWeeks; ctrl.mode = { step: { months: 1 }, next: 'month' }; function getDaysInMonth( year, month ) { return new Date(year, month, 0).getDate(); } 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.currentCalendarDate.getFullYear(), month = ctrl.currentCalendarDate.getMonth(), firstDayOfMonth = new Date(year, month, 1), difference = ctrl.startingDay - firstDayOfMonth.getDay(), numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, firstDate = new Date(firstDayOfMonth), numDates = 0; if ( numDisplayedFromPreviousMonth > 0 ) { firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); numDates += numDisplayedFromPreviousMonth; // Previous } numDates += getDaysInMonth(year, month + 1); // Current numDates += (7 - numDates % 7) % 7; // Next var days = getDates(firstDate, numDates); for (var i = 0; i < numDates; i ++) { days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { secondary: days[i].getMonth() !== month }); } scope.labels = new Array(7); for (var j = 0; j < 7; j++) { scope.labels[j] = dateFilter(days[j].date, ctrl.formatDayHeader); } scope.title = dateFilter(ctrl.currentCalendarDate, 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.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.mode = { step: { years: 1 }, previous: 'day', next: 'year' }; ctrl._refreshView = function() { var months = new Array(12), year = ctrl.currentCalendarDate.getFullYear(); for ( var i = 0; i < 12; i++ ) { months[i] = ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth); } scope.title = dateFilter(ctrl.currentCalendarDate, 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.refreshView(); } }; }]) .directive('yearpicker', ['dateFilter', function (dateFilter) { return { restrict: 'EA', replace: true, templateUrl: 'template/datepicker/year.html', require: '^datepicker', link: function(scope, element, attrs, ctrl) { ctrl.mode = { step: { years: ctrl.yearRange }, previous: 'month' }; ctrl._refreshView = function() { var range = this.mode.step.years, years = new Array(range), start = parseInt((ctrl.currentCalendarDate.getFullYear() - 1) / range, 10) * range + 1; for ( var i = 0; i < range; i++ ) { years[i] = ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear); } 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.refreshView(); } }; }]) .directive( 'datepicker', function () { return { restrict: 'EA', replace: true, templateUrl: 'template/datepicker/datepicker.html', scope: { datepickerMode: '=?', dateDisabled: '&' }, require: ['datepicker', '?^ngModel'], controller: 'DatepickerController', link: function(scope, element, attrs, ctrls) { var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if ( ngModelCtrl ) { datepickerCtrl.init( ngModelCtrl ); } } }; }) .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: '@' }, 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; 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', attrs.dateDisabled); } // TODO: reverse from dateFilter string to Date object 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); if (!date || !angular.isDate(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.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); if (ngModel.$modelValue) { if (angular.isDate(ngModel.$modelValue)) { scope.date = ngModel.$modelValue; } else if (isNaN(scope.date = new Date(ngModel.$modelValue))) { scope.date = dateParser.parse(ngModel.$modelValue, dateFormat); } } else { scope.date = null; } }; var documentClickBind = function(event) { if (scope.isOpen && event.target !== element[0]) { scope.$apply(function() { scope.isOpen = false; }); } }; var openCalendar = function() { scope.$apply(function() { scope.isOpen = true; }); }; scope.$watch('isOpen', function(value) { if (value) { scope.position = appendToBody ? $position.offset(element) : $position.position(element); scope.position.top = scope.position.top + element.prop('offsetHeight'); $document.bind('click', documentClickBind); element.unbind('focus', openCalendar); element[0].focus(); } else { $document.unbind('click', documentClickBind); element.bind('focus', openCalendar); } }); 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 ); }; var $popup = $compile(popupEl)(scope); if ( appendToBody ) { $document.find('body').append($popup); } else { element.after($popup); } scope.$on('$destroy', function() { $popup.remove(); element.unbind('focus', openCalendar); $document.unbind('click', documentClickBind); }); } }; }]) .directive('datepickerPopupWrap', 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(); }); } }; });