From fbc873cfb2523984d593a1b9f0f64ab8d13c7842 Mon Sep 17 00:00:00 2001 From: Wesley Cho Date: Tue, 22 Mar 2016 07:04:40 -0700 Subject: [PATCH] feat(datepickerPopup): split off popup from datepicker - Split off datepicker popup into new datepickerPopup module BREAKING CHANGE: The datepicker popup is no longer a part of the datepicker module, but now a part of the datepickerPopup module. Please change code accordingly if including specific modules Closes #5676 --- src/datepicker/datepicker.css | 10 - src/datepicker/datepicker.js | 461 +---- src/datepicker/docs/readme.md | 139 +- src/datepicker/index.js | 4 +- src/datepicker/test/datepicker.spec.js | 1555 ---------------- src/datepickerPopup/docs/demo.html | 47 + src/datepickerPopup/docs/demo.js | 95 + src/datepickerPopup/docs/readme.md | 102 + src/datepickerPopup/index.js | 11 + src/datepickerPopup/popup.css | 9 + src/datepickerPopup/popup.js | 453 +++++ src/datepickerPopup/test/popup.spec.js | 1656 +++++++++++++++++ .../popup.html | 0 13 files changed, 2396 insertions(+), 2146 deletions(-) create mode 100644 src/datepickerPopup/docs/demo.html create mode 100644 src/datepickerPopup/docs/demo.js create mode 100644 src/datepickerPopup/docs/readme.md create mode 100644 src/datepickerPopup/index.js create mode 100644 src/datepickerPopup/popup.css create mode 100644 src/datepickerPopup/popup.js create mode 100644 src/datepickerPopup/test/popup.spec.js rename template/{datepicker => datepickerPopup}/popup.html (100%) diff --git a/src/datepicker/datepicker.css b/src/datepicker/datepicker.css index fcb0f36907..c49e180968 100644 --- a/src/datepicker/datepicker.css +++ b/src/datepicker/datepicker.css @@ -6,16 +6,6 @@ min-width: 100%; } -.uib-datepicker-popup.dropdown-menu { - display: block; - float: none; - margin: 0; -} - -.uib-button-bar { - padding: 10px 9px 2px; -} - .uib-left, .uib-right { width: 100% } diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index dd81bac35f..143955926e 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -1,7 +1,6 @@ -angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.isClass', 'ui.bootstrap.position']) +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.isClass']) .value('$datepickerSuppressError', false) -.value('uibDatepickerAttributeWarning', true) .constant('uibDatepickerConfig', { datepickerMode: 'day', @@ -22,8 +21,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst yearRows: 4 }) -.controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerSuppressError', 'uibDatepickerAttributeWarning', 'uibDateParser', - function($scope, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerSuppressError, datepickerAttributeWarning, dateParser) { +.controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerSuppressError', 'uibDateParser', + function($scope, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerSuppressError, dateParser) { var self = this, ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl; ngModelOptions = {}, @@ -632,458 +631,4 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst ctrl.refreshView(); } }; -}) - -.value('uibDatepickerPopupAttributeWarning', true) - -.constant('uibDatepickerPopupConfig', { - altInputFormats: [], - appendToBody: false, - clearText: 'Clear', - closeOnDateSelection: true, - closeText: 'Done', - currentText: 'Today', - datepickerPopup: 'yyyy-MM-dd', - datepickerPopupTemplateUrl: 'uib/template/datepicker/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', 'uibDatepickerPopupAttributeWarning', -function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig, datepickerPopupAttributeWarning) { - var cache = {}, - isHtml5DateInput = false; - var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus, - datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, scrollParentEl, - ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = [], - timezone; - - this.init = function(_ngModel_) { - ngModel = _ngModel_; - ngModelOptions = _ngModel_.$options; - 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('
'); - if (ngModelOptions) { - timezone = ngModelOptions.timezone; - $scope.ngModelOptions = angular.copy(ngModelOptions); - $scope.ngModelOptions.timezone = null; - if ($scope.ngModelOptions.updateOnDefault === true) { - $scope.ngModelOptions.updateOn = $scope.ngModelOptions.updateOn ? - $scope.ngModelOptions.updateOn + ' default' : 'default'; - } - - popupEl.attr('ng-model-options', 'ngModelOptions'); - } else { - timezone = null; - } - - 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; - } - - $scope.date = dateParser.fromTimezone(value, timezone); - - if (angular.isNumber($scope.date)) { - $scope.date = new Date($scope.date); - } - - return dateParser.filter($scope.date, dateFormat); - }); - } else { - ngModel.$formatters.push(function(value) { - $scope.date = dateParser.fromTimezone(value, 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(), timezone); - } - - var dates = {}; - angular.forEach(['minDate', 'maxDate'], function(key) { - if ($scope.datepickerOptions[key] === null) { - dates[key] = null; - } else if (angular.isDate($scope.datepickerOptions[key])) { - dates[key] = dateParser.fromTimezone(new Date($scope.datepickerOptions[key]), timezone); - } else { - 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) { - if (angular.isDefined(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 = new Date(today.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, timezone); - } - } - - return ngModel.$options && ngModel.$options.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(viewValue)); - } - - 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'); - } - } - } - - $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 { - replace: true, - transclude: true, - templateUrl: function(element, attrs) { - return attrs.templateUrl || 'uib/template/datepicker/popup.html'; - } - }; }); diff --git a/src/datepicker/docs/readme.md b/src/datepicker/docs/readme.md index dd341c5e74..af88960824 100644 --- a/src/datepicker/docs/readme.md +++ b/src/datepicker/docs/readme.md @@ -2,8 +2,6 @@ Our datepicker is flexible and fully customizable. You can navigate through days, months and years. -It comes in two formats, an inline `uib-datepicker` and an `uib-datepicker-popup` to be embedded in an input. - The datepicker has 3 modes: * `day` - In this mode you're presented with a 6-week calendar for a specified month. @@ -24,11 +22,11 @@ The datepicker has 3 modes: Supported [angular ngModelOptions](https://docs.angularjs.org/api/ng/directive/ngModelOptions): * allowInvalid * timezone - + * `template-url` _(Default: `uib/template/datepicker/datepicker.html`)_ - Add the ability to override the template used on the component. - + Apart from the previous settings, to configure the uib-datepicker you need to create an object in Javascript with all the options and use it on the `datepicker-options` attribute: * `datepicker-options` @@ -37,85 +35,85 @@ Apart from the previous settings, to configure the uib-datepicker you need to cr * `customClass (date, mode)` - An optional expression to add classes based on passing a date and current mode. - + * `dateDisabled (date, mode)` - An optional expression to disable visible options based on passing a date and current mode. - + * `datepickerMode` C _(Default: `day`)_ - Current mode of the datepicker _(day|month|year)_. Can be used to initialize the datepicker in a specific mode. - + * `formatDay` C _(Default: `dd`)_ - Format of day in month. - + * `formatMonth` C _(Default: `MMMM`)_ - Format of month in year. - + * `formatYear` C _(Default: `yyyy`)_ - Format of year in year range. - + * `formatDayHeader` C _(Default: `EEE`)_ - Format of day in week header. - + * `formatDayTitle` C _(Default: `MMMM yyyy`)_ - Format of title when selecting day. - + * `formatMonthTitle` C _(Default: `yyyy`)_ - Format of title when selecting month. - + * `initDate` _(Default: `null`)_ - The initial date view when no model value is specified. - + * `maxDate` C _(Default: `null`)_ - Defines the maximum available date. - + * `maxMode` C _(Default: `year`)_ - Sets an upper limit for mode. - + * `minDate` C _(Default: `null`)_ - Defines the minimum available date. - + * `minMode` C _(Default: `day`)_ - Sets a lower limit for mode. - + * `shortcutPropagation` C _(Default: `false`)_ - An option to disable the propagation of the keydown event. - + * `showWeeks` C _(Default: `true`)_ - Whether to display week numbers. - + * `startingDay` C *(Default: `$locale.DATETIME_FORMATS.FIRSTDAYOFWEEK`)* - @@ -125,109 +123,12 @@ Apart from the previous settings, to configure the uib-datepicker you need to cr C _(Default: `4`)_ - Number of rows displayed in year selection. - + * `yearColumns` C _(Default: `5`)_ - Number of columns displayed in year selection. -### uib-datepicker-popup settings - -The popup is a wrapper that you can use in an input to toggle a datepicker. To configure the datepicker, use `datepicker-options`. - -* `alt-input-formats` - $ - C - _(Default: `[]`)_ - - A list of alternate formats acceptable for manual entry. - -* `clear-text` - C - _(Default: `Clear`)_ - - The text to display for the clear button. - -* `close-on-date-selection` - $ - C - _(Default: `true`)_ - - Whether to close calendar when a date is chosen. - -* `close-text` - C - _(Default: `Done`)_ - - The text to display for the close button. - -* `current-text` - C - _(Default: `Today`)_ - - The text to display for the current day button. - -* `datepicker-append-to-body` - $ - C - _(Default: `false`, Config: `appendToBody`)_ - - Append the datepicker popup element to `body`, rather than inserting after `datepicker-popup`. - -* `datepicker-options` - $ - - An object with any combination of the datepicker settings (in camelCase) used to configure the wrapped datepicker. - -* `datepicker-popup-template-url` - C - _(Default: `uib/template/datepicker/popup.html`)_ - - Add the ability to override the template used on the component. - -* `datepicker-template-url` - C - _(Default: `uib/template/datepicker/datepicker.html`)_ - - Add the ability to override the template used on the component (inner uib-datepicker). - -* `is-open` - $ - - _(Default: `false`)_ - - Whether or not to show the datepicker. - -* `on-open-focus` - $ - C - _(Default: `true`)_ - - Whether or not to focus the datepicker popup upon opening. - -* `show-button-bar` - $ - C - _(Default: `true`)_ - - Whether or not to display a button bar underneath the uib-datepicker. - -* `type` - C - _(Default: `text`, Config: `html5Types`)_ - - You can override the input type to be _(date|datetime-local|month)_. That will change the date format of the popup. - -* `popup-placement` - C - _(Default: `auto bottom-left`, Config: 'placement')_ - - Passing in 'auto' separated by a space before the placement will enable auto positioning, e.g: "auto bottom-left". The popup will attempt to position where it fits in the closest scrollable ancestor. Accepts: - - * `top` - popup on top, horizontally centered on input element. - * `top-left` - popup on top, left edge aligned with input element left edge. - * `top-right` - popup on top, right edge aligned with input element right edge. - * `bottom` - popup on bottom, horizontally centered on input element. - * `bottom-left` - popup on bottom, left edge aligned with input element left edge. - * `bottom-right` - popup on bottom, right edge aligned with input element right edge. - * `left` - popup on left, vertically centered on input element. - * `left-top` - popup on left, top edge aligned with input element top edge. - * `left-bottom` - popup on left, bottom edge aligned with input element bottom edge. - * `right` - popup on right, vertically centered on input element. - * `right-top` - popup on right, top edge aligned with input element top edge. - * `right-bottom` - popup on right, bottom edge aligned with input element bottom edge. - -* `uib-datepicker-popup` - C - _(Default: `yyyy-MM-dd`, Config: `datepickerConfig`)_ - - The format for displayed dates. This string can take string literals by surrounding the value with single quotes, i.e. `yyyy-MM-dd h 'o\'clock'`. - ### Keyboard support Depending on datepicker's current mode, the date may refer either to day, month or year. Accordingly, the term view refers either to a month, year or year range. @@ -248,5 +149,3 @@ Depending on datepicker's current mode, the date may refer either to day, month **Notes** If the date a user enters falls outside of the min-/max-date range, a `dateDisabled` validation error will show on the form. - -If using this directive on input type date, a native browser datepicker could also appear. diff --git a/src/datepicker/index.js b/src/datepicker/index.js index 3020420699..4155ba8cc3 100644 --- a/src/datepicker/index.js +++ b/src/datepicker/index.js @@ -1,17 +1,15 @@ require('../dateparser'); require('../isClass'); -require('../position'); require('../../template/datepicker/datepicker.html.js'); require('../../template/datepicker/day.html.js'); require('../../template/datepicker/month.html.js'); require('../../template/datepicker/year.html.js'); -require('../../template/datepicker/popup.html.js'); require('./datepicker'); require('./datepicker.css'); var MODULE_NAME = 'ui.bootstrap.module.datepicker'; -angular.module(MODULE_NAME, ['ui.bootstrap.datepicker', 'uib/template/datepicker/datepicker.html', 'uib/template/datepicker/day.html', 'uib/template/datepicker/month.html', 'uib/template/datepicker/year.html', 'uib/template/datepicker/popup.html']); +angular.module(MODULE_NAME, ['ui.bootstrap.datepicker', 'uib/template/datepicker/datepicker.html', 'uib/template/datepicker/day.html', 'uib/template/datepicker/month.html', 'uib/template/datepicker/year.html']); module.exports = MODULE_NAME; diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index 77f9e6e41b..87c605e4da 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -5,7 +5,6 @@ describe('datepicker', function() { beforeEach(module('uib/template/datepicker/day.html')); beforeEach(module('uib/template/datepicker/month.html')); beforeEach(module('uib/template/datepicker/year.html')); - beforeEach(module('uib/template/datepicker/popup.html')); beforeEach(module(function($compileProvider) { $compileProvider.directive('dateModel', function() { return { @@ -1282,83 +1281,6 @@ describe('datepicker', function() { }); }); - describe('basic popup', function() { - var wrapElement, inputEl, dropdownEl; - - function assignElements(wrapElement) { - inputEl = wrapElement.find('input'); - dropdownEl = wrapElement.find('ul'); - element = dropdownEl.find('table'); - } - - beforeEach(function() { - $rootScope.date = new Date('September 30, 2010 15:30:00'); - $rootScope.isopen = true; - wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - }); - - it('should stop click event from bubbling from today button', function() { - var bubbled = false; - wrapElement.on('click', function() { - bubbled = true; - }); - - wrapElement.find('.uib-datepicker-current').trigger('click'); - - expect(bubbled).toBe(false); - }); - - it('should stop click event from bubbling from clear button', function() { - var bubbled = false; - wrapElement.on('click', function() { - bubbled = true; - }); - - wrapElement.find('.uib-clear').trigger('click'); - - expect(bubbled).toBe(false); - }); - - it('should stop click event from bubbling from close button', function() { - var bubbled = false; - wrapElement.on('click', function() { - bubbled = true; - }); - - wrapElement.find('.uib-close').trigger('click'); - - expect(bubbled).toBe(false); - }); - }); - - describe('ngModelOptions allowInvalid', function() { - var $sniffer, inputEl; - - beforeEach(inject(function(_$sniffer_) { - $sniffer = _$sniffer_; - - $rootScope.date = new Date('September 30, 2010 15:30:00'); - $rootScope.modelOptions = {allowInvalid: true}; - element = $compile('
')($rootScope); - inputEl = element.find('input'); - $rootScope.$digest(); - })); - - function changeInputValueTo(el, value) { - el.val(value); - el.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); - $rootScope.$digest(); - } - - it('should update ng-model even if the date is invalid when allowInvalid is true', function() { - changeInputValueTo(inputEl, 'pizza'); - expect($rootScope.date).toBe('pizza'); - expect(inputEl.val()).toBe('pizza'); - }); - }); - describe('datepickerConfig ngModelOptions', function() { describe('timezone', function() { var originalConfig = {}; @@ -1432,1483 +1354,6 @@ describe('datepicker', function() { }); }); - describe('setting datepickerPopupConfig', function() { - var originalConfig = {}; - beforeEach(inject(function(uibDatepickerPopupConfig) { - angular.extend(originalConfig, uibDatepickerPopupConfig); - uibDatepickerPopupConfig.datepickerPopup = 'MM-dd-yyyy'; - - element = $compile('')($rootScope); - $rootScope.$digest(); - })); - afterEach(inject(function(uibDatepickerPopupConfig) { - // return it to the original state - angular.extend(uibDatepickerPopupConfig, originalConfig); - })); - - it('changes date format', function() { - expect(element.val()).toEqual('09-30-2010'); - }); - - }); - - describe('setting datepickerPopupConfig inside ng-if', function() { - var originalConfig = {}; - beforeEach(inject(function (uibDatepickerPopupConfig) { - angular.extend(originalConfig, uibDatepickerPopupConfig); - uibDatepickerPopupConfig.datepickerPopup = 'MM-dd-yyyy'; - - element = $compile('
')($rootScope); - $rootScope.$digest(); - })); - afterEach(inject(function (uibDatepickerPopupConfig) { - // return it to the original state - angular.extend(uibDatepickerPopupConfig, originalConfig); - })); - - it('changes date format', function () { - expect(element.find('input').val()).toEqual('09-30-2010'); - }); - }); - - describe('as popup', function () { - var inputEl, dropdownEl, $document, $sniffer, $timeout; - - function assignElements(wrapElement) { - inputEl = wrapElement.find('input'); - dropdownEl = wrapElement.find('ul'); - element = dropdownEl.find('table'); - } - - function changeInputValueTo(el, value) { - el.val(value); - el.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); - $rootScope.$digest(); - } - - describe('initially', function () { - beforeEach(inject(function(_$document_, _$sniffer_) { - $document = _$document_; - $sniffer = _$sniffer_; - $rootScope.isopen = true; - $rootScope.date = new Date('September 30, 2010 15:30:00'); - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('does not to display datepicker initially', function() { - expect(dropdownEl.length).toBe(0); - }); - - it('to display the correct value in input', function() { - expect(inputEl.val()).toBe('2010-09-30'); - }); - }); - - describe('initially opened', function() { - var wrapElement; - - beforeEach(inject(function(_$document_, _$sniffer_, _$timeout_) { - $document = _$document_; - $sniffer = _$sniffer_; - $timeout = _$timeout_; - $rootScope.isopen = true; - $rootScope.date = new Date('September 30, 2010 15:30:00'); - wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('datepicker is displayed', function() { - expect(dropdownEl.length).toBe(1); - }); - - it('renders the calendar correctly', function() { - expect(getLabelsRow().css('display')).not.toBe('none'); - expect(getLabels(true)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); - expect(getOptions(true)).toEqual([ - ['29', '30', '31', '01', '02', '03', '04'], - ['05', '06', '07', '08', '09', '10', '11'], - ['12', '13', '14', '15', '16', '17', '18'], - ['19', '20', '21', '22', '23', '24', '25'], - ['26', '27', '28', '29', '30', '01', '02'], - ['03', '04', '05', '06', '07', '08', '09'] - ]); - }); - - it('updates the input when a day is clicked', function() { - clickOption(17); - expect(inputEl.val()).toBe('2010-09-15'); - expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); - }); - - it('should mark the input field dirty when a day is clicked', function() { - expect(inputEl).toHaveClass('ng-pristine'); - clickOption(17); - expect(inputEl).toHaveClass('ng-dirty'); - }); - - it('updates the input correctly when model changes', function() { - $rootScope.date = new Date('January 10, 1983 10:00:00'); - $rootScope.$digest(); - expect(inputEl.val()).toBe('1983-01-10'); - }); - - it('closes the dropdown when a day is clicked', function() { - expect(dropdownEl.length).toBe(1); - - clickOption(17); - assignElements(wrapElement); - expect(dropdownEl.length).toBe(0); - }); - - it('updates the model & calendar when input value changes', function() { - changeInputValueTo(inputEl, '2010-09-15'); - - expect($rootScope.date.getFullYear()).toEqual(2010); - expect($rootScope.date.getMonth()).toEqual(8); - expect($rootScope.date.getDate()).toEqual(15); - - expect(getOptions(true)).toEqual([ - ['29', '30', '31', '01', '02', '03', '04'], - ['05', '06', '07', '08', '09', '10', '11'], - ['12', '13', '14', '15', '16', '17', '18'], - ['19', '20', '21', '22', '23', '24', '25'], - ['26', '27', '28', '29', '30', '01', '02'], - ['03', '04', '05', '06', '07', '08', '09'] - ]); - expectSelectedElement(17); - }); - - it('closes when click outside of calendar', function() { - expect(dropdownEl.length).toBe(1); - - $timeout.flush(0); - $document.find('body').click(); - assignElements(wrapElement); - expect(dropdownEl.length).toBe(0); - }); - - it('sets `ng-invalid` for invalid input', function() { - changeInputValueTo(inputEl, 'pizza'); - - expect(inputEl).toHaveClass('ng-invalid'); - expect(inputEl).toHaveClass('ng-invalid-date'); - expect($rootScope.date).toBeUndefined(); - expect(inputEl.val()).toBe('pizza'); - }); - - it('unsets `ng-invalid` for valid input', function() { - changeInputValueTo(inputEl, 'pizza'); - expect(inputEl).toHaveClass('ng-invalid-date'); - - $rootScope.date = new Date('August 11, 2013'); - $rootScope.$digest(); - expect(inputEl).not.toHaveClass('ng-invalid'); - expect(inputEl).not.toHaveClass('ng-invalid-date'); - }); - - describe('focus', function () { - beforeEach(function() { - var body = $document.find('body'); - body.append(inputEl); - body.append(dropdownEl); - }); - - afterEach(function() { - inputEl.remove(); - dropdownEl.remove(); - }); - - it('returns to the input when ESC key is pressed in the popup and closes', function() { - expect(dropdownEl.length).toBe(1); - - dropdownEl.find('button').eq(0).focus(); - expect(document.activeElement.tagName).toBe('BUTTON'); - - triggerKeyDown(dropdownEl, 'esc'); - assignElements(wrapElement); - expect(dropdownEl.length).toBe(0); - expect(document.activeElement.tagName).toBe('INPUT'); - }); - - it('returns to the input when ESC key is pressed in the input and closes', function() { - expect(dropdownEl.length).toBe(1); - - dropdownEl.find('button').eq(0).focus(); - expect(document.activeElement.tagName).toBe('BUTTON'); - - triggerKeyDown(inputEl, 'esc'); - $rootScope.$digest(); - assignElements(wrapElement); - expect(dropdownEl.length).toBe(0); - expect(document.activeElement.tagName).toBe('INPUT'); - }); - - it('stops the ESC key from propagating if the dropdown is open, but not when closed', function() { - var documentKey = -1; - var getKey = function(evt) { documentKey = evt.which; }; - $document.bind('keydown', getKey); - - triggerKeyDown(inputEl, 'esc'); - expect(documentKey).toBe(-1); - - triggerKeyDown(inputEl, 'esc'); - expect(documentKey).toBe(27); - - $document.unbind('keydown', getKey); - }); - }); - - describe('works with HTML5 date input types', function() { - var date2 = new Date('October 1, 2010 12:34:56.789'); - beforeEach(inject(function(_$document_) { - $document = _$document_; - $rootScope.isopen = true; - $rootScope.date = new Date('September 30, 2010 15:30:00'); - })); - - it('works as date', function() { - setupInputWithType('date'); - expect(dropdownEl.length).toBe(1); - expect(inputEl.val()).toBe('2010-09-30'); - - changeInputValueTo(inputEl, '1980-03-05'); - - expect($rootScope.date.getFullYear()).toEqual(1980); - expect($rootScope.date.getMonth()).toEqual(2); - expect($rootScope.date.getDate()).toEqual(5); - - expect(getOptions(true)).toEqual([ - ['24', '25', '26', '27', '28', '29', '01'], - ['02', '03', '04', '05', '06', '07', '08'], - ['09', '10', '11', '12', '13', '14', '15'], - ['16', '17', '18', '19', '20', '21', '22'], - ['23', '24', '25', '26', '27', '28', '29'], - ['30', '31', '01', '02', '03', '04', '05'] - ]); - expect(selectedElementIndex()).toEqual(10); - }); - - it('works as datetime-local', function() { - setupInputWithType('datetime-local'); - expect(inputEl.val()).toBe('2010-09-30T15:30:00.000'); - - changeInputValueTo(inputEl, '1980-03-05T12:34:56.000'); - - expect($rootScope.date.getFullYear()).toEqual(1980); - expect($rootScope.date.getMonth()).toEqual(2); - expect($rootScope.date.getDate()).toEqual(5); - - expect(getOptions(true)).toEqual([ - ['24', '25', '26', '27', '28', '29', '01'], - ['02', '03', '04', '05', '06', '07', '08'], - ['09', '10', '11', '12', '13', '14', '15'], - ['16', '17', '18', '19', '20', '21', '22'], - ['23', '24', '25', '26', '27', '28', '29'], - ['30', '31', '01', '02', '03', '04', '05'] - ]); - expect(selectedElementIndex()).toEqual(10); - }); - - it('works as month', function() { - setupInputWithType('month'); - expect(inputEl.val()).toBe('2010-09'); - - changeInputValueTo(inputEl, '1980-03'); - - expect($rootScope.date.getFullYear()).toEqual(1980); - expect($rootScope.date.getMonth()).toEqual(2); - expect($rootScope.date.getDate()).toEqual(30); - - expect(getOptions()).toEqual([ - ['January', 'February', 'March'], - ['April', 'May', 'June'], - ['July', 'August', 'September'], - ['October', 'November', 'December'] - ]); - expect(selectedElementIndex()).toEqual(2); - }); - - function setupInputWithType(type) { - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - } - }); - }); - - describe('works with ngModelOptions', function() { - var $timeout; - - beforeEach(inject(function(_$document_, _$sniffer_, _$timeout_) { - $document = _$document_; - $timeout = _$timeout_; - $rootScope.isopen = true; - $rootScope.date = new Date('September 30, 2010 15:30:00'); - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('should change model and update calendar after debounce timeout', function() { - changeInputValueTo(inputEl, '1980-03-05'); - - expect($rootScope.date.getFullYear()).toEqual(2010); - expect($rootScope.date.getMonth()).toEqual(9 - 1); - expect($rootScope.date.getDate()).toEqual(30); - - expect(getOptions(true)).toEqual([ - ['29', '30', '31', '01', '02', '03', '04'], - ['05', '06', '07', '08', '09', '10', '11'], - ['12', '13', '14', '15', '16', '17', '18'], - ['19', '20', '21', '22', '23', '24', '25'], - ['26', '27', '28', '29', '30', '01', '02'], - ['03', '04', '05', '06', '07', '08', '09'] - ]); - - // No changes yet - $timeout.flush(2000); - expect($rootScope.date.getFullYear()).toEqual(2010); - expect($rootScope.date.getMonth()).toEqual(9 - 1); - expect($rootScope.date.getDate()).toEqual(30); - - expect(getOptions(true)).toEqual([ - ['29', '30', '31', '01', '02', '03', '04'], - ['05', '06', '07', '08', '09', '10', '11'], - ['12', '13', '14', '15', '16', '17', '18'], - ['19', '20', '21', '22', '23', '24', '25'], - ['26', '27', '28', '29', '30', '01', '02'], - ['03', '04', '05', '06', '07', '08', '09'] - ]); - - $timeout.flush(10000); - expect($rootScope.date.getFullYear()).toEqual(1980); - expect($rootScope.date.getMonth()).toEqual(2); - expect($rootScope.date.getDate()).toEqual(5); - - expect(getOptions(true)).toEqual([ - ['24', '25', '26', '27', '28', '29', '01'], - ['02', '03', '04', '05', '06', '07', '08'], - ['09', '10', '11', '12', '13', '14', '15'], - ['16', '17', '18', '19', '20', '21', '22'], - ['23', '24', '25', '26', '27', '28', '29'], - ['30', '31', '01', '02', '03', '04', '05'] - ]); - expectSelectedElement( 10 ); - }); - }); - - describe('works with ngModelOptions updateOn : "default"', function() { - var $timeout, wrapElement; - - beforeEach(inject(function(_$document_, _$sniffer_, _$timeout_) { - $document = _$document_; - $timeout = _$timeout_; - $rootScope.isopen = true; - $rootScope.date = new Date('2010-09-30T10:00:00.000Z'); - wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('should close the popup and update the input when a day is clicked', function() { - clickOption(17); - assignElements(wrapElement); - expect(dropdownEl.length).toBe(0); - expect(inputEl.val()).toBe('2010-09-15'); - expect($rootScope.date).toEqual(new Date('2010-09-15T10:00:00.000Z')); - }); - }); - - describe('attribute `datepickerOptions`', function() { - describe('show-weeks', function() { - beforeEach(function() { - $rootScope.opts = { - showWeeks: false - }; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - }); - - it('hides week numbers based on variable', function() { - expect(getLabelsRow().find('th').length).toEqual(7); - var tr = element.find('tbody').find('tr'); - for (var i = 0; i < 5; i++) { - expect(tr.eq(i).find('td').length).toEqual(7); - } - }); - }); - - describe('init-date', function(){ - beforeEach(function() { - $rootScope.date = null; - $rootScope.opts = { - initDate: new Date('November 9, 1980') - }; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - }); - - it('does not alter the model', function() { - expect($rootScope.date).toBe(null); - }); - - it('shows the correct title', function() { - expect(getTitle()).toBe('November 1980'); - }); - }); - - describe('min-date', function() { - it('should be able to specify a min-date through options', function() { - $rootScope.opts = { - minDate: new Date('September 12, 2010'), - shortcutPropagation: 'dog' - }; - - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - - var buttons = getAllOptionsEl(); - angular.forEach(buttons, function(button, index) { - expect(angular.element(button).prop('disabled')).toBe(index < 14); - }); - - $rootScope.opts.minDate = new Date('September 13, 2010'); - $rootScope.$digest(); - buttons = getAllOptionsEl(); - angular.forEach(buttons, function(button, index) { - expect(angular.element(button).prop('disabled')).toBe(index < 15); - }); - }); - }); - - describe('max-date', function() { - it('should be able to specify a max-date through options', function() { - $rootScope.opts = { - maxDate: new Date('September 25, 2010') - }; - - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - - var buttons = getAllOptionsEl(); - angular.forEach(buttons, function(button, index) { - expect(angular.element(button).prop('disabled')).toBe(index > 27); - }); - - $rootScope.opts.maxDate = new Date('September 15, 2010'); - $rootScope.$digest(); - buttons = getAllOptionsEl(); - angular.forEach(buttons, function(button, index) { - expect(angular.element(button).prop('disabled')).toBe(index > 17); - }); - }); - }); - - describe('min-mode', function() { - it('should be able to specify min-mode through options', function() { - $rootScope.opts = { - minMode: 'month' - }; - - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - - expect(getTitle()).toBe('2010'); - }); - }); - - describe('max-mode', function() { - it('should be able to specify max-mode through options', function() { - $rootScope.opts = { - maxMode: 'month' - }; - - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - - expect(getTitle()).toBe('September 2010'); - clickTitleButton(); - assignElements(wrapElement); - expect(getTitle()).toBe('2010'); - clickTitleButton(); - assignElements(wrapElement); - expect(getTitle()).toBe('2010'); - }); - }); - - describe('datepicker-mode', function() { - beforeEach(inject(function() { - $rootScope.date = new Date('August 11, 2013'); - $rootScope.opts = { - datepickerMode: 'month' - }; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('shows the correct title', function() { - expect(getTitle()).toBe('2013'); - }); - - it('updates binding', function() { - clickTitleButton(); - expect($rootScope.opts.datepickerMode).toBe('year'); - }); - }); - }); - - describe('option `init-date`', function() { - beforeEach(function() { - $rootScope.date = null; - $rootScope.options = { - initDate: new Date('November 9, 1980') - }; - }); - - describe('when initially set', function() { - beforeEach(function() { - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - }); - - it('does not alter the model', function() { - expect($rootScope.date).toBe(null); - }); - - it('shows the correct title', function() { - expect(getTitle()).toBe('November 1980'); - }); - }); - - describe('when modified before date selected.', function() { - beforeEach(function() { - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - - $rootScope.options.initDate = new Date('December 20, 1981'); - $rootScope.$digest(); - }); - - it('does not alter the model', function() { - expect($rootScope.date).toBe(null); - }); - - it('shows the correct title', function() { - expect(getTitle()).toBe('December 1981'); - }); - }); - - describe('when modified after date selected.', function() { - beforeEach(function() { - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - $rootScope.date = new Date('April 1, 1982'); - $rootScope.options.initDate = new Date('December 20, 1981'); - $rootScope.$digest(); - }); - - it('does not alter the model', function() { - expect($rootScope.date).toEqual(new Date('April 1, 1982')); - }); - - it('shows the correct title', function() { - expect(getTitle()).toBe('April 1982'); - }); - }); - }); - - describe('toggles programatically by `open` attribute', function() { - var wrapElement; - - beforeEach(inject(function() { - $rootScope.open = true; - wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('to display initially', function() { - expect(dropdownEl.length).toBe(1); - }); - - it('to close / open from scope variable', function() { - expect(dropdownEl.length).toBe(1); - $rootScope.open = false; - $rootScope.$digest(); - assignElements(wrapElement); - expect(dropdownEl.length).toBe(0); - - $rootScope.open = true; - $rootScope.$digest(); - assignElements(wrapElement); - expect(dropdownEl.length).toBe(1); - }); - }); - - describe('custom format', function() { - beforeEach(inject(function() { - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('to display the correct value in input', function() { - expect(inputEl.val()).toBe('30-September-2010'); - }); - - it('updates the input when a day is clicked', function() { - clickOption(17); - expect(inputEl.val()).toBe('15-September-2010'); - expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); - }); - - it('updates the input correctly when model changes', function() { - $rootScope.date = new Date('January 10, 1983 10:00:00'); - $rootScope.$digest(); - expect(inputEl.val()).toBe('10-January-1983'); - }); - }); - - describe('custom format with time', function() { - beforeEach(inject(function() { - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('updates the model correctly when the input value changes', function() { - $rootScope.date = new Date(2015, 10, 24, 10, 0); - $rootScope.$digest(); - expect(inputEl.val()).toBe('Nov-24-2015 10:00 AM'); - - inputEl.val('Nov-24-2015 11:00 AM').trigger('input'); - $rootScope.$digest(); - expect($rootScope.date).toEqual(new Date(2015, 10, 24, 11, 0)); - }); - }); - - describe('custom format with optional leading zeroes', function() { - beforeEach(inject(function() { - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('to display the correct value in input', function() { - expect(inputEl.val()).toBe('30-09-2010'); - }); - - it('updates the input when a day is clicked', function() { - clickOption(10); - expect(inputEl.val()).toBe('08-09-2010'); - expect($rootScope.date).toEqual(new Date('September 8, 2010 15:30:00')); - }); - - it('updates the input correctly when model changes', function() { - $rootScope.date = new Date('December 25, 1983 10:00:00'); - $rootScope.$digest(); - expect(inputEl.val()).toBe('25-12-1983'); - }); - }); - - describe('dynamic custom format', function() { - beforeEach(inject(function() { - $rootScope.format = 'dd-MMMM-yyyy'; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('to display the correct value in input', function() { - expect(inputEl.val()).toBe('30-September-2010'); - }); - - it('updates the input when a day is clicked', function() { - clickOption(17); - expect(inputEl.val()).toBe('15-September-2010'); - expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); - }); - - it('updates the input correctly when model changes', function() { - $rootScope.date = new Date('August 11, 2013 09:09:00'); - $rootScope.$digest(); - expect(inputEl.val()).toBe('11-August-2013'); - }); - - it('updates the input correctly when format changes', function() { - $rootScope.format = 'dd/MM/yyyy'; - $rootScope.$digest(); - expect(inputEl.val()).toBe('30/09/2010'); - }); - }); - - describe('format errors', function() { - var originalConfig = {}; - beforeEach(inject(function(uibDatepickerPopupConfig) { - angular.extend(originalConfig, uibDatepickerPopupConfig); - uibDatepickerPopupConfig.datepickerPopup = null; - })); - afterEach(inject(function(uibDatepickerPopupConfig) { - // return it to the original state - angular.extend(uibDatepickerPopupConfig, originalConfig); - })); - - it('should throw an error if there is no format', function() { - expect(function() { - $compile('
')($rootScope); - }).toThrow(new Error('uibDatepickerPopup must have a date format specified.')); - }); - - it('should throw an error if the format changes to null without fallback', function() { - $rootScope.format = 'dd-MMMM-yyyy'; - $compile('
')($rootScope); - $rootScope.$digest(); - - expect(function() { - $rootScope.format = null; - $rootScope.$digest(); - }).toThrow(new Error('uibDatepickerPopup must have a date format specified.')); - }); - - it('should thrown an error on date inputs with custom formats', function() { - expect(function() { - $compile('
')($rootScope); - }).toThrow(new Error('HTML5 date input types do not support custom formats.')); - }); - }); - - describe('european format', function() { - it('dd.MM.yyyy', function() { - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - - changeInputValueTo(inputEl, '11.08.2013'); - expect($rootScope.date.getFullYear()).toEqual(2013); - expect($rootScope.date.getMonth()).toEqual(7); - expect($rootScope.date.getDate()).toEqual(11); - }); - }); - - describe('`close-on-date-selection` attribute', function() { - var wrapElement; - beforeEach(inject(function() { - $rootScope.close = false; - wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('does not close the dropdown when a day is clicked', function() { - clickOption(17); - assignElements(wrapElement); - expect(dropdownEl.length).toBe(1); - }); - }); - - describe('button bar', function() { - var buttons, buttonBarElement; - - function assignButtonBar() { - buttonBarElement = dropdownEl.find('li').eq(-1); - buttons = buttonBarElement.find('button'); - } - - describe('', function() { - var wrapElement; - - beforeEach(inject(function() { - $rootScope.isopen = true; - wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - assignButtonBar(); - })); - - it('should exist', function() { - expect(dropdownEl.length).toBe(1); - expect(dropdownEl.find('li').length).toBe(2); - }); - - it('should have three buttons', function() { - expect(buttons.length).toBe(3); - - expect(buttons.eq(0).text()).toBe('Today'); - expect(buttons.eq(1).text()).toBe('Clear'); - expect(buttons.eq(2).text()).toBe('Done'); - }); - - it('should have a button to set today date without altering time part', function() { - var today = new Date(); - buttons.eq(0).click(); - expect($rootScope.date.getFullYear()).toBe(today.getFullYear()); - expect($rootScope.date.getMonth()).toBe(today.getMonth()); - expect($rootScope.date.getDate()).toBe(today.getDate()); - - expect($rootScope.date.getHours()).toBe(15); - expect($rootScope.date.getMinutes()).toBe(30); - expect($rootScope.date.getSeconds()).toBe(0); - }); - - it('should have a button to set today date if blank', function() { - $rootScope.date = null; - $rootScope.$digest(); - - var today = new Date(); - buttons.eq(0).click(); - expect($rootScope.date.getFullYear()).toBe(today.getFullYear()); - expect($rootScope.date.getMonth()).toBe(today.getMonth()); - expect($rootScope.date.getDate()).toBe(today.getDate()); - - expect($rootScope.date.getHours()).toBe(0); - expect($rootScope.date.getMinutes()).toBe(0); - expect($rootScope.date.getSeconds()).toBe(0); - }); - - it('should have a button to clear value', function() { - buttons.eq(1).click(); - expect($rootScope.date).toBe(null); - }); - - it('should have a button to close calendar', function() { - buttons.eq(2).click(); - assignElements(wrapElement); - expect(dropdownEl.length).toBe(0); - }); - }); - - describe('customization', function() { - it('should change text from attributes', function() { - $rootScope.clearText = 'Null it!'; - $rootScope.close = 'Close'; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - assignButtonBar(); - - expect(buttons.eq(0).text()).toBe('Now'); - expect(buttons.eq(1).text()).toBe('Null it!'); - expect(buttons.eq(2).text()).toBe('CloseME'); - }); - - it('should disable today button if before min date', function() { - var date = new Date(); - date.setDate(new Date().getDate() + 1); - $rootScope.options = { - minDate: date - }; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - assignButtonBar(); - - expect(buttons.eq(0).prop('disabled')).toBe(true); - }); - - it('should disable today button if before min date, yyyy-MM-dd case', inject(function(dateFilter) { - var date = new Date(); - date.setDate(new Date().getDate() + 1); - var literalMinDate = dateFilter(date, 'yyyy-MM-dd'); - $rootScope.options = { - minDate: literalMinDate - }; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - assignButtonBar(); - - expect(buttons.eq(0).prop('disabled')).toBe(true); - })); - - it('should not disable any button if min date is null', function() { - $rootScope.options = { - minDate: null - }; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - assignButtonBar(); - - for (var i = 0; i < buttons.length; i++) { - expect(buttons.eq(i).prop('disabled')).toBe(false); - } - }); - - it('should disable today button if after max date', function() { - var date = new Date(); - date.setDate(new Date().getDate() - 2); - $rootScope.options = { - maxDate: date - }; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - assignButtonBar(); - - expect(buttons.eq(0).prop('disabled')).toBe(true); - }); - - it('should not disable any button if max date is null', function() { - $rootScope.options = { - maxDate: null - }; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - assignButtonBar(); - - for (var i = 0; i < buttons.length; i++) { - expect(buttons.eq(i).prop('disabled')).toBe(false); - } - }); - - it('should remove bar', function() { - $rootScope.showBar = false; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - expect(dropdownEl.find('li').length).toBe(1); - }); - - it('should hide weeks column on popup', function() { - $rootScope.options = { - showWeeks: false - }; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - - expect(getLabelsRow().find('th').length).toEqual(7); - var tr = element.find('tbody').find('tr'); - for (var i = 0; i < 5; i++) { - expect(tr.eq(i).find('td').length).toEqual(7); - } - }); - - it('should show weeks column on popup', function() { - $rootScope.options = { - showWeeks: true - }; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - - expect(getLabelsRow().find('th').eq(0)).not.toBeHidden(); - var tr = element.find('tbody').find('tr'); - for (var i = 0; i < 5; i++) { - expect(tr.eq(i).find('td').eq(0)).not.toBeHidden(); - } - }); - }); - - describe('`ng-change`', function() { - beforeEach(inject(function() { - $rootScope.changeHandler = jasmine.createSpy('changeHandler'); - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - assignButtonBar(); - })); - - it('should be called when `today` is clicked', function() { - buttons.eq(0).click(); - expect($rootScope.changeHandler).toHaveBeenCalled(); - }); - - it('should be called when `clear` is clicked', function() { - buttons.eq(1).click(); - expect($rootScope.changeHandler).toHaveBeenCalled(); - }); - - it('should not be called when `close` is clicked', function() { - buttons.eq(2).click(); - expect($rootScope.changeHandler).not.toHaveBeenCalled(); - }); - }); - }); - - describe('use with `ng-required` directive', function() { - describe('`ng-required is true`', function() { - beforeEach(inject(function() { - $rootScope.date = ''; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('should be invalid initially and when no date', function() { - expect(inputEl.hasClass('ng-invalid')).toBeTruthy(); - }); - - it('should be valid if model has been specified', function() { - $rootScope.date = new Date(); - $rootScope.$digest(); - expect(inputEl.hasClass('ng-valid')).toBeTruthy(); - }); - - it('should be valid if model value is a valid timestamp', function() { - $rootScope.date = Date.now(); - $rootScope.$digest(); - expect(inputEl.hasClass('ng-valid')).toBeTruthy(); - }); - }); - - describe('`ng-required is false`', function() { - beforeEach(inject(function() { - $rootScope.date = ''; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('should be valid initially and when no date', function() { - expect(inputEl.hasClass('ng-valid')).toBeTruthy(); - }); - }); - }); - - describe('use with `ng-change` directive', function() { - beforeEach(inject(function() { - $rootScope.changeHandler = jasmine.createSpy('changeHandler'); - $rootScope.date = new Date('09/16/2010'); - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('should not be called initially', function() { - expect($rootScope.changeHandler).not.toHaveBeenCalled(); - }); - - it('should be called when a day is clicked', function() { - clickOption(17); - expect($rootScope.changeHandler).toHaveBeenCalled(); - }); - - it('should not be called when model changes programatically', function() { - $rootScope.date = new Date(); - $rootScope.$digest(); - expect($rootScope.changeHandler).not.toHaveBeenCalled(); - }); - }); - - describe('with disabled', function() { - var wrapElement; - - beforeEach(function() { - $rootScope.isOpen = false; - wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - }); - - it('should not open the popup', function() { - $rootScope.isOpen = true; - $rootScope.$digest(); - - expect($rootScope.isOpen).toBe(false); - expect(wrapElement.find('ul').length).toBe(0); - }); - }); - - describe('with ng-disabled', function() { - var wrapElement; - - beforeEach(function() { - $rootScope.disabled = false; - $rootScope.isOpen = false; - wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - }); - - it('should not open the popup when disabled', function() { - $rootScope.isOpen = true; - $rootScope.$digest(); - - expect($rootScope.isOpen).toBe(true); - expect(wrapElement.find('ul').length).toBe(1); - - $rootScope.isOpen = false; - $rootScope.$digest(); - - expect($rootScope.isOpen).toBe(false); - expect(wrapElement.find('ul').length).toBe(0); - - $rootScope.disabled = true; - $rootScope.isOpen = true; - $rootScope.$digest(); - - expect($rootScope.isOpen).toBe(false); - expect(wrapElement.find('ul').length).toBe(0); - - $rootScope.disabled = false; - $rootScope.isOpen = true; - $rootScope.$digest(); - - expect($rootScope.isOpen).toBe(true); - expect(wrapElement.find('ul').length).toBe(1); - }); - }); - - describe('with datepicker-popup-template-url', function() { - beforeEach(function() { - $rootScope.date = new Date(); - }); - - afterEach(function () { - $document.find('body').find('.dropdown-menu').remove(); - }); - - it('should allow custom templates for the popup', function() { - $templateCache.put('foo/bar.html', '
baz
'); - - var elm = angular.element('
'); - - $compile(elm)($rootScope); - $rootScope.$digest(); - - expect(elm.children().eq(1).html()).toBe('baz'); - }); - }); - - describe('with datepicker-template-url', function() { - beforeEach(function() { - $rootScope.date = new Date(); - }); - - afterEach(function() { - $document.find('body').find('.dropdown-menu').remove(); - }); - - it('should allow custom templates for the datepicker', function() { - $templateCache.put('foo/bar.html', '
baz
'); - - var elm = angular.element('
'); - - $compile(elm)($rootScope); - $rootScope.$digest(); - - var datepicker = elm.find('[uib-datepicker]'); - - expect(datepicker.html()).toBe('baz'); - }); - }); - - describe('with an append-to-body attribute', function() { - beforeEach(function() { - $rootScope.date = new Date(); - }); - - afterEach(function() { - $document.find('body').children().remove(); - }); - - it('should append to the body', function() { - var $body = $document.find('body'), - bodyLength = $body.children().length, - elm = angular.element( - '
' - ); - $compile(elm)($rootScope); - $rootScope.$digest(); - - expect($body.children().length).toEqual(bodyLength + 1); - expect(elm.children().length).toEqual(1); - }); - - it('should be removed on scope destroy', function() { - var $body = $document.find('body'), - bodyLength = $body.children().length, - isolatedScope = $rootScope.$new(), - elm = angular.element( - '' - ); - $compile(elm)(isolatedScope); - isolatedScope.$digest(); - expect($body.children().length).toEqual(bodyLength + 1); - isolatedScope.$destroy(); - expect($body.children().length).toEqual(bodyLength); - }); - }); - - describe('with setting datepickerConfig.showWeeks to false', function() { - var originalConfig = {}; - beforeEach(inject(function(uibDatepickerConfig) { - angular.extend(originalConfig, uibDatepickerConfig); - uibDatepickerConfig.showWeeks = false; - - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - afterEach(inject(function(uibDatepickerConfig) { - // return it to the original state - angular.extend(uibDatepickerConfig, originalConfig); - })); - - it('changes initial visibility for weeks', function() { - expect(getLabelsRow().find('th').length).toEqual(7); - var tr = element.find('tbody').find('tr'); - for (var i = 0; i < 5; i++) { - expect(tr.eq(i).find('td').length).toEqual(7); - } - }); - }); - - describe('`datepicker-mode`', function() { - beforeEach(inject(function() { - $rootScope.date = new Date('August 11, 2013'); - $rootScope.options = { - datepickerMode: 'month' - }; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - it('shows the correct title', function() { - expect(getTitle()).toBe('2013'); - }); - - it('updates binding', function() { - clickTitleButton(); - expect($rootScope.options.datepickerMode).toBe('year'); - }); - }); - - describe('attribute `onOpenFocus`', function() { - beforeEach(function() { - $rootScope.date = null; - $rootScope.isopen = false; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - }); - - it('should remain focused on the input', function() { - var focused = true; - expect(dropdownEl.length).toBe(0); - - inputEl[0].focus(); - inputEl.on('blur', function() { - focused = false; - }); - $rootScope.isopen = true; - $rootScope.$digest(); - - expect(inputEl.parent().find('.dropdown-menu').length).toBe(1); - expect(focused).toBe(true); - }); - }); - - describe('altInputFormats', function() { - describe('datepickerPopupConfig.altInputFormats', function() { - var originalConfig = {}; - beforeEach(inject(function(uibDatepickerPopupConfig) { - $rootScope.date = new Date('November 9, 1980'); - angular.extend(originalConfig, uibDatepickerPopupConfig); - uibDatepickerPopupConfig.datepickerPopup = 'MM-dd-yyyy'; - uibDatepickerPopupConfig.altInputFormats = ['M!/d!/yyyy']; - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - })); - - afterEach(inject(function(uibDatepickerPopupConfig) { - // return it to the original state - angular.extend(uibDatepickerPopupConfig, originalConfig); - })); - - it('changes date format', function() { - changeInputValueTo(inputEl, '11/8/1980'); - - expect($rootScope.date.getFullYear()).toEqual(1980); - expect($rootScope.date.getMonth()).toEqual(10); - expect($rootScope.date.getDate()).toEqual(8); - }); - - it('changes the datepicker', function() { - expect(selectedElementIndex()).toEqual(14); - changeInputValueTo(inputEl, '11/8/1980'); - expect(selectedElementIndex()).toEqual(13); - }); - }); - - describe('attribute `alt-input-formats`', function() { - beforeEach(function() { - $rootScope.date = new Date('November 9, 1980'); - var wrapElement = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - }); - - it('should accept alternate input formats', function() { - changeInputValueTo(inputEl, '11/8/1980'); - - expect($rootScope.date.getFullYear()).toEqual(1980); - expect($rootScope.date.getMonth()).toEqual(10); - expect($rootScope.date.getDate()).toEqual(8); - }); - - it('changes the datepicker', function() { - expect(selectedElementIndex()).toEqual(14); - changeInputValueTo(inputEl, '11/8/1980'); - expect(selectedElementIndex()).toEqual(13); - }); - }); - }); - }); - - describe('uibDatepickerConfig ngModelOptions', function() { - var inputEl, dropdownEl; - - function assignElements(wrapElement) { - inputEl = wrapElement.find('input'); - dropdownEl = wrapElement.find('ul'); - element = dropdownEl.find('table'); - } - - beforeEach(inject(function(uibDatepickerConfig) { - uibDatepickerConfig.ngModelOptions = { timezone: '+600' }; - $rootScope.date = new Date('2010-09-30T10:00:00.000Z'); - $rootScope.isopen = true; - })); - - afterEach(inject(function(uibDatepickerConfig) { - uibDatepickerConfig.ngModelOptions = {}; - })); - - describe('timezone', function() { - beforeEach(inject(function(uibDatepickerConfig) { - var wrapper = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapper); - })); - - it('interprets the date appropriately', function() { - expect(inputEl.val()).toBe('09/30/2010'); - }); - - it('updates the input when a day is clicked', function() { - clickOption(17); - expect(inputEl.val()).toBe('09/15/2010'); - expect($rootScope.date).toEqual(new Date('2010-09-15T10:00:00.000Z')); - }); - - it('shows the correct title', function() { - expect(getTitle()).toBe('September 2010'); - }); - }); - - it('timezone interprets init date appropriately', function() { - $rootScope.options = { - initDate: new Date('2010-09-30T23:00:00.000Z') - }; - $rootScope.date = null; - var wrapper = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapper); - - expect(getTitle()).toBe('October 2010'); - }); - - it('timezone interprets min date appropriately', function() { - $rootScope.options = { - minDate: new Date('2010-10-01T00:00:00.000Z') - }; - var wrapper = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapper); - - expect(getSelectedElement().prop('disabled')).toBe(true); - }); - }); - - describe('ng-model-options', function() { - describe('timezone', function() { - var inputEl, dropdownEl, $document, $sniffer, $timeout; - - function assignElements(wrapElement) { - inputEl = wrapElement.find('input'); - dropdownEl = wrapElement.find('ul'); - element = dropdownEl.find('table'); - } - - beforeEach(function() { - $rootScope.date = new Date('2010-09-30T10:00:00.000Z'); - $rootScope.ngModelOptions = { timezone: '+600' }; - $rootScope.isopen = true; - var wrapper = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapper); - }); - - it('interprets the date appropriately', function() { - expect(inputEl.val()).toBe('09/30/2010'); - }); - - it('has `selected` only the correct day', function() { - expectSelectedElement(32); - }); - - it('updates the input when a day is clicked', function() { - clickOption(17); - expect(inputEl.val()).toBe('09/15/2010'); - expect($rootScope.date).toEqual(new Date('2010-09-15T10:00:00.000Z')); - }); - }); - - describe('timezone HTML5 date input', function() { - var inputEl, dropdownEl, $document, $sniffer, $timeout; - - function assignElements(wrapElement) { - inputEl = wrapElement.find('input'); - dropdownEl = wrapElement.find('ul'); - element = dropdownEl.find('table'); - } - - beforeEach(function() { - $rootScope.date = new Date('2010-09-30T10:00:00.000Z'); - $rootScope.ngModelOptions = { timezone: '+600' }; - $rootScope.isopen = true; - var wrapper = $compile('
')($rootScope); - $rootScope.$digest(); - assignElements(wrapper); - }); - - it('interprets the date appropriately', function() { - expect(inputEl.val()).toBe('2010-09-30'); - }); - - it('has `selected` only the correct day', function() { - expectSelectedElement(32); - }); - - it('updates the input when a day is clicked', function() { - clickOption(17); - expect(inputEl.val()).toBe('2010-09-15'); - expect($rootScope.date).toEqual(new Date('2010-09-15T10:00:00.000Z')); - }); - }); - }); - describe('with empty initial state', function() { beforeEach(inject(function() { $rootScope.date = null; diff --git a/src/datepickerPopup/docs/demo.html b/src/datepickerPopup/docs/demo.html new file mode 100644 index 0000000000..499e388af1 --- /dev/null +++ b/src/datepickerPopup/docs/demo.html @@ -0,0 +1,47 @@ + +
+
Selected date is: {{dt | date:'fullDate' }}
+ +

Popup

+
+
+

+ + + + +

+
+ +
+

+ + + + +

+
+
+
+
+ +
+
+ +
+ + + + +
diff --git a/src/datepickerPopup/docs/demo.js b/src/datepickerPopup/docs/demo.js new file mode 100644 index 0000000000..8494d64caa --- /dev/null +++ b/src/datepickerPopup/docs/demo.js @@ -0,0 +1,95 @@ +angular.module('ui.bootstrap.demo').controller('DatepickerPopupDemoCtrl', function ($scope) { + $scope.today = function() { + $scope.dt = new Date(); + }; + $scope.today(); + + $scope.clear = function() { + $scope.dt = null; + }; + + $scope.inlineOptions = { + customClass: getDayClass, + minDate: new Date(), + showWeeks: true + }; + + $scope.dateOptions = { + dateDisabled: disabled, + formatYear: 'yy', + maxDate: new Date(2020, 5, 22), + minDate: new Date(), + startingDay: 1 + }; + + // Disable weekend selection + function disabled(data) { + var date = data.date, + mode = data.mode; + return mode === 'day' && (date.getDay() === 0 || date.getDay() === 6); + } + + $scope.toggleMin = function() { + $scope.inlineOptions.minDate = $scope.inlineOptions.minDate ? null : new Date(); + $scope.dateOptions.minDate = $scope.inlineOptions.minDate; + }; + + $scope.toggleMin(); + + $scope.open1 = function() { + $scope.popup1.opened = true; + }; + + $scope.open2 = function() { + $scope.popup2.opened = true; + }; + + $scope.setDate = function(year, month, day) { + $scope.dt = new Date(year, month, day); + }; + + $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate']; + $scope.format = $scope.formats[0]; + $scope.altInputFormats = ['M!/d!/yyyy']; + + $scope.popup1 = { + opened: false + }; + + $scope.popup2 = { + opened: false + }; + + var tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + var afterTomorrow = new Date(); + afterTomorrow.setDate(tomorrow.getDate() + 1); + $scope.events = [ + { + date: tomorrow, + status: 'full' + }, + { + date: afterTomorrow, + status: 'partially' + } + ]; + + function getDayClass(data) { + var date = data.date, + mode = data.mode; + if (mode === 'day') { + var dayToCheck = new Date(date).setHours(0,0,0,0); + + for (var i = 0; i < $scope.events.length; i++) { + var currentDay = new Date($scope.events[i].date).setHours(0,0,0,0); + + if (dayToCheck === currentDay) { + return $scope.events[i].status; + } + } + } + + return ''; + } +}); diff --git a/src/datepickerPopup/docs/readme.md b/src/datepickerPopup/docs/readme.md new file mode 100644 index 0000000000..6f32b36a2f --- /dev/null +++ b/src/datepickerPopup/docs/readme.md @@ -0,0 +1,102 @@ +The datepicker popup is meant to be used with an input element. To understand usage of the datepicker, please refer to its documentation [here](https://angular-ui.github.io/bootstrap/#/datepicker). + +### uib-datepicker-popup settings + +The popup is a wrapper that you can use in an input to toggle a datepicker. To configure the datepicker, use `datepicker-options` as documented in the [inline datepicker](https://angular-ui.github.io/bootstrap/#/datepicker). + +* `alt-input-formats` +$ +C +_(Default: `[]`)_ - +A list of alternate formats acceptable for manual entry. + +* `clear-text` +C +_(Default: `Clear`)_ - +The text to display for the clear button. + +* `close-on-date-selection` +$ +C +_(Default: `true`)_ - +Whether to close calendar when a date is chosen. + +* `close-text` +C +_(Default: `Done`)_ - +The text to display for the close button. + +* `current-text` +C +_(Default: `Today`)_ - +The text to display for the current day button. + +* `datepicker-append-to-body` +$ +C +_(Default: `false`, Config: `appendToBody`)_ - +Append the datepicker popup element to `body`, rather than inserting after `datepicker-popup`. + +* `datepicker-options` +$ - +An object with any combination of the datepicker settings (in camelCase) used to configure the wrapped datepicker. + +* `datepicker-popup-template-url` +C +_(Default: `uib/template/datepickerPopup/popup.html`)_ - +Add the ability to override the template used on the component. + +* `datepicker-template-url` +C +_(Default: `uib/template/datepicker/datepicker.html`)_ - +Add the ability to override the template used on the component (inner uib-datepicker). + +* `is-open` +$ + +_(Default: `false`)_ - +Whether or not to show the datepicker. + +* `on-open-focus` +$ +C +_(Default: `true`)_ - +Whether or not to focus the datepicker popup upon opening. + +* `show-button-bar` +$ +C +_(Default: `true`)_ - +Whether or not to display a button bar underneath the uib-datepicker. + +* `type` +C +_(Default: `text`, Config: `html5Types`)_ - +You can override the input type to be _(date|datetime-local|month)_. That will change the date format of the popup. + +* `popup-placement` + C + _(Default: `auto bottom-left`, Config: 'placement')_ - +Passing in 'auto' separated by a space before the placement will enable auto positioning, e.g: "auto bottom-left". The popup will attempt to position where it fits in the closest scrollable ancestor. Accepts: + + * `top` - popup on top, horizontally centered on input element. + * `top-left` - popup on top, left edge aligned with input element left edge. + * `top-right` - popup on top, right edge aligned with input element right edge. + * `bottom` - popup on bottom, horizontally centered on input element. + * `bottom-left` - popup on bottom, left edge aligned with input element left edge. + * `bottom-right` - popup on bottom, right edge aligned with input element right edge. + * `left` - popup on left, vertically centered on input element. + * `left-top` - popup on left, top edge aligned with input element top edge. + * `left-bottom` - popup on left, bottom edge aligned with input element bottom edge. + * `right` - popup on right, vertically centered on input element. + * `right-top` - popup on right, top edge aligned with input element top edge. + * `right-bottom` - popup on right, bottom edge aligned with input element bottom edge. + +* `uib-datepicker-popup` +C +_(Default: `yyyy-MM-dd`, Config: `datepickerConfig`)_ - +The format for displayed dates. This string can take string literals by surrounding the value with single quotes, i.e. `yyyy-MM-dd h 'o\'clock'`. + +**Notes** + +If using this directive on input type date, a native browser datepicker could also appear. diff --git a/src/datepickerPopup/index.js b/src/datepickerPopup/index.js new file mode 100644 index 0000000000..cdd16a23a9 --- /dev/null +++ b/src/datepickerPopup/index.js @@ -0,0 +1,11 @@ +require('../datepicker'); +require('../position'); +require('../../template/datepickerPopup/popup.html.js'); + +require('./popup.css'); + +var MODULE_NAME = 'ui.bootstrap.module.datepickerPopup'; + +angular.module(MODULE_NAME, ['ui.bootstrap.datepickerPopup', 'uib/template/datepickerPopup/popup.html']); + +module.exports = MODULE_NAME; diff --git a/src/datepickerPopup/popup.css b/src/datepickerPopup/popup.css new file mode 100644 index 0000000000..a0192b6567 --- /dev/null +++ b/src/datepickerPopup/popup.css @@ -0,0 +1,9 @@ +.uib-datepicker-popup.dropdown-menu { + display: block; + float: none; + margin: 0; +} + +.uib-button-bar { + padding: 10px 9px 2px; +} diff --git a/src/datepickerPopup/popup.js b/src/datepickerPopup/popup.js new file mode 100644 index 0000000000..59af66517f --- /dev/null +++ b/src/datepickerPopup/popup.js @@ -0,0 +1,453 @@ +angular.module('ui.bootstrap.datepickerPopup', ['ui.bootstrap.datepicker', 'ui.bootstrap.position']) + +.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', +function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig) { + var cache = {}, + isHtml5DateInput = false; + var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus, + datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, scrollParentEl, + ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = [], + timezone; + + this.init = function(_ngModel_) { + ngModel = _ngModel_; + ngModelOptions = _ngModel_.$options; + 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('
'); + if (ngModelOptions) { + timezone = ngModelOptions.timezone; + $scope.ngModelOptions = angular.copy(ngModelOptions); + $scope.ngModelOptions.timezone = null; + if ($scope.ngModelOptions.updateOnDefault === true) { + $scope.ngModelOptions.updateOn = $scope.ngModelOptions.updateOn ? + $scope.ngModelOptions.updateOn + ' default' : 'default'; + } + + popupEl.attr('ng-model-options', 'ngModelOptions'); + } else { + timezone = null; + } + + 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; + } + + $scope.date = dateParser.fromTimezone(value, timezone); + + if (angular.isNumber($scope.date)) { + $scope.date = new Date($scope.date); + } + + return dateParser.filter($scope.date, dateFormat); + }); + } else { + ngModel.$formatters.push(function(value) { + $scope.date = dateParser.fromTimezone(value, 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(), timezone); + } + + var dates = {}; + angular.forEach(['minDate', 'maxDate'], function(key) { + if ($scope.datepickerOptions[key] === null) { + dates[key] = null; + } else if (angular.isDate($scope.datepickerOptions[key])) { + dates[key] = dateParser.fromTimezone(new Date($scope.datepickerOptions[key]), timezone); + } else { + 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) { + if (angular.isDefined(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 = new Date(today.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, timezone); + } + } + + return ngModel.$options && ngModel.$options.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(viewValue)); + } + + 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'); + } + } + } + + $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 { + replace: true, + transclude: true, + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepickerPopup/popup.html'; + } + }; +}); diff --git a/src/datepickerPopup/test/popup.spec.js b/src/datepickerPopup/test/popup.spec.js new file mode 100644 index 0000000000..0fb5729507 --- /dev/null +++ b/src/datepickerPopup/test/popup.spec.js @@ -0,0 +1,1656 @@ +describe('datepicker popup', function() { + var inputEl, dropdownEl, $compile, $document, $rootScope, $sniffer, + $templateCache, $timeout; + beforeEach(module('ui.bootstrap.datepickerPopup')); + beforeEach(module('uib/template/datepicker/datepicker.html')); + beforeEach(module('uib/template/datepicker/day.html')); + beforeEach(module('uib/template/datepicker/month.html')); + beforeEach(module('uib/template/datepicker/year.html')); + beforeEach(module('uib/template/datepickerPopup/popup.html')); + beforeEach(inject(function(_$compile_, _$rootScope_, _$templateCache_) { + $compile = _$compile_; + $rootScope = _$rootScope_; + $rootScope.date = new Date('September 30, 2010 15:30:00'); + $templateCache = _$templateCache_; + })); + + function getTitleButton() { + return element.find('th').eq(1).find('button').first(); + } + + function getTitle() { + return getTitleButton().text(); + } + + function clickTitleButton() { + getTitleButton().click(); + } + + function getLabelsRow() { + return element.find('thead').find('tr').eq(1); + } + + function getLabels(dayMode) { + var els = getLabelsRow().find('th'), + labels = []; + for (var i = dayMode ? 1 : 0, n = els.length; i < n; i++) { + labels.push(els.eq(i).text()); + } + return labels; + } + + function getOptions(dayMode) { + var tr = element.find('tbody').find('tr'); + var rows = []; + + for (var j = 0, numRows = tr.length; j < numRows; j++) { + var cols = tr.eq(j).find('td'), days = []; + for (var i = dayMode ? 1 : 0, n = cols.length; i < n; i++) { + days.push(cols.eq(i).find('button').text()); + } + rows.push(days); + } + return rows; + } + + function clickOption(index) { + getAllOptionsEl().eq(index).click(); + } + + function getAllOptionsEl(dayMode) { + return element.find('tbody').find('button'); + } + + function selectedElementIndex() { + var buttons = getAllOptionsEl(); + for (var i = 0; i < buttons.length; i++) { + if (angular.element(buttons[i]).hasClass('btn-info')) { + return i; + } + } + } + + function expectSelectedElement(index) { + var buttons = getAllOptionsEl(); + angular.forEach( buttons, function(button, idx) { + expect(angular.element(button).hasClass('btn-info')).toBe(idx === index); + }); + } + + function getSelectedElement(index) { + var buttons = getAllOptionsEl(); + var el = $.grep(buttons, function(button, idx) { + return angular.element(button).hasClass('btn-info'); + })[0]; + return angular.element(el); + } + + function triggerKeyDown(element, key, ctrl) { + var keyCodes = { + 'enter': 13, + 'space': 32, + 'pageup': 33, + 'pagedown': 34, + 'end': 35, + 'home': 36, + 'left': 37, + 'up': 38, + 'right': 39, + 'down': 40, + 'esc': 27 + }; + var e = $.Event('keydown'); + e.which = keyCodes[key]; + if (ctrl) { + e.ctrlKey = true; + } + element.trigger(e); + } + + function assignElements(wrapElement) { + inputEl = wrapElement.find('input'); + dropdownEl = wrapElement.find('ul'); + element = dropdownEl.find('table'); + } + + function changeInputValueTo(el, value) { + el.val(value); + el.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); + $rootScope.$digest(); + } + + describe('basic', function() { + var wrapElement, inputEl, dropdownEl; + + function assignElements(wrapElement) { + inputEl = wrapElement.find('input'); + dropdownEl = wrapElement.find('ul'); + element = dropdownEl.find('table'); + } + + beforeEach(function() { + $rootScope.date = new Date('September 30, 2010 15:30:00'); + $rootScope.isopen = true; + wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + }); + + it('should stop click event from bubbling from today button', function() { + var bubbled = false; + wrapElement.on('click', function() { + bubbled = true; + }); + + wrapElement.find('.uib-datepicker-current').trigger('click'); + + expect(bubbled).toBe(false); + }); + + it('should stop click event from bubbling from clear button', function() { + var bubbled = false; + wrapElement.on('click', function() { + bubbled = true; + }); + + wrapElement.find('.uib-clear').trigger('click'); + + expect(bubbled).toBe(false); + }); + + it('should stop click event from bubbling from close button', function() { + var bubbled = false; + wrapElement.on('click', function() { + bubbled = true; + }); + + wrapElement.find('.uib-close').trigger('click'); + + expect(bubbled).toBe(false); + }); + }); + + describe('ngModelOptions allowInvalid', function() { + beforeEach(inject(function(_$sniffer_) { + $sniffer = _$sniffer_; + + $rootScope.date = new Date('September 30, 2010 15:30:00'); + $rootScope.modelOptions = {allowInvalid: true}; + element = $compile('
')($rootScope); + inputEl = element.find('input'); + $rootScope.$digest(); + })); + + function changeInputValueTo(el, value) { + el.val(value); + el.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); + $rootScope.$digest(); + } + + it('should update ng-model even if the date is invalid when allowInvalid is true', function() { + changeInputValueTo(inputEl, 'pizza'); + expect($rootScope.date).toBe('pizza'); + expect(inputEl.val()).toBe('pizza'); + }); + }); + + describe('setting datepickerPopupConfig', function() { + var originalConfig = {}; + beforeEach(inject(function(uibDatepickerPopupConfig) { + angular.extend(originalConfig, uibDatepickerPopupConfig); + uibDatepickerPopupConfig.datepickerPopup = 'MM-dd-yyyy'; + + element = $compile('')($rootScope); + $rootScope.$digest(); + })); + afterEach(inject(function(uibDatepickerPopupConfig) { + // return it to the original state + angular.extend(uibDatepickerPopupConfig, originalConfig); + })); + + it('changes date format', function() { + expect(element.val()).toEqual('09-30-2010'); + }); + }); + + describe('setting datepickerPopupConfig inside ng-if', function() { + var originalConfig = {}; + beforeEach(inject(function(uibDatepickerPopupConfig) { + angular.extend(originalConfig, uibDatepickerPopupConfig); + uibDatepickerPopupConfig.datepickerPopup = 'MM-dd-yyyy'; + + element = $compile('
')($rootScope); + $rootScope.$digest(); + })); + afterEach(inject(function(uibDatepickerPopupConfig) { + // return it to the original state + angular.extend(uibDatepickerPopupConfig, originalConfig); + })); + + it('changes date format', function() { + expect(element.find('input').val()).toEqual('09-30-2010'); + }); + }); + + describe('initially', function() { + beforeEach(inject(function(_$document_, _$sniffer_) { + $document = _$document_; + $sniffer = _$sniffer_; + $rootScope.isopen = true; + $rootScope.date = new Date('September 30, 2010 15:30:00'); + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('does not to display datepicker initially', function() { + expect(dropdownEl.length).toBe(0); + }); + + it('to display the correct value in input', function() { + expect(inputEl.val()).toBe('2010-09-30'); + }); + }); + + describe('initially opened', function() { + var wrapElement; + + beforeEach(inject(function(_$document_, _$sniffer_, _$timeout_) { + $document = _$document_; + $sniffer = _$sniffer_; + $timeout = _$timeout_; + $rootScope.isopen = true; + $rootScope.date = new Date('September 30, 2010 15:30:00'); + wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('datepicker is displayed', function() { + expect(dropdownEl.length).toBe(1); + }); + + it('renders the calendar correctly', function() { + expect(getLabelsRow().css('display')).not.toBe('none'); + expect(getLabels(true)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); + expect(getOptions(true)).toEqual([ + ['29', '30', '31', '01', '02', '03', '04'], + ['05', '06', '07', '08', '09', '10', '11'], + ['12', '13', '14', '15', '16', '17', '18'], + ['19', '20', '21', '22', '23', '24', '25'], + ['26', '27', '28', '29', '30', '01', '02'], + ['03', '04', '05', '06', '07', '08', '09'] + ]); + }); + + it('updates the input when a day is clicked', function() { + clickOption(17); + expect(inputEl.val()).toBe('2010-09-15'); + expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); + }); + + it('should mark the input field dirty when a day is clicked', function() { + expect(inputEl).toHaveClass('ng-pristine'); + clickOption(17); + expect(inputEl).toHaveClass('ng-dirty'); + }); + + it('updates the input correctly when model changes', function() { + $rootScope.date = new Date('January 10, 1983 10:00:00'); + $rootScope.$digest(); + expect(inputEl.val()).toBe('1983-01-10'); + }); + + it('closes the dropdown when a day is clicked', function() { + expect(dropdownEl.length).toBe(1); + + clickOption(17); + assignElements(wrapElement); + expect(dropdownEl.length).toBe(0); + }); + + it('updates the model & calendar when input value changes', function() { + changeInputValueTo(inputEl, '2010-09-15'); + + expect($rootScope.date.getFullYear()).toEqual(2010); + expect($rootScope.date.getMonth()).toEqual(8); + expect($rootScope.date.getDate()).toEqual(15); + + expect(getOptions(true)).toEqual([ + ['29', '30', '31', '01', '02', '03', '04'], + ['05', '06', '07', '08', '09', '10', '11'], + ['12', '13', '14', '15', '16', '17', '18'], + ['19', '20', '21', '22', '23', '24', '25'], + ['26', '27', '28', '29', '30', '01', '02'], + ['03', '04', '05', '06', '07', '08', '09'] + ]); + expectSelectedElement(17); + }); + + it('closes when click outside of calendar', function() { + expect(dropdownEl.length).toBe(1); + + $timeout.flush(0); + $document.find('body').click(); + assignElements(wrapElement); + expect(dropdownEl.length).toBe(0); + }); + + it('sets `ng-invalid` for invalid input', function() { + changeInputValueTo(inputEl, 'pizza'); + + expect(inputEl).toHaveClass('ng-invalid'); + expect(inputEl).toHaveClass('ng-invalid-date'); + expect($rootScope.date).toBeUndefined(); + expect(inputEl.val()).toBe('pizza'); + }); + + it('unsets `ng-invalid` for valid input', function() { + changeInputValueTo(inputEl, 'pizza'); + expect(inputEl).toHaveClass('ng-invalid-date'); + + $rootScope.date = new Date('August 11, 2013'); + $rootScope.$digest(); + expect(inputEl).not.toHaveClass('ng-invalid'); + expect(inputEl).not.toHaveClass('ng-invalid-date'); + }); + + describe('focus', function () { + beforeEach(function() { + var body = $document.find('body'); + body.append(inputEl); + body.append(dropdownEl); + }); + + afterEach(function() { + inputEl.remove(); + dropdownEl.remove(); + }); + + it('returns to the input when ESC key is pressed in the popup and closes', function() { + expect(dropdownEl.length).toBe(1); + + dropdownEl.find('button').eq(0).focus(); + expect(document.activeElement.tagName).toBe('BUTTON'); + + triggerKeyDown(dropdownEl, 'esc'); + assignElements(wrapElement); + expect(dropdownEl.length).toBe(0); + expect(document.activeElement.tagName).toBe('INPUT'); + }); + + it('returns to the input when ESC key is pressed in the input and closes', function() { + expect(dropdownEl.length).toBe(1); + + dropdownEl.find('button').eq(0).focus(); + expect(document.activeElement.tagName).toBe('BUTTON'); + + triggerKeyDown(inputEl, 'esc'); + $rootScope.$digest(); + assignElements(wrapElement); + expect(dropdownEl.length).toBe(0); + expect(document.activeElement.tagName).toBe('INPUT'); + }); + + it('stops the ESC key from propagating if the dropdown is open, but not when closed', function() { + var documentKey = -1; + var getKey = function(evt) { documentKey = evt.which; }; + $document.bind('keydown', getKey); + + triggerKeyDown(inputEl, 'esc'); + expect(documentKey).toBe(-1); + + triggerKeyDown(inputEl, 'esc'); + expect(documentKey).toBe(27); + + $document.unbind('keydown', getKey); + }); + }); + + describe('works with HTML5 date input types', function() { + var date2 = new Date('October 1, 2010 12:34:56.789'); + beforeEach(inject(function(_$document_) { + $document = _$document_; + $rootScope.isopen = true; + $rootScope.date = new Date('September 30, 2010 15:30:00'); + })); + + it('works as date', function() { + setupInputWithType('date'); + expect(dropdownEl.length).toBe(1); + expect(inputEl.val()).toBe('2010-09-30'); + + changeInputValueTo(inputEl, '1980-03-05'); + + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(2); + expect($rootScope.date.getDate()).toEqual(5); + + expect(getOptions(true)).toEqual([ + ['24', '25', '26', '27', '28', '29', '01'], + ['02', '03', '04', '05', '06', '07', '08'], + ['09', '10', '11', '12', '13', '14', '15'], + ['16', '17', '18', '19', '20', '21', '22'], + ['23', '24', '25', '26', '27', '28', '29'], + ['30', '31', '01', '02', '03', '04', '05'] + ]); + expect(selectedElementIndex()).toEqual(10); + }); + + it('works as datetime-local', function() { + setupInputWithType('datetime-local'); + expect(inputEl.val()).toBe('2010-09-30T15:30:00.000'); + + changeInputValueTo(inputEl, '1980-03-05T12:34:56.000'); + + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(2); + expect($rootScope.date.getDate()).toEqual(5); + + expect(getOptions(true)).toEqual([ + ['24', '25', '26', '27', '28', '29', '01'], + ['02', '03', '04', '05', '06', '07', '08'], + ['09', '10', '11', '12', '13', '14', '15'], + ['16', '17', '18', '19', '20', '21', '22'], + ['23', '24', '25', '26', '27', '28', '29'], + ['30', '31', '01', '02', '03', '04', '05'] + ]); + expect(selectedElementIndex()).toEqual(10); + }); + + it('works as month', function() { + setupInputWithType('month'); + expect(inputEl.val()).toBe('2010-09'); + + changeInputValueTo(inputEl, '1980-03'); + + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(2); + expect($rootScope.date.getDate()).toEqual(30); + + expect(getOptions()).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); + expect(selectedElementIndex()).toEqual(2); + }); + + function setupInputWithType(type) { + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + } + }); + }); + + describe('works with ngModelOptions', function() { + var $timeout; + + beforeEach(inject(function(_$document_, _$sniffer_, _$timeout_) { + $document = _$document_; + $timeout = _$timeout_; + $rootScope.isopen = true; + $rootScope.date = new Date('September 30, 2010 15:30:00'); + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('should change model and update calendar after debounce timeout', function() { + changeInputValueTo(inputEl, '1980-03-05'); + + expect($rootScope.date.getFullYear()).toEqual(2010); + expect($rootScope.date.getMonth()).toEqual(9 - 1); + expect($rootScope.date.getDate()).toEqual(30); + + expect(getOptions(true)).toEqual([ + ['29', '30', '31', '01', '02', '03', '04'], + ['05', '06', '07', '08', '09', '10', '11'], + ['12', '13', '14', '15', '16', '17', '18'], + ['19', '20', '21', '22', '23', '24', '25'], + ['26', '27', '28', '29', '30', '01', '02'], + ['03', '04', '05', '06', '07', '08', '09'] + ]); + + // No changes yet + $timeout.flush(2000); + expect($rootScope.date.getFullYear()).toEqual(2010); + expect($rootScope.date.getMonth()).toEqual(9 - 1); + expect($rootScope.date.getDate()).toEqual(30); + + expect(getOptions(true)).toEqual([ + ['29', '30', '31', '01', '02', '03', '04'], + ['05', '06', '07', '08', '09', '10', '11'], + ['12', '13', '14', '15', '16', '17', '18'], + ['19', '20', '21', '22', '23', '24', '25'], + ['26', '27', '28', '29', '30', '01', '02'], + ['03', '04', '05', '06', '07', '08', '09'] + ]); + + $timeout.flush(10000); + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(2); + expect($rootScope.date.getDate()).toEqual(5); + + expect(getOptions(true)).toEqual([ + ['24', '25', '26', '27', '28', '29', '01'], + ['02', '03', '04', '05', '06', '07', '08'], + ['09', '10', '11', '12', '13', '14', '15'], + ['16', '17', '18', '19', '20', '21', '22'], + ['23', '24', '25', '26', '27', '28', '29'], + ['30', '31', '01', '02', '03', '04', '05'] + ]); + expectSelectedElement( 10 ); + }); + }); + + describe('works with ngModelOptions updateOn : "default"', function() { + var $timeout, wrapElement; + + beforeEach(inject(function(_$document_, _$sniffer_, _$timeout_) { + $document = _$document_; + $timeout = _$timeout_; + $rootScope.isopen = true; + $rootScope.date = new Date('2010-09-30T10:00:00.000Z'); + wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('should close the popup and update the input when a day is clicked', function() { + clickOption(17); + assignElements(wrapElement); + expect(dropdownEl.length).toBe(0); + expect(inputEl.val()).toBe('2010-09-15'); + expect($rootScope.date).toEqual(new Date('2010-09-15T10:00:00.000Z')); + }); + }); + + describe('attribute `datepickerOptions`', function() { + describe('show-weeks', function() { + beforeEach(function() { + $rootScope.opts = { + showWeeks: false + }; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + }); + + it('hides week numbers based on variable', function() { + expect(getLabelsRow().find('th').length).toEqual(7); + var tr = element.find('tbody').find('tr'); + for (var i = 0; i < 5; i++) { + expect(tr.eq(i).find('td').length).toEqual(7); + } + }); + }); + + describe('init-date', function(){ + beforeEach(function() { + $rootScope.date = null; + $rootScope.opts = { + initDate: new Date('November 9, 1980') + }; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + }); + + it('does not alter the model', function() { + expect($rootScope.date).toBe(null); + }); + + it('shows the correct title', function() { + expect(getTitle()).toBe('November 1980'); + }); + }); + + describe('min-date', function() { + it('should be able to specify a min-date through options', function() { + $rootScope.opts = { + minDate: new Date('September 12, 2010'), + shortcutPropagation: 'dog' + }; + + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + + var buttons = getAllOptionsEl(); + angular.forEach(buttons, function(button, index) { + expect(angular.element(button).prop('disabled')).toBe(index < 14); + }); + + $rootScope.opts.minDate = new Date('September 13, 2010'); + $rootScope.$digest(); + buttons = getAllOptionsEl(); + angular.forEach(buttons, function(button, index) { + expect(angular.element(button).prop('disabled')).toBe(index < 15); + }); + }); + }); + + describe('max-date', function() { + it('should be able to specify a max-date through options', function() { + $rootScope.opts = { + maxDate: new Date('September 25, 2010') + }; + + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + + var buttons = getAllOptionsEl(); + angular.forEach(buttons, function(button, index) { + expect(angular.element(button).prop('disabled')).toBe(index > 27); + }); + + $rootScope.opts.maxDate = new Date('September 15, 2010'); + $rootScope.$digest(); + buttons = getAllOptionsEl(); + angular.forEach(buttons, function(button, index) { + expect(angular.element(button).prop('disabled')).toBe(index > 17); + }); + }); + }); + + describe('min-mode', function() { + it('should be able to specify min-mode through options', function() { + $rootScope.opts = { + minMode: 'month' + }; + + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + + expect(getTitle()).toBe('2010'); + }); + }); + + describe('max-mode', function() { + it('should be able to specify max-mode through options', function() { + $rootScope.opts = { + maxMode: 'month' + }; + + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + + expect(getTitle()).toBe('September 2010'); + clickTitleButton(); + assignElements(wrapElement); + expect(getTitle()).toBe('2010'); + clickTitleButton(); + assignElements(wrapElement); + expect(getTitle()).toBe('2010'); + }); + }); + + describe('datepicker-mode', function() { + beforeEach(inject(function() { + $rootScope.date = new Date('August 11, 2013'); + $rootScope.opts = { + datepickerMode: 'month' + }; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('shows the correct title', function() { + expect(getTitle()).toBe('2013'); + }); + + it('updates binding', function() { + clickTitleButton(); + expect($rootScope.opts.datepickerMode).toBe('year'); + }); + }); + }); + + describe('option `init-date`', function() { + beforeEach(function() { + $rootScope.date = null; + $rootScope.options = { + initDate: new Date('November 9, 1980') + }; + }); + + describe('when initially set', function() { + beforeEach(function() { + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + }); + + it('does not alter the model', function() { + expect($rootScope.date).toBe(null); + }); + + it('shows the correct title', function() { + expect(getTitle()).toBe('November 1980'); + }); + }); + + describe('when modified before date selected.', function() { + beforeEach(function() { + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + + $rootScope.options.initDate = new Date('December 20, 1981'); + $rootScope.$digest(); + }); + + it('does not alter the model', function() { + expect($rootScope.date).toBe(null); + }); + + it('shows the correct title', function() { + expect(getTitle()).toBe('December 1981'); + }); + }); + + describe('when modified after date selected.', function() { + beforeEach(function() { + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + $rootScope.date = new Date('April 1, 1982'); + $rootScope.options.initDate = new Date('December 20, 1981'); + $rootScope.$digest(); + }); + + it('does not alter the model', function() { + expect($rootScope.date).toEqual(new Date('April 1, 1982')); + }); + + it('shows the correct title', function() { + expect(getTitle()).toBe('April 1982'); + }); + }); + }); + + describe('toggles programatically by `open` attribute', function() { + var wrapElement; + + beforeEach(inject(function() { + $rootScope.open = true; + wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('to display initially', function() { + expect(dropdownEl.length).toBe(1); + }); + + it('to close / open from scope variable', function() { + expect(dropdownEl.length).toBe(1); + $rootScope.open = false; + $rootScope.$digest(); + assignElements(wrapElement); + expect(dropdownEl.length).toBe(0); + + $rootScope.open = true; + $rootScope.$digest(); + assignElements(wrapElement); + expect(dropdownEl.length).toBe(1); + }); + }); + + describe('custom format', function() { + beforeEach(inject(function() { + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('to display the correct value in input', function() { + expect(inputEl.val()).toBe('30-September-2010'); + }); + + it('updates the input when a day is clicked', function() { + clickOption(17); + expect(inputEl.val()).toBe('15-September-2010'); + expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); + }); + + it('updates the input correctly when model changes', function() { + $rootScope.date = new Date('January 10, 1983 10:00:00'); + $rootScope.$digest(); + expect(inputEl.val()).toBe('10-January-1983'); + }); + }); + + describe('custom format with time', function() { + beforeEach(inject(function() { + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('updates the model correctly when the input value changes', function() { + $rootScope.date = new Date(2015, 10, 24, 10, 0); + $rootScope.$digest(); + expect(inputEl.val()).toBe('Nov-24-2015 10:00 AM'); + + inputEl.val('Nov-24-2015 11:00 AM').trigger('input'); + $rootScope.$digest(); + expect($rootScope.date).toEqual(new Date(2015, 10, 24, 11, 0)); + }); + }); + + describe('custom format with optional leading zeroes', function() { + beforeEach(inject(function() { + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('to display the correct value in input', function() { + expect(inputEl.val()).toBe('30-09-2010'); + }); + + it('updates the input when a day is clicked', function() { + clickOption(10); + expect(inputEl.val()).toBe('08-09-2010'); + expect($rootScope.date).toEqual(new Date('September 8, 2010 15:30:00')); + }); + + it('updates the input correctly when model changes', function() { + $rootScope.date = new Date('December 25, 1983 10:00:00'); + $rootScope.$digest(); + expect(inputEl.val()).toBe('25-12-1983'); + }); + }); + + describe('dynamic custom format', function() { + beforeEach(inject(function() { + $rootScope.format = 'dd-MMMM-yyyy'; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('to display the correct value in input', function() { + expect(inputEl.val()).toBe('30-September-2010'); + }); + + it('updates the input when a day is clicked', function() { + clickOption(17); + expect(inputEl.val()).toBe('15-September-2010'); + expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); + }); + + it('updates the input correctly when model changes', function() { + $rootScope.date = new Date('August 11, 2013 09:09:00'); + $rootScope.$digest(); + expect(inputEl.val()).toBe('11-August-2013'); + }); + + it('updates the input correctly when format changes', function() { + $rootScope.format = 'dd/MM/yyyy'; + $rootScope.$digest(); + expect(inputEl.val()).toBe('30/09/2010'); + }); + }); + + describe('format errors', function() { + var originalConfig = {}; + beforeEach(inject(function(uibDatepickerPopupConfig) { + angular.extend(originalConfig, uibDatepickerPopupConfig); + uibDatepickerPopupConfig.datepickerPopup = null; + })); + afterEach(inject(function(uibDatepickerPopupConfig) { + // return it to the original state + angular.extend(uibDatepickerPopupConfig, originalConfig); + })); + + it('should throw an error if there is no format', function() { + expect(function() { + $compile('
')($rootScope); + }).toThrow(new Error('uibDatepickerPopup must have a date format specified.')); + }); + + it('should throw an error if the format changes to null without fallback', function() { + $rootScope.format = 'dd-MMMM-yyyy'; + $compile('
')($rootScope); + $rootScope.$digest(); + + expect(function() { + $rootScope.format = null; + $rootScope.$digest(); + }).toThrow(new Error('uibDatepickerPopup must have a date format specified.')); + }); + + it('should thrown an error on date inputs with custom formats', function() { + expect(function() { + $compile('
')($rootScope); + }).toThrow(new Error('HTML5 date input types do not support custom formats.')); + }); + }); + + describe('european format', function() { + it('dd.MM.yyyy', function() { + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + + changeInputValueTo(inputEl, '11.08.2013'); + expect($rootScope.date.getFullYear()).toEqual(2013); + expect($rootScope.date.getMonth()).toEqual(7); + expect($rootScope.date.getDate()).toEqual(11); + }); + }); + + describe('`close-on-date-selection` attribute', function() { + var wrapElement; + beforeEach(inject(function() { + $rootScope.close = false; + wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('does not close the dropdown when a day is clicked', function() { + clickOption(17); + assignElements(wrapElement); + expect(dropdownEl.length).toBe(1); + }); + }); + + describe('button bar', function() { + var buttons, buttonBarElement; + + function assignButtonBar() { + buttonBarElement = dropdownEl.find('li').eq(-1); + buttons = buttonBarElement.find('button'); + } + + describe('', function() { + var wrapElement; + + beforeEach(inject(function() { + $rootScope.isopen = true; + wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + assignButtonBar(); + })); + + it('should exist', function() { + expect(dropdownEl.length).toBe(1); + expect(dropdownEl.find('li').length).toBe(2); + }); + + it('should have three buttons', function() { + expect(buttons.length).toBe(3); + + expect(buttons.eq(0).text()).toBe('Today'); + expect(buttons.eq(1).text()).toBe('Clear'); + expect(buttons.eq(2).text()).toBe('Done'); + }); + + it('should have a button to set today date without altering time part', function() { + var today = new Date(); + buttons.eq(0).click(); + expect($rootScope.date.getFullYear()).toBe(today.getFullYear()); + expect($rootScope.date.getMonth()).toBe(today.getMonth()); + expect($rootScope.date.getDate()).toBe(today.getDate()); + + expect($rootScope.date.getHours()).toBe(15); + expect($rootScope.date.getMinutes()).toBe(30); + expect($rootScope.date.getSeconds()).toBe(0); + }); + + it('should have a button to set today date if blank', function() { + $rootScope.date = null; + $rootScope.$digest(); + + var today = new Date(); + buttons.eq(0).click(); + expect($rootScope.date.getFullYear()).toBe(today.getFullYear()); + expect($rootScope.date.getMonth()).toBe(today.getMonth()); + expect($rootScope.date.getDate()).toBe(today.getDate()); + + expect($rootScope.date.getHours()).toBe(0); + expect($rootScope.date.getMinutes()).toBe(0); + expect($rootScope.date.getSeconds()).toBe(0); + }); + + it('should have a button to clear value', function() { + buttons.eq(1).click(); + expect($rootScope.date).toBe(null); + }); + + it('should have a button to close calendar', function() { + buttons.eq(2).click(); + assignElements(wrapElement); + expect(dropdownEl.length).toBe(0); + }); + }); + + describe('customization', function() { + it('should change text from attributes', function() { + $rootScope.clearText = 'Null it!'; + $rootScope.close = 'Close'; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + assignButtonBar(); + + expect(buttons.eq(0).text()).toBe('Now'); + expect(buttons.eq(1).text()).toBe('Null it!'); + expect(buttons.eq(2).text()).toBe('CloseME'); + }); + + it('should disable today button if before min date', function() { + var date = new Date(); + date.setDate(new Date().getDate() + 1); + $rootScope.options = { + minDate: date + }; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + assignButtonBar(); + + expect(buttons.eq(0).prop('disabled')).toBe(true); + }); + + it('should disable today button if before min date, yyyy-MM-dd case', inject(function(dateFilter) { + var date = new Date(); + date.setDate(new Date().getDate() + 1); + var literalMinDate = dateFilter(date, 'yyyy-MM-dd'); + $rootScope.options = { + minDate: literalMinDate + }; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + assignButtonBar(); + + expect(buttons.eq(0).prop('disabled')).toBe(true); + })); + + it('should not disable any button if min date is null', function() { + $rootScope.options = { + minDate: null + }; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + assignButtonBar(); + + for (var i = 0; i < buttons.length; i++) { + expect(buttons.eq(i).prop('disabled')).toBe(false); + } + }); + + it('should disable today button if after max date', function() { + var date = new Date(); + date.setDate(new Date().getDate() - 2); + $rootScope.options = { + maxDate: date + }; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + assignButtonBar(); + + expect(buttons.eq(0).prop('disabled')).toBe(true); + }); + + it('should not disable any button if max date is null', function() { + $rootScope.options = { + maxDate: null + }; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + assignButtonBar(); + + for (var i = 0; i < buttons.length; i++) { + expect(buttons.eq(i).prop('disabled')).toBe(false); + } + }); + + it('should remove bar', function() { + $rootScope.showBar = false; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + expect(dropdownEl.find('li').length).toBe(1); + }); + + it('should hide weeks column on popup', function() { + $rootScope.options = { + showWeeks: false + }; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + + expect(getLabelsRow().find('th').length).toEqual(7); + var tr = element.find('tbody').find('tr'); + for (var i = 0; i < 5; i++) { + expect(tr.eq(i).find('td').length).toEqual(7); + } + }); + + it('should show weeks column on popup', function() { + $rootScope.options = { + showWeeks: true + }; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + + expect(getLabelsRow().find('th').eq(0)).not.toBeHidden(); + var tr = element.find('tbody').find('tr'); + for (var i = 0; i < 5; i++) { + expect(tr.eq(i).find('td').eq(0)).not.toBeHidden(); + } + }); + }); + + describe('`ng-change`', function() { + beforeEach(inject(function() { + $rootScope.changeHandler = jasmine.createSpy('changeHandler'); + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + assignButtonBar(); + })); + + it('should be called when `today` is clicked', function() { + buttons.eq(0).click(); + expect($rootScope.changeHandler).toHaveBeenCalled(); + }); + + it('should be called when `clear` is clicked', function() { + buttons.eq(1).click(); + expect($rootScope.changeHandler).toHaveBeenCalled(); + }); + + it('should not be called when `close` is clicked', function() { + buttons.eq(2).click(); + expect($rootScope.changeHandler).not.toHaveBeenCalled(); + }); + }); + }); + + describe('use with `ng-required` directive', function() { + describe('`ng-required is true`', function() { + beforeEach(inject(function() { + $rootScope.date = ''; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('should be invalid initially and when no date', function() { + expect(inputEl.hasClass('ng-invalid')).toBeTruthy(); + }); + + it('should be valid if model has been specified', function() { + $rootScope.date = new Date(); + $rootScope.$digest(); + expect(inputEl.hasClass('ng-valid')).toBeTruthy(); + }); + + it('should be valid if model value is a valid timestamp', function() { + $rootScope.date = Date.now(); + $rootScope.$digest(); + expect(inputEl.hasClass('ng-valid')).toBeTruthy(); + }); + }); + + describe('`ng-required is false`', function() { + beforeEach(inject(function() { + $rootScope.date = ''; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('should be valid initially and when no date', function() { + expect(inputEl.hasClass('ng-valid')).toBeTruthy(); + }); + }); + }); + + describe('use with `ng-change` directive', function() { + beforeEach(inject(function() { + $rootScope.changeHandler = jasmine.createSpy('changeHandler'); + $rootScope.date = new Date('09/16/2010'); + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('should not be called initially', function() { + expect($rootScope.changeHandler).not.toHaveBeenCalled(); + }); + + it('should be called when a day is clicked', function() { + clickOption(17); + expect($rootScope.changeHandler).toHaveBeenCalled(); + }); + + it('should not be called when model changes programatically', function() { + $rootScope.date = new Date(); + $rootScope.$digest(); + expect($rootScope.changeHandler).not.toHaveBeenCalled(); + }); + }); + + describe('with disabled', function() { + var wrapElement; + + beforeEach(function() { + $rootScope.isOpen = false; + wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + }); + + it('should not open the popup', function() { + $rootScope.isOpen = true; + $rootScope.$digest(); + + expect($rootScope.isOpen).toBe(false); + expect(wrapElement.find('ul').length).toBe(0); + }); + }); + + describe('with ng-disabled', function() { + var wrapElement; + + beforeEach(function() { + $rootScope.disabled = false; + $rootScope.isOpen = false; + wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + }); + + it('should not open the popup when disabled', function() { + $rootScope.isOpen = true; + $rootScope.$digest(); + + expect($rootScope.isOpen).toBe(true); + expect(wrapElement.find('ul').length).toBe(1); + + $rootScope.isOpen = false; + $rootScope.$digest(); + + expect($rootScope.isOpen).toBe(false); + expect(wrapElement.find('ul').length).toBe(0); + + $rootScope.disabled = true; + $rootScope.isOpen = true; + $rootScope.$digest(); + + expect($rootScope.isOpen).toBe(false); + expect(wrapElement.find('ul').length).toBe(0); + + $rootScope.disabled = false; + $rootScope.isOpen = true; + $rootScope.$digest(); + + expect($rootScope.isOpen).toBe(true); + expect(wrapElement.find('ul').length).toBe(1); + }); + }); + + describe('with datepicker-popup-template-url', function() { + beforeEach(function() { + $rootScope.date = new Date(); + }); + + afterEach(function () { + $document.find('body').find('.dropdown-menu').remove(); + }); + + it('should allow custom templates for the popup', function() { + $templateCache.put('foo/bar.html', '
baz
'); + + var elm = angular.element('
'); + + $compile(elm)($rootScope); + $rootScope.$digest(); + + expect(elm.children().eq(1).html()).toBe('baz'); + }); + }); + + describe('with datepicker-template-url', function() { + beforeEach(function() { + $rootScope.date = new Date(); + }); + + afterEach(function() { + $document.find('body').find('.dropdown-menu').remove(); + }); + + it('should allow custom templates for the datepicker', function() { + $templateCache.put('foo/bar.html', '
baz
'); + + var elm = angular.element('
'); + + $compile(elm)($rootScope); + $rootScope.$digest(); + + var datepicker = elm.find('[uib-datepicker]'); + + expect(datepicker.html()).toBe('baz'); + }); + }); + + describe('with an append-to-body attribute', function() { + beforeEach(function() { + $rootScope.date = new Date(); + }); + + afterEach(function() { + $document.find('body').children().remove(); + }); + + it('should append to the body', function() { + var $body = $document.find('body'), + bodyLength = $body.children().length, + elm = angular.element( + '
' + ); + $compile(elm)($rootScope); + $rootScope.$digest(); + + expect($body.children().length).toEqual(bodyLength + 1); + expect(elm.children().length).toEqual(1); + }); + + it('should be removed on scope destroy', function() { + var $body = $document.find('body'), + bodyLength = $body.children().length, + isolatedScope = $rootScope.$new(), + elm = angular.element( + '' + ); + $compile(elm)(isolatedScope); + isolatedScope.$digest(); + expect($body.children().length).toEqual(bodyLength + 1); + isolatedScope.$destroy(); + expect($body.children().length).toEqual(bodyLength); + }); + }); + + describe('with setting datepickerConfig.showWeeks to false', function() { + var originalConfig = {}; + beforeEach(inject(function(uibDatepickerConfig) { + angular.extend(originalConfig, uibDatepickerConfig); + uibDatepickerConfig.showWeeks = false; + + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + afterEach(inject(function(uibDatepickerConfig) { + // return it to the original state + angular.extend(uibDatepickerConfig, originalConfig); + })); + + it('changes initial visibility for weeks', function() { + expect(getLabelsRow().find('th').length).toEqual(7); + var tr = element.find('tbody').find('tr'); + for (var i = 0; i < 5; i++) { + expect(tr.eq(i).find('td').length).toEqual(7); + } + }); + }); + + describe('`datepicker-mode`', function() { + beforeEach(inject(function() { + $rootScope.date = new Date('August 11, 2013'); + $rootScope.options = { + datepickerMode: 'month' + }; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('shows the correct title', function() { + expect(getTitle()).toBe('2013'); + }); + + it('updates binding', function() { + clickTitleButton(); + expect($rootScope.options.datepickerMode).toBe('year'); + }); + }); + + describe('attribute `onOpenFocus`', function() { + beforeEach(function() { + $rootScope.date = null; + $rootScope.isopen = false; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + }); + + it('should remain focused on the input', function() { + var focused = true; + expect(dropdownEl.length).toBe(0); + + inputEl[0].focus(); + inputEl.on('blur', function() { + focused = false; + }); + $rootScope.isopen = true; + $rootScope.$digest(); + + expect(inputEl.parent().find('.dropdown-menu').length).toBe(1); + expect(focused).toBe(true); + }); + }); + + describe('altInputFormats', function() { + describe('datepickerPopupConfig.altInputFormats', function() { + var originalConfig = {}; + beforeEach(inject(function(uibDatepickerPopupConfig) { + $rootScope.date = new Date('November 9, 1980'); + angular.extend(originalConfig, uibDatepickerPopupConfig); + uibDatepickerPopupConfig.datepickerPopup = 'MM-dd-yyyy'; + uibDatepickerPopupConfig.altInputFormats = ['M!/d!/yyyy']; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + afterEach(inject(function(uibDatepickerPopupConfig) { + // return it to the original state + angular.extend(uibDatepickerPopupConfig, originalConfig); + })); + + it('changes date format', function() { + changeInputValueTo(inputEl, '11/8/1980'); + + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(10); + expect($rootScope.date.getDate()).toEqual(8); + }); + + it('changes the datepicker', function() { + expect(selectedElementIndex()).toEqual(14); + changeInputValueTo(inputEl, '11/8/1980'); + expect(selectedElementIndex()).toEqual(13); + }); + }); + + describe('attribute `alt-input-formats`', function() { + beforeEach(function() { + $rootScope.date = new Date('November 9, 1980'); + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + }); + + it('should accept alternate input formats', function() { + changeInputValueTo(inputEl, '11/8/1980'); + + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(10); + expect($rootScope.date.getDate()).toEqual(8); + }); + + it('changes the datepicker', function() { + expect(selectedElementIndex()).toEqual(14); + changeInputValueTo(inputEl, '11/8/1980'); + expect(selectedElementIndex()).toEqual(13); + }); + }); + }); + + describe('uibDatepickerConfig ngModelOptions', function() { + var inputEl, dropdownEl; + + function assignElements(wrapElement) { + inputEl = wrapElement.find('input'); + dropdownEl = wrapElement.find('ul'); + element = dropdownEl.find('table'); + } + + beforeEach(inject(function(uibDatepickerConfig) { + uibDatepickerConfig.ngModelOptions = { timezone: '+600' }; + $rootScope.date = new Date('2010-09-30T10:00:00.000Z'); + $rootScope.isopen = true; + })); + + afterEach(inject(function(uibDatepickerConfig) { + uibDatepickerConfig.ngModelOptions = {}; + })); + + describe('timezone', function() { + beforeEach(inject(function(uibDatepickerConfig) { + var wrapper = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapper); + })); + + it('interprets the date appropriately', function() { + expect(inputEl.val()).toBe('09/30/2010'); + }); + + it('updates the input when a day is clicked', function() { + clickOption(17); + expect(inputEl.val()).toBe('09/15/2010'); + expect($rootScope.date).toEqual(new Date('2010-09-15T10:00:00.000Z')); + }); + + it('shows the correct title', function() { + expect(getTitle()).toBe('September 2010'); + }); + }); + + it('timezone interprets init date appropriately', function() { + $rootScope.options = { + initDate: new Date('2010-09-30T23:00:00.000Z') + }; + $rootScope.date = null; + var wrapper = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapper); + + expect(getTitle()).toBe('October 2010'); + }); + + it('timezone interprets min date appropriately', function() { + $rootScope.options = { + minDate: new Date('2010-10-01T00:00:00.000Z') + }; + var wrapper = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapper); + + expect(getSelectedElement().prop('disabled')).toBe(true); + }); + }); + + describe('ng-model-options', function() { + describe('timezone', function() { + var inputEl, dropdownEl, $document, $sniffer, $timeout; + + function assignElements(wrapElement) { + inputEl = wrapElement.find('input'); + dropdownEl = wrapElement.find('ul'); + element = dropdownEl.find('table'); + } + + beforeEach(function() { + $rootScope.date = new Date('2010-09-30T10:00:00.000Z'); + $rootScope.ngModelOptions = { timezone: '+600' }; + $rootScope.isopen = true; + var wrapper = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapper); + }); + + it('interprets the date appropriately', function() { + expect(inputEl.val()).toBe('09/30/2010'); + }); + + it('has `selected` only the correct day', function() { + expectSelectedElement(32); + }); + + it('updates the input when a day is clicked', function() { + clickOption(17); + expect(inputEl.val()).toBe('09/15/2010'); + expect($rootScope.date).toEqual(new Date('2010-09-15T10:00:00.000Z')); + }); + }); + + describe('timezone HTML5 date input', function() { + var inputEl, dropdownEl, $document, $sniffer, $timeout; + + function assignElements(wrapElement) { + inputEl = wrapElement.find('input'); + dropdownEl = wrapElement.find('ul'); + element = dropdownEl.find('table'); + } + + beforeEach(function() { + $rootScope.date = new Date('2010-09-30T10:00:00.000Z'); + $rootScope.ngModelOptions = { timezone: '+600' }; + $rootScope.isopen = true; + var wrapper = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapper); + }); + + it('interprets the date appropriately', function() { + expect(inputEl.val()).toBe('2010-09-30'); + }); + + it('has `selected` only the correct day', function() { + expectSelectedElement(32); + }); + + it('updates the input when a day is clicked', function() { + clickOption(17); + expect(inputEl.val()).toBe('2010-09-15'); + expect($rootScope.date).toEqual(new Date('2010-09-15T10:00:00.000Z')); + }); + }); + }); +}); diff --git a/template/datepicker/popup.html b/template/datepickerPopup/popup.html similarity index 100% rename from template/datepicker/popup.html rename to template/datepickerPopup/popup.html