UNPKG

angular-ui-bootstrap

Version:

Native AngularJS (Angular) directives for Bootstrap

686 lines (590 loc) 22.2 kB
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; date = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')); ngModelCtrl.$setValidity('dateDisabled', !date || this.element && !this.isDisabled(date)); } }; this.createDateObject = function(date, format) { var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; model = dateParser.fromTimezone(model, ngModelOptions.getOption('timezone')); var today = new Date(); today = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone')); var time = this.compare(date, today); var dt = { date: date, label: dateParser.filter(date, format), selected: model && this.compare(date, model) === 0, disabled: this.isDisabled(date), 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 $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 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.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.getOption('timezone')) : new Date(0, 0, 0, 0, 0, 0, 0); dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); dt = dateParser.toTimezone(dt, ngModelOptions.getOption('timezone')); ngModelCtrl.$setViewValue(dt); ngModelCtrl.$render(); } else { self.activeDate = date; setMode(self.modes[self.modes.indexOf($scope.datepickerMode) - 1]); $scope.$emit('uib:datepicker.mode'); } $scope.$broadcast('uib:datepicker.focus'); }; $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) { direction = direction || 1; if ($scope.datepickerMode === self.maxMode && direction === 1 || $scope.datepickerMode === self.minMode && direction === -1) { return; } 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' }; var focusElement = function() { self.element[0].focus(); }; // Listen for focus requests from popup directive $scope.$on('uib:datepicker.focus', focusElement); $scope.keydown = function(evt) { var key = $scope.keys[evt.which]; if (!key || evt.shiftKey || evt.altKey || $scope.disabled) { return; } evt.preventDefault(); if (!self.shortcutPropagation) { evt.stopPropagation(); } if (key === 'enter' || key === 'space') { if (self.isDisabled(self.activeDate)) { return; // do nothing } $scope.select(self.activeDate); } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { $scope.toggleMode(key === 'up' ? 1 : -1); } else { self.handleKeyDown(key, evt); self.refreshView(); } }; $element.on('keydown', function(evt) { $scope.$apply(function() { $scope.keydown(evt); }); }); $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; } function extractOptions(ngModelCtrl) { var ngModelOptions; if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing // guarantee a value ngModelOptions = ngModelCtrl.$options || $scope.datepickerOptions.ngModelOptions || datepickerConfig.ngModelOptions || {}; // mimic 1.6+ api ngModelOptions.getOption = function (key) { return ngModelOptions[key]; }; } else { // in angular >=1.6 $options is always present // ng-model-options defaults timezone to null; don't let its precedence squash a non-null value var timezone = ngModelCtrl.$options.getOption('timezone') || ($scope.datepickerOptions.ngModelOptions ? $scope.datepickerOptions.ngModelOptions.timezone : null) || (datepickerConfig.ngModelOptions ? datepickerConfig.ngModelOptions.timezone : null); // values passed to createChild override existing values ngModelOptions = ngModelCtrl.$options // start with a ModelOptions instance .createChild(datepickerConfig.ngModelOptions) // lowest precedence .createChild($scope.datepickerOptions.ngModelOptions) .createChild(ngModelCtrl.$options) // highest precedence .createChild({timezone: timezone}); // to keep from squashing a non-null value } return ngModelOptions; } }]) .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, this.monthColumns); scope.yearHeaderColspan = this.monthColumns > 3 ? this.monthColumns - 2 : 1; }; 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 - this.monthColumns; } else if (key === 'right') { date = date + 1; } else if (key === 'down') { date = date + this.monthColumns; } 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 { templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/datepicker/datepicker.html'; }, scope: { datepickerOptions: '=?' }, require: ['uibDatepicker', '^ngModel'], restrict: 'A', controller: 'UibDatepickerController', controllerAs: 'datepicker', link: function(scope, element, attrs, ctrls) { var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; datepickerCtrl.init(ngModelCtrl); } }; }) .directive('uibDaypicker', function() { return { templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/datepicker/day.html'; }, require: ['^uibDatepicker', 'uibDaypicker'], restrict: 'A', controller: 'UibDaypickerController', link: function(scope, element, attrs, ctrls) { var datepickerCtrl = ctrls[0], daypickerCtrl = ctrls[1]; daypickerCtrl.init(datepickerCtrl); } }; }) .directive('uibMonthpicker', function() { return { templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/datepicker/month.html'; }, require: ['^uibDatepicker', 'uibMonthpicker'], restrict: 'A', controller: 'UibMonthpickerController', link: function(scope, element, attrs, ctrls) { var datepickerCtrl = ctrls[0], monthpickerCtrl = ctrls[1]; monthpickerCtrl.init(datepickerCtrl); } }; }) .directive('uibYearpicker', function() { return { templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/datepicker/year.html'; }, require: ['^uibDatepicker', 'uibYearpicker'], restrict: 'A', controller: 'UibYearpickerController', link: function(scope, element, attrs, ctrls) { var ctrl = ctrls[0]; angular.extend(ctrl, ctrls[1]); ctrl.yearpickerInit(); ctrl.refreshView(); } }; });