UNPKG

angular-ui-bootstrap

Version:

Native AngularJS (Angular) directives for Bootstrap

467 lines (395 loc) 14.7 kB
angular.module('ui.bootstrap.datepickerPopup', ['ui.bootstrap.datepicker', 'ui.bootstrap.position']) .value('$datepickerPopupLiteralWarning', true) .constant('uibDatepickerPopupConfig', { altInputFormats: [], appendToBody: false, clearText: 'Clear', closeOnDateSelection: true, closeText: 'Done', currentText: 'Today', datepickerPopup: 'yyyy-MM-dd', datepickerPopupTemplateUrl: 'uib/template/datepickerPopup/popup.html', datepickerTemplateUrl: 'uib/template/datepicker/datepicker.html', html5Types: { date: 'yyyy-MM-dd', 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss', 'month': 'yyyy-MM' }, onOpenFocus: true, showButtonBar: true, placement: 'auto bottom-left' }) .controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$log', '$parse', '$window', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig', '$datepickerPopupLiteralWarning', function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig, $datepickerPopupLiteralWarning) { var cache = {}, isHtml5DateInput = false; var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus, datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, scrollParentEl, ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = []; this.init = function(_ngModel_) { ngModel = _ngModel_; ngModelOptions = extractOptions(ngModel); closeOnDateSelection = angular.isDefined($attrs.closeOnDateSelection) ? $scope.$parent.$eval($attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection; appendToBody = angular.isDefined($attrs.datepickerAppendToBody) ? $scope.$parent.$eval($attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; onOpenFocus = angular.isDefined($attrs.onOpenFocus) ? $scope.$parent.$eval($attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus; datepickerPopupTemplateUrl = angular.isDefined($attrs.datepickerPopupTemplateUrl) ? $attrs.datepickerPopupTemplateUrl : datepickerPopupConfig.datepickerPopupTemplateUrl; datepickerTemplateUrl = angular.isDefined($attrs.datepickerTemplateUrl) ? $attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl; altInputFormats = angular.isDefined($attrs.altInputFormats) ? $scope.$parent.$eval($attrs.altInputFormats) : datepickerPopupConfig.altInputFormats; $scope.showButtonBar = angular.isDefined($attrs.showButtonBar) ? $scope.$parent.$eval($attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; if (datepickerPopupConfig.html5Types[$attrs.type]) { dateFormat = datepickerPopupConfig.html5Types[$attrs.type]; isHtml5DateInput = true; } else { dateFormat = $attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup; $attrs.$observe('uibDatepickerPopup', function(value, oldValue) { var newDateFormat = value || datepickerPopupConfig.datepickerPopup; // Invalidate the $modelValue to ensure that formatters re-run // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764 if (newDateFormat !== dateFormat) { dateFormat = newDateFormat; ngModel.$modelValue = null; if (!dateFormat) { throw new Error('uibDatepickerPopup must have a date format specified.'); } } }); } if (!dateFormat) { throw new Error('uibDatepickerPopup must have a date format specified.'); } if (isHtml5DateInput && $attrs.uibDatepickerPopup) { throw new Error('HTML5 date input types do not support custom formats.'); } // popup element used to display calendar popupEl = angular.element('<div uib-datepicker-popup-wrap><div uib-datepicker></div></div>'); popupEl.attr({ 'ng-model': 'date', 'ng-change': 'dateSelection(date)', 'template-url': datepickerPopupTemplateUrl }); // datepicker element datepickerEl = angular.element(popupEl.children()[0]); datepickerEl.attr('template-url', datepickerTemplateUrl); if (!$scope.datepickerOptions) { $scope.datepickerOptions = {}; } if (isHtml5DateInput) { if ($attrs.type === 'month') { $scope.datepickerOptions.datepickerMode = 'month'; $scope.datepickerOptions.minMode = 'month'; } } datepickerEl.attr('datepicker-options', 'datepickerOptions'); if (!isHtml5DateInput) { // Internal API to maintain the correct ng-invalid-[key] class ngModel.$$parserName = 'date'; ngModel.$validators.date = validator; ngModel.$parsers.unshift(parseDate); ngModel.$formatters.push(function(value) { if (ngModel.$isEmpty(value)) { $scope.date = value; return value; } if (angular.isNumber(value)) { value = new Date(value); } $scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone')); return dateParser.filter($scope.date, dateFormat); }); } else { ngModel.$formatters.push(function(value) { $scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone')); return value; }); } // Detect changes in the view from the text box ngModel.$viewChangeListeners.push(function() { $scope.date = parseDateString(ngModel.$viewValue); }); $element.on('keydown', inputKeydownBind); $popup = $compile(popupEl)($scope); // Prevent jQuery cache memory leak (template is now redundant after linking) popupEl.remove(); if (appendToBody) { $document.find('body').append($popup); } else { $element.after($popup); } $scope.$on('$destroy', function() { if ($scope.isOpen === true) { if (!$rootScope.$$phase) { $scope.$apply(function() { $scope.isOpen = false; }); } } $popup.remove(); $element.off('keydown', inputKeydownBind); $document.off('click', documentClickBind); if (scrollParentEl) { scrollParentEl.off('scroll', positionPopup); } angular.element($window).off('resize', positionPopup); //Clear all watch listeners on destroy while (watchListeners.length) { watchListeners.shift()(); } }); }; $scope.getText = function(key) { return $scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; }; $scope.isDisabled = function(date) { if (date === 'today') { date = dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone')); } var dates = {}; angular.forEach(['minDate', 'maxDate'], function(key) { if (!$scope.datepickerOptions[key]) { dates[key] = null; } else if (angular.isDate($scope.datepickerOptions[key])) { dates[key] = new Date($scope.datepickerOptions[key]); } else { if ($datepickerPopupLiteralWarning) { $log.warn('Literal date support has been deprecated, please switch to date object usage'); } dates[key] = new Date(dateFilter($scope.datepickerOptions[key], 'medium')); } }); return $scope.datepickerOptions && dates.minDate && $scope.compare(date, dates.minDate) < 0 || dates.maxDate && $scope.compare(date, dates.maxDate) > 0; }; $scope.compare = function(date1, date2) { return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); }; // Inner change $scope.dateSelection = function(dt) { $scope.date = dt; var date = $scope.date ? dateParser.filter($scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function $element.val(date); ngModel.$setViewValue(date); if (closeOnDateSelection) { $scope.isOpen = false; $element[0].focus(); } }; $scope.keydown = function(evt) { if (evt.which === 27) { evt.stopPropagation(); $scope.isOpen = false; $element[0].focus(); } }; $scope.select = function(date, evt) { evt.stopPropagation(); if (date === 'today') { var today = new Date(); if (angular.isDate($scope.date)) { date = new Date($scope.date); date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); } else { date = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone')); date.setHours(0, 0, 0, 0); } } $scope.dateSelection(date); }; $scope.close = function(evt) { evt.stopPropagation(); $scope.isOpen = false; $element[0].focus(); }; $scope.disabled = angular.isDefined($attrs.disabled) || false; if ($attrs.ngDisabled) { watchListeners.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(disabled) { $scope.disabled = disabled; })); } $scope.$watch('isOpen', function(value) { if (value) { if (!$scope.disabled) { $timeout(function() { positionPopup(); if (onOpenFocus) { $scope.$broadcast('uib:datepicker.focus'); } $document.on('click', documentClickBind); var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement; if (appendToBody || $position.parsePlacement(placement)[2]) { scrollParentEl = scrollParentEl || angular.element($position.scrollParent($element)); if (scrollParentEl) { scrollParentEl.on('scroll', positionPopup); } } else { scrollParentEl = null; } angular.element($window).on('resize', positionPopup); }, 0, false); } else { $scope.isOpen = false; } } else { $document.off('click', documentClickBind); if (scrollParentEl) { scrollParentEl.off('scroll', positionPopup); } angular.element($window).off('resize', positionPopup); } }); function cameltoDash(string) { return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); } function parseDateString(viewValue) { var date = dateParser.parse(viewValue, dateFormat, $scope.date); if (isNaN(date)) { for (var i = 0; i < altInputFormats.length; i++) { date = dateParser.parse(viewValue, altInputFormats[i], $scope.date); if (!isNaN(date)) { return date; } } } return date; } function parseDate(viewValue) { if (angular.isNumber(viewValue)) { // presumably timestamp to date object viewValue = new Date(viewValue); } if (!viewValue) { return null; } if (angular.isDate(viewValue) && !isNaN(viewValue)) { return viewValue; } if (angular.isString(viewValue)) { var date = parseDateString(viewValue); if (!isNaN(date)) { return dateParser.toTimezone(date, ngModelOptions.getOption('timezone')); } } return ngModelOptions.getOption('allowInvalid') ? viewValue : undefined; } function validator(modelValue, viewValue) { var value = modelValue || viewValue; if (!$attrs.ngRequired && !value) { return true; } if (angular.isNumber(value)) { value = new Date(value); } if (!value) { return true; } if (angular.isDate(value) && !isNaN(value)) { return true; } if (angular.isString(value)) { return !isNaN(parseDateString(value)); } return false; } function documentClickBind(event) { if (!$scope.isOpen && $scope.disabled) { return; } var popup = $popup[0]; var dpContainsTarget = $element[0].contains(event.target); // The popup node may not be an element node // In some browsers (IE) only element nodes have the 'contains' function var popupContainsTarget = popup.contains !== undefined && popup.contains(event.target); if ($scope.isOpen && !(dpContainsTarget || popupContainsTarget)) { $scope.$apply(function() { $scope.isOpen = false; }); } } function inputKeydownBind(evt) { if (evt.which === 27 && $scope.isOpen) { evt.preventDefault(); evt.stopPropagation(); $scope.$apply(function() { $scope.isOpen = false; }); $element[0].focus(); } else if (evt.which === 40 && !$scope.isOpen) { evt.preventDefault(); evt.stopPropagation(); $scope.$apply(function() { $scope.isOpen = true; }); } } function positionPopup() { if ($scope.isOpen) { var dpElement = angular.element($popup[0].querySelector('.uib-datepicker-popup')); var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement; var position = $position.positionElements($element, dpElement, placement, appendToBody); dpElement.css({top: position.top + 'px', left: position.left + 'px'}); if (dpElement.hasClass('uib-position-measure')) { dpElement.removeClass('uib-position-measure'); } } } function extractOptions(ngModelCtrl) { var ngModelOptions; if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing // guarantee a value ngModelOptions = angular.isObject(ngModelCtrl.$options) ? ngModelCtrl.$options : { timezone: null }; // mimic 1.6+ api ngModelOptions.getOption = function (key) { return ngModelOptions[key]; }; } else { // in angular >=1.6 $options is always present ngModelOptions = ngModelCtrl.$options; } return ngModelOptions; } $scope.$on('uib:datepicker.mode', function() { $timeout(positionPopup, 0, false); }); }]) .directive('uibDatepickerPopup', function() { return { require: ['ngModel', 'uibDatepickerPopup'], controller: 'UibDatepickerPopupController', scope: { datepickerOptions: '=?', isOpen: '=?', currentText: '@', clearText: '@', closeText: '@' }, link: function(scope, element, attrs, ctrls) { var ngModel = ctrls[0], ctrl = ctrls[1]; ctrl.init(ngModel); } }; }) .directive('uibDatepickerPopupWrap', function() { return { restrict: 'A', transclude: true, templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/datepickerPopup/popup.html'; } }; });