From 220b7e4674c59199af6f1c93018c009f6a4326ba Mon Sep 17 00:00:00 2001 From: Daniel Gornstein Date: Thu, 14 Jan 2016 19:23:54 -0800 Subject: [PATCH 1/3] chore(datepicker): Unregister parent watchers on $destroy Closes #5242 --- src/datepicker/datepicker.js | 693 ++++++++++++++++++----------------- 1 file changed, 353 insertions(+), 340 deletions(-) diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index 4f7db572f7..65e35fd761 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -26,7 +26,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError, dateParser) { var self = this, ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl; - ngModelOptions = {}; + ngModelOptions = {}, + watchListeners = []; // Modes chain this.modes = ['day', 'month', 'year']; @@ -44,10 +45,10 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst // Watchable date attributes angular.forEach(['minDate', 'maxDate'], function(key) { if ($attrs[key]) { - $scope.$parent.$watch($attrs[key], function(value) { + watchListeners.push($scope.$parent.$watch($attrs[key], function(value) { self[key] = value ? angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium')) : null; self.refreshView(); - }); + })); } else { self[key] = datepickerConfig[key] ? dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : null; } @@ -55,13 +56,13 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst angular.forEach(['minMode', 'maxMode'], function(key) { if ($attrs[key]) { - $scope.$parent.$watch($attrs[key], function(value) { + watchListeners.push($scope.$parent.$watch($attrs[key], function(value) { self[key] = $scope[key] = angular.isDefined(value) ? value : $attrs[key]; if (key === 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key]) || key === 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key])) { $scope.datepickerMode = self[key]; } - }); + })); } else { self[key] = $scope[key] = datepickerConfig[key] || null; } @@ -72,22 +73,22 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst if (angular.isDefined($attrs.initDate)) { this.activeDate = dateParser.fromTimezone($scope.$parent.$eval($attrs.initDate), ngModelOptions.timezone) || new Date(); - $scope.$parent.$watch($attrs.initDate, function(initDate) { + watchListeners.push($scope.$parent.$watch($attrs.initDate, function(initDate) { if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) { self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone); self.refreshView(); } - }); + })); } else { this.activeDate = new Date(); } $scope.disabled = angular.isDefined($attrs.disabled) || false; if (angular.isDefined($attrs.ngDisabled)) { - $scope.$parent.$watch($attrs.ngDisabled, function(disabled) { + watchListeners.push($scope.$parent.$watch($attrs.ngDisabled, function(disabled) { $scope.disabled = disabled; self.refreshView(); - }); + })); } $scope.isActive = function(dateObject) { @@ -248,6 +249,10 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst self.refreshView(); } }; + + $scope.$on("$destroy", function() { + clearWatchListeners(watchListeners); + }); }]) .controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { @@ -572,393 +577,395 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst }) .controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig', -function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig) { - var self = this; - var cache = {}, - isHtml5DateInput = false; - var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus, - datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, - ngModel, ngModelOptions, $popup, altInputFormats; - - scope.watchData = {}; - - this.init = function(_ngModel_) { - ngModel = _ngModel_; - ngModelOptions = _ngModel_.$options || datepickerConfig.ngModelOptions; - 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.'); - } + function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig) { + var cache = {}, + isHtml5DateInput = false; + var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus, + datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, + ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = []; + + scope.watchData = {}; + + this.init = function(_ngModel_) { + ngModel = _ngModel_; + ngModelOptions = _ngModel_.$options || datepickerConfig.ngModelOptions; + 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 (!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.'); - } + 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('
'); - scope.ngModelOptions = angular.copy(ngModelOptions); - scope.ngModelOptions.timezone = null; - popupEl.attr({ - 'ng-model': 'date', - 'ng-model-options': 'ngModelOptions', - 'ng-change': 'dateSelection(date)', - 'template-url': datepickerPopupTemplateUrl - }); - - // datepicker element - datepickerEl = angular.element(popupEl.children()[0]); - datepickerEl.attr('template-url', datepickerTemplateUrl); - - if (isHtml5DateInput) { - if (attrs.type === 'month') { - datepickerEl.attr('datepicker-mode', '"month"'); - datepickerEl.attr('min-mode', 'month'); - } - } + // popup element used to display calendar + popupEl = angular.element('
'); + scope.ngModelOptions = angular.copy(ngModelOptions); + scope.ngModelOptions.timezone = null; + popupEl.attr({ + 'ng-model': 'date', + 'ng-model-options': 'ngModelOptions', + 'ng-change': 'dateSelection(date)', + 'template-url': datepickerPopupTemplateUrl + }); - if (attrs.datepickerOptions) { - var options = scope.$parent.$eval(attrs.datepickerOptions); - if (options && options.initDate) { - scope.initDate = dateParser.fromTimezone(options.initDate, ngModelOptions.timezone); - datepickerEl.attr('init-date', 'initDate'); - delete options.initDate; - } - angular.forEach(options, function(value, option) { - datepickerEl.attr(cameltoDash(option), value); - }); - } + // datepicker element + datepickerEl = angular.element(popupEl.children()[0]); + datepickerEl.attr('template-url', datepickerTemplateUrl); - angular.forEach(['minMode', 'maxMode'], function(key) { - if (attrs[key]) { - scope.$parent.$watch(function() { return attrs[key]; }, function(value) { - scope.watchData[key] = value; - }); - datepickerEl.attr(cameltoDash(key), 'watchData.' + key); - } - }); - - angular.forEach(['datepickerMode', 'shortcutPropagation'], function(key) { - if (attrs[key]) { - var getAttribute = $parse(attrs[key]); - var propConfig = { - get: function() { - return getAttribute(scope.$parent); + if (isHtml5DateInput) { + if (attrs.type === 'month') { + datepickerEl.attr('datepicker-mode', '"month"'); + datepickerEl.attr('min-mode', 'month'); + } } - }; - datepickerEl.attr(cameltoDash(key), 'watchData.' + key); - - // Propagate changes from datepicker to outside - if (key === 'datepickerMode') { - var setAttribute = getAttribute.assign; - propConfig.set = function(v) { - setAttribute(scope.$parent, v); - }; - } + if (attrs.datepickerOptions) { + var options = scope.$parent.$eval(attrs.datepickerOptions); + if (options && options.initDate) { + scope.initDate = dateParser.fromTimezone(options.initDate, ngModelOptions.timezone); + datepickerEl.attr('init-date', 'initDate'); + delete options.initDate; + } + angular.forEach(options, function(value, option) { + datepickerEl.attr(cameltoDash(option), value); + }); + } - Object.defineProperty(scope.watchData, key, propConfig); - } - }); + angular.forEach(['minMode', 'maxMode'], function(key) { + if (attrs[key]) { + watchListeners.push(scope.$parent.$watch(function() { return attrs[key]; }, function(value) { + scope.watchData[key] = value; + })); + datepickerEl.attr(cameltoDash(key), 'watchData.' + key); + } + }); - angular.forEach(['minDate', 'maxDate', 'initDate'], function(key) { - if (attrs[key]) { - var getAttribute = $parse(attrs[key]); + angular.forEach(['datepickerMode', 'shortcutPropagation'], function(key) { + if (attrs[key]) { + var getAttribute = $parse(attrs[key]); + var propConfig = { + get: function() { + return getAttribute(scope.$parent); + } + }; + + datepickerEl.attr(cameltoDash(key), 'watchData.' + key); + + // Propagate changes from datepicker to outside + if (key === 'datepickerMode') { + var setAttribute = getAttribute.assign; + propConfig.set = function(v) { + setAttribute(scope.$parent, v); + }; + } + + Object.defineProperty(scope.watchData, key, propConfig); + } + }); - scope.$parent.$watch(getAttribute, function(value) { - if (key === 'minDate' || key === 'maxDate') { - cache[key] = angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium')); - } + angular.forEach(['minDate', 'maxDate', 'initDate'], function(key) { + if (attrs[key]) { + var getAttribute = $parse(attrs[key]); - scope.watchData[key] = cache[key] || dateParser.fromTimezone(new Date(value), ngModelOptions.timezone); - }); + watchListeners.push(scope.$parent.$watch(getAttribute, function(value) { + if (key === 'minDate' || key === 'maxDate') { + cache[key] = angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium')); + } - datepickerEl.attr(cameltoDash(key), 'watchData.' + key); - } - }); + scope.watchData[key] = cache[key] || dateParser.fromTimezone(new Date(value), ngModelOptions.timezone); + })); - if (attrs.dateDisabled) { - datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); - } + datepickerEl.attr(cameltoDash(key), 'watchData.' + key); + } + }); - angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', 'showWeeks', 'startingDay', 'yearRows', 'yearColumns'], function(key) { - if (angular.isDefined(attrs[key])) { - datepickerEl.attr(cameltoDash(key), attrs[key]); - } - }); + if (attrs.dateDisabled) { + datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); + } - if (attrs.customClass) { - datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })'); - } + angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', 'showWeeks', 'startingDay', 'yearRows', 'yearColumns'], function(key) { + if (angular.isDefined(attrs[key])) { + datepickerEl.attr(cameltoDash(key), attrs[key]); + } + }); - 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, ngModelOptions.timezone); - dateFormat = dateFormat.replace(/M!/, 'MM') - .replace(/d!/, 'dd'); + if (attrs.customClass) { + datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })'); + } - return dateFilter(scope.date, dateFormat); - }); - } else { - ngModel.$formatters.push(function(value) { - scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone); - return value; - }); - } + 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, ngModelOptions.timezone); + dateFormat = dateFormat.replace(/M!/, 'MM') + .replace(/d!/, 'dd'); + + return dateFilter(scope.date, dateFormat); + }); + } else { + ngModel.$formatters.push(function(value) { + scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone); + return value; + }); + } - // Detect changes in the view from the text box - ngModel.$viewChangeListeners.push(function() { - scope.date = parseDateString(ngModel.$viewValue); - }); + // Detect changes in the view from the text box + ngModel.$viewChangeListeners.push(function() { + scope.date = parseDateString(ngModel.$viewValue); + }); - element.bind('keydown', inputKeydownBind); + element.bind('keydown', inputKeydownBind); - $popup = $compile(popupEl)(scope); - // Prevent jQuery cache memory leak (template is now redundant after linking) - popupEl.remove(); + $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); - } + 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; - }); - } - } + scope.$on('$destroy', function() { + if (scope.isOpen === true) { + if (!$rootScope.$$phase) { + scope.$apply(function() { + scope.isOpen = false; + }); + } + } - $popup.remove(); - element.unbind('keydown', inputKeydownBind); - $document.unbind('click', documentClickBind); - }); - }; + $popup.remove(); + element.unbind('keydown', inputKeydownBind); + $document.unbind('click', documentClickBind); - scope.getText = function(key) { - return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; - }; + //Clear all watch listeners on destroy + clearWatchListeners(watchListeners); + }); + }; - scope.isDisabled = function(date) { - if (date === 'today') { - date = new Date(); - } + scope.getText = function(key) { + return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; + }; - return scope.watchData.minDate && scope.compare(date, cache.minDate) < 0 || - scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0; - }; + scope.isDisabled = function(date) { + if (date === 'today') { + date = new Date(); + } - scope.compare = function(date1, date2) { - return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); - }; + return scope.watchData.minDate && scope.compare(date, cache.minDate) < 0 || + scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0; + }; - // Inner change - scope.dateSelection = function(dt) { - if (angular.isDefined(dt)) { - scope.date = dt; - } - var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function - element.val(date); - ngModel.$setViewValue(date); + scope.compare = function(date1, date2) { + return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); + }; - if (closeOnDateSelection) { - scope.isOpen = false; - element[0].focus(); - } - }; + // Inner change + scope.dateSelection = function(dt) { + if (angular.isDefined(dt)) { + scope.date = dt; + } + var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function + element.val(date); + ngModel.$setViewValue(date); - scope.keydown = function(evt) { - if (evt.which === 27) { - evt.stopPropagation(); - scope.isOpen = false; - element[0].focus(); - } - }; + if (closeOnDateSelection) { + scope.isOpen = false; + element[0].focus(); + } + }; - scope.select = function(date) { - 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.keydown = function(evt) { + if (evt.which === 27) { + evt.stopPropagation(); + scope.isOpen = false; + element[0].focus(); + } + }; - scope.close = function() { - scope.isOpen = false; - element[0].focus(); - }; + scope.select = function(date) { + 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.disabled = angular.isDefined(attrs.disabled) || false; - if (attrs.ngDisabled) { - scope.$parent.$watch($parse(attrs.ngDisabled), function(disabled) { - scope.disabled = disabled; - }); - } + scope.close = function() { + scope.isOpen = false; + element[0].focus(); + }; - scope.$watch('isOpen', function(value) { - if (value) { - if (!scope.disabled) { - scope.position = appendToBody ? $position.offset(element) : $position.position(element); - scope.position.top = scope.position.top + element.prop('offsetHeight'); + scope.disabled = angular.isDefined(attrs.disabled) || false; + if (attrs.ngDisabled) { + watchListeners.push(scope.$parent.$watch($parse(attrs.ngDisabled), function(disabled) { + scope.disabled = disabled; + })); + } - $timeout(function() { - if (onOpenFocus) { - scope.$broadcast('uib:datepicker.focus'); + scope.$watch('isOpen', function(value) { + if (value) { + if (!scope.disabled) { + scope.position = appendToBody ? $position.offset(element) : $position.position(element); + scope.position.top = scope.position.top + element.prop('offsetHeight'); + + $timeout(function() { + if (onOpenFocus) { + scope.$broadcast('uib:datepicker.focus'); + } + $document.bind('click', documentClickBind); + }, 0, false); + } else { + scope.isOpen = false; + } + } else { + $document.unbind('click', documentClickBind); } - $document.bind('click', documentClickBind); - }, 0, false); - } else { - scope.isOpen = false; - } - } else { - $document.unbind('click', documentClickBind); - } - }); + }); - function cameltoDash(string) { - return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); - } + 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)) { + 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; } - } - } - return date; - } - function parseDate(viewValue) { - if (angular.isNumber(viewValue)) { - // presumably timestamp to date object - viewValue = new Date(viewValue); - } + function parseDate(viewValue) { + if (angular.isNumber(viewValue)) { + // presumably timestamp to date object + viewValue = new Date(viewValue); + } - if (!viewValue) { - return null; - } + if (!viewValue) { + return null; + } - if (angular.isDate(viewValue) && !isNaN(viewValue)) { - return viewValue; - } + if (angular.isDate(viewValue) && !isNaN(viewValue)) { + return viewValue; + } - if (angular.isString(viewValue)) { - var date = parseDateString(viewValue); - if (!isNaN(date)) { - return dateParser.toTimezone(date, ngModelOptions.timezone); - } - } + if (angular.isString(viewValue)) { + var date = parseDateString(viewValue); + if (!isNaN(date)) { + return dateParser.toTimezone(date, ngModelOptions.timezone); + } + } - return ngModel.$options && ngModel.$options.allowInvalid ? viewValue : undefined; - } + return ngModel.$options && ngModel.$options.allowInvalid ? viewValue : undefined; + } - function validator(modelValue, viewValue) { - var value = modelValue || viewValue; + function validator(modelValue, viewValue) { + var value = modelValue || viewValue; - if (!attrs.ngRequired && !value) { - return true; - } + if (!attrs.ngRequired && !value) { + return true; + } - if (angular.isNumber(value)) { - value = new Date(value); - } + if (angular.isNumber(value)) { + value = new Date(value); + } - if (!value) { - return true; - } + if (!value) { + return true; + } - if (angular.isDate(value) && !isNaN(value)) { - return true; - } + if (angular.isDate(value) && !isNaN(value)) { + return true; + } - if (angular.isString(value)) { - return !isNaN(parseDateString(viewValue)); - } + if (angular.isString(value)) { + return !isNaN(parseDateString(viewValue)); + } - return false; - } + return false; + } - function documentClickBind(event) { - if (!scope.isOpen && scope.disabled) { - return; - } + 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; - }); - } - } + 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 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; + }); + } + } + }]) .directive('uibDatepickerPopup', function() { return { @@ -990,3 +997,9 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi } }; }); + +function clearWatchListeners(watchListeners) { + for (var i = 0; i < watchListeners.length; i++) { + watchListeners[i](); + } +} \ No newline at end of file From 64831b5d8dab6a5bf0c842098b4ec4ddbc283afc Mon Sep 17 00:00:00 2001 From: Daniel Gornstein Date: Thu, 14 Jan 2016 19:36:38 -0800 Subject: [PATCH 2/3] chore(datepicker): Unregister parent watchers on $destroy Closes #5242 --- src/datepicker/datepicker.js | 666 +++++++++++++++++------------------ 1 file changed, 333 insertions(+), 333 deletions(-) diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index 65e35fd761..6fe81afe95 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -577,395 +577,395 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst }) .controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig', - function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig) { - var cache = {}, - isHtml5DateInput = false; - var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus, - datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, - ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = []; - - scope.watchData = {}; - - this.init = function(_ngModel_) { - ngModel = _ngModel_; - ngModelOptions = _ngModel_.$options || datepickerConfig.ngModelOptions; - 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.'); - } - } - }); - } +function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig) { + var cache = {}, + isHtml5DateInput = false; + var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus, + datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, + ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = []; + + scope.watchData = {}; + + this.init = function(_ngModel_) { + ngModel = _ngModel_; + ngModelOptions = _ngModel_.$options || datepickerConfig.ngModelOptions; + 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 (isHtml5DateInput && attrs.uibDatepickerPopup) { - throw new Error('HTML5 date input types do not support custom formats.'); - } + if (!dateFormat) { + throw new Error('uibDatepickerPopup must have a date format specified.'); + } - // popup element used to display calendar - popupEl = angular.element('
'); - scope.ngModelOptions = angular.copy(ngModelOptions); - scope.ngModelOptions.timezone = null; - popupEl.attr({ - 'ng-model': 'date', - 'ng-model-options': 'ngModelOptions', - 'ng-change': 'dateSelection(date)', - 'template-url': datepickerPopupTemplateUrl - }); + if (isHtml5DateInput && attrs.uibDatepickerPopup) { + throw new Error('HTML5 date input types do not support custom formats.'); + } - // datepicker element - datepickerEl = angular.element(popupEl.children()[0]); - datepickerEl.attr('template-url', datepickerTemplateUrl); + // popup element used to display calendar + popupEl = angular.element('
'); + scope.ngModelOptions = angular.copy(ngModelOptions); + scope.ngModelOptions.timezone = null; + popupEl.attr({ + 'ng-model': 'date', + 'ng-model-options': 'ngModelOptions', + 'ng-change': 'dateSelection(date)', + 'template-url': datepickerPopupTemplateUrl + }); + + // datepicker element + datepickerEl = angular.element(popupEl.children()[0]); + datepickerEl.attr('template-url', datepickerTemplateUrl); + + if (isHtml5DateInput) { + if (attrs.type === 'month') { + datepickerEl.attr('datepicker-mode', '"month"'); + datepickerEl.attr('min-mode', 'month'); + } + } - if (isHtml5DateInput) { - if (attrs.type === 'month') { - datepickerEl.attr('datepicker-mode', '"month"'); - datepickerEl.attr('min-mode', 'month'); - } - } + if (attrs.datepickerOptions) { + var options = scope.$parent.$eval(attrs.datepickerOptions); + if (options && options.initDate) { + scope.initDate = dateParser.fromTimezone(options.initDate, ngModelOptions.timezone); + datepickerEl.attr('init-date', 'initDate'); + delete options.initDate; + } + angular.forEach(options, function(value, option) { + datepickerEl.attr(cameltoDash(option), value); + }); + } - if (attrs.datepickerOptions) { - var options = scope.$parent.$eval(attrs.datepickerOptions); - if (options && options.initDate) { - scope.initDate = dateParser.fromTimezone(options.initDate, ngModelOptions.timezone); - datepickerEl.attr('init-date', 'initDate'); - delete options.initDate; - } - angular.forEach(options, function(value, option) { - datepickerEl.attr(cameltoDash(option), value); - }); + angular.forEach(['minMode', 'maxMode'], function(key) { + if (attrs[key]) { + watchListeners.push(scope.$parent.$watch(function() { return attrs[key]; }, function(value) { + scope.watchData[key] = value; + })); + datepickerEl.attr(cameltoDash(key), 'watchData.' + key); + } + }); + + angular.forEach(['datepickerMode', 'shortcutPropagation'], function(key) { + if (attrs[key]) { + var getAttribute = $parse(attrs[key]); + var propConfig = { + get: function() { + return getAttribute(scope.$parent); } + }; - angular.forEach(['minMode', 'maxMode'], function(key) { - if (attrs[key]) { - watchListeners.push(scope.$parent.$watch(function() { return attrs[key]; }, function(value) { - scope.watchData[key] = value; - })); - datepickerEl.attr(cameltoDash(key), 'watchData.' + key); - } - }); + datepickerEl.attr(cameltoDash(key), 'watchData.' + key); - angular.forEach(['datepickerMode', 'shortcutPropagation'], function(key) { - if (attrs[key]) { - var getAttribute = $parse(attrs[key]); - var propConfig = { - get: function() { - return getAttribute(scope.$parent); - } - }; - - datepickerEl.attr(cameltoDash(key), 'watchData.' + key); - - // Propagate changes from datepicker to outside - if (key === 'datepickerMode') { - var setAttribute = getAttribute.assign; - propConfig.set = function(v) { - setAttribute(scope.$parent, v); - }; - } - - Object.defineProperty(scope.watchData, key, propConfig); - } - }); + // Propagate changes from datepicker to outside + if (key === 'datepickerMode') { + var setAttribute = getAttribute.assign; + propConfig.set = function(v) { + setAttribute(scope.$parent, v); + }; + } - angular.forEach(['minDate', 'maxDate', 'initDate'], function(key) { - if (attrs[key]) { - var getAttribute = $parse(attrs[key]); + Object.defineProperty(scope.watchData, key, propConfig); + } + }); - watchListeners.push(scope.$parent.$watch(getAttribute, function(value) { - if (key === 'minDate' || key === 'maxDate') { - cache[key] = angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium')); - } + angular.forEach(['minDate', 'maxDate', 'initDate'], function(key) { + if (attrs[key]) { + var getAttribute = $parse(attrs[key]); - scope.watchData[key] = cache[key] || dateParser.fromTimezone(new Date(value), ngModelOptions.timezone); - })); + watchListeners.push(scope.$parent.$watch(getAttribute, function(value) { + if (key === 'minDate' || key === 'maxDate') { + cache[key] = angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium')); + } - datepickerEl.attr(cameltoDash(key), 'watchData.' + key); - } - }); + scope.watchData[key] = cache[key] || dateParser.fromTimezone(new Date(value), ngModelOptions.timezone); + })); - if (attrs.dateDisabled) { - datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); - } + datepickerEl.attr(cameltoDash(key), 'watchData.' + key); + } + }); - angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', 'showWeeks', 'startingDay', 'yearRows', 'yearColumns'], function(key) { - if (angular.isDefined(attrs[key])) { - datepickerEl.attr(cameltoDash(key), attrs[key]); - } - }); + if (attrs.dateDisabled) { + datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); + } - if (attrs.customClass) { - datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })'); - } + angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', 'showWeeks', 'startingDay', 'yearRows', 'yearColumns'], function(key) { + if (angular.isDefined(attrs[key])) { + datepickerEl.attr(cameltoDash(key), attrs[key]); + } + }); - 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, ngModelOptions.timezone); - dateFormat = dateFormat.replace(/M!/, 'MM') - .replace(/d!/, 'dd'); - - return dateFilter(scope.date, dateFormat); - }); - } else { - ngModel.$formatters.push(function(value) { - scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone); - return value; - }); - } + if (attrs.customClass) { + datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })'); + } - // Detect changes in the view from the text box - ngModel.$viewChangeListeners.push(function() { - scope.date = parseDateString(ngModel.$viewValue); - }); + 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, ngModelOptions.timezone); + dateFormat = dateFormat.replace(/M!/, 'MM') + .replace(/d!/, 'dd'); + + return dateFilter(scope.date, dateFormat); + }); + } else { + ngModel.$formatters.push(function(value) { + scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone); + return value; + }); + } - element.bind('keydown', inputKeydownBind); + // Detect changes in the view from the text box + ngModel.$viewChangeListeners.push(function() { + scope.date = parseDateString(ngModel.$viewValue); + }); - $popup = $compile(popupEl)(scope); - // Prevent jQuery cache memory leak (template is now redundant after linking) - popupEl.remove(); + element.bind('keydown', inputKeydownBind); - if (appendToBody) { - $document.find('body').append($popup); - } else { - element.after($popup); - } + $popup = $compile(popupEl)(scope); + // Prevent jQuery cache memory leak (template is now redundant after linking) + popupEl.remove(); - scope.$on('$destroy', function() { - if (scope.isOpen === true) { - if (!$rootScope.$$phase) { - scope.$apply(function() { - scope.isOpen = false; - }); - } - } - - $popup.remove(); - element.unbind('keydown', inputKeydownBind); - $document.unbind('click', documentClickBind); - - //Clear all watch listeners on destroy - clearWatchListeners(watchListeners); + 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; }); - }; + } + } - scope.getText = function(key) { - return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; - }; + $popup.remove(); + element.unbind('keydown', inputKeydownBind); + $document.unbind('click', documentClickBind); - scope.isDisabled = function(date) { - if (date === 'today') { - date = new Date(); - } + //Clear all watch listeners on destroy + clearWatchListeners(watchListeners); + }); + }; - return scope.watchData.minDate && scope.compare(date, cache.minDate) < 0 || - scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0; - }; + scope.getText = function(key) { + return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; + }; - scope.compare = function(date1, date2) { - return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); - }; + scope.isDisabled = function(date) { + if (date === 'today') { + date = new Date(); + } - // Inner change - scope.dateSelection = function(dt) { - if (angular.isDefined(dt)) { - scope.date = dt; - } - var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function - element.val(date); - ngModel.$setViewValue(date); + return scope.watchData.minDate && scope.compare(date, cache.minDate) < 0 || + scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0; + }; - if (closeOnDateSelection) { - scope.isOpen = false; - element[0].focus(); - } - }; + scope.compare = function(date1, date2) { + return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); + }; - scope.keydown = function(evt) { - if (evt.which === 27) { - evt.stopPropagation(); - scope.isOpen = false; - element[0].focus(); - } - }; + // Inner change + scope.dateSelection = function(dt) { + if (angular.isDefined(dt)) { + scope.date = dt; + } + var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function + element.val(date); + ngModel.$setViewValue(date); - scope.select = function(date) { - 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); - }; + if (closeOnDateSelection) { + scope.isOpen = false; + element[0].focus(); + } + }; - scope.close = function() { - scope.isOpen = false; - element[0].focus(); - }; + scope.keydown = function(evt) { + if (evt.which === 27) { + 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.select = function(date) { + 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.$watch('isOpen', function(value) { - if (value) { - if (!scope.disabled) { - scope.position = appendToBody ? $position.offset(element) : $position.position(element); - scope.position.top = scope.position.top + element.prop('offsetHeight'); - - $timeout(function() { - if (onOpenFocus) { - scope.$broadcast('uib:datepicker.focus'); - } - $document.bind('click', documentClickBind); - }, 0, false); - } else { - scope.isOpen = false; - } - } else { - $document.unbind('click', documentClickBind); - } - }); + scope.close = function() { + scope.isOpen = false; + element[0].focus(); + }; - function cameltoDash(string) { - return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); - } + 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) { + scope.position = appendToBody ? $position.offset(element) : $position.position(element); + scope.position.top = scope.position.top + element.prop('offsetHeight'); - 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; - } - } + $timeout(function() { + if (onOpenFocus) { + scope.$broadcast('uib:datepicker.focus'); } + $document.bind('click', documentClickBind); + }, 0, false); + } else { + scope.isOpen = false; + } + } else { + $document.unbind('click', documentClickBind); + } + }); + + 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); - } + function parseDate(viewValue) { + if (angular.isNumber(viewValue)) { + // presumably timestamp to date object + viewValue = new Date(viewValue); + } - if (!viewValue) { - return null; - } + if (!viewValue) { + return null; + } - if (angular.isDate(viewValue) && !isNaN(viewValue)) { - return viewValue; - } + if (angular.isDate(viewValue) && !isNaN(viewValue)) { + return viewValue; + } - if (angular.isString(viewValue)) { - var date = parseDateString(viewValue); - if (!isNaN(date)) { - return dateParser.toTimezone(date, ngModelOptions.timezone); - } - } + if (angular.isString(viewValue)) { + var date = parseDateString(viewValue); + if (!isNaN(date)) { + return dateParser.toTimezone(date, ngModelOptions.timezone); + } + } - return ngModel.$options && ngModel.$options.allowInvalid ? viewValue : undefined; - } + return ngModel.$options && ngModel.$options.allowInvalid ? viewValue : undefined; + } - function validator(modelValue, viewValue) { - var value = modelValue || viewValue; + function validator(modelValue, viewValue) { + var value = modelValue || viewValue; - if (!attrs.ngRequired && !value) { - return true; - } + if (!attrs.ngRequired && !value) { + return true; + } - if (angular.isNumber(value)) { - value = new Date(value); - } + if (angular.isNumber(value)) { + value = new Date(value); + } - if (!value) { - return true; - } + if (!value) { + return true; + } - if (angular.isDate(value) && !isNaN(value)) { - return true; - } + if (angular.isDate(value) && !isNaN(value)) { + return true; + } - if (angular.isString(value)) { - return !isNaN(parseDateString(viewValue)); - } + if (angular.isString(value)) { + return !isNaN(parseDateString(viewValue)); + } - return false; - } + return false; + } - function documentClickBind(event) { - if (!scope.isOpen && scope.disabled) { - return; - } + 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; - }); - } - } + 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 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; + }); + } + } +}]) .directive('uibDatepickerPopup', function() { return { @@ -1002,4 +1002,4 @@ function clearWatchListeners(watchListeners) { for (var i = 0; i < watchListeners.length; i++) { watchListeners[i](); } -} \ No newline at end of file +} From 973739bf28d8febe56474a8dc4af7a94623056fe Mon Sep 17 00:00:00 2001 From: Daniel Gornstein Date: Thu, 14 Jan 2016 19:41:58 -0800 Subject: [PATCH 3/3] chore(datepicker): Unregister parent watchers on $destroy Closes #5242 --- src/datepicker/datepicker.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index 6fe81afe95..c60484f851 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -251,7 +251,10 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst }; $scope.$on("$destroy", function() { - clearWatchListeners(watchListeners); + //Clear all watch listeners on destroy + while (watchListeners.length) { + watchListeners.shift()(); + } }); }]) @@ -776,7 +779,9 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi $document.unbind('click', documentClickBind); //Clear all watch listeners on destroy - clearWatchListeners(watchListeners); + while (watchListeners.length) { + watchListeners.shift()(); + } }); }; @@ -997,9 +1002,3 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi } }; }); - -function clearWatchListeners(watchListeners) { - for (var i = 0; i < watchListeners.length; i++) { - watchListeners[i](); - } -}