From 90a8aa79c819e79f09e049ad7622c5c82e3da9b0 Mon Sep 17 00:00:00 2001 From: Stefan Valentin Date: Mon, 1 Jul 2013 14:26:07 -0400 Subject: [PATCH 0001/1761] fix(typeahead): fixed waitTime functionality Closes #573 --- src/typeahead/test/typeahead.spec.js | 35 ++++++++++++++++++++++++++++ src/typeahead/typeahead.js | 11 +++++---- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index f42bad15c0..8f1bd3fdf7 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -183,6 +183,41 @@ describe('typeahead tests', function () { $timeout.flush(); expect(element).toBeOpenWithActive(1, 0); })); + + it('should cancel old timeouts when something is typed within waitTime', inject(function ($timeout) { + var values = []; + $scope.loadMatches = function(viewValue) { + values.push(viewValue); + return $scope.source; + }; + var element = prepareInputEl("
"); + changeInputValueTo(element, 'first'); + changeInputValueTo(element, 'second'); + + $timeout.flush(); + + expect(values).not.toContain('first'); + })); + + it('should allow timeouts when something is typed after waitTime has passed', inject(function ($timeout) { + var values = []; + + $scope.loadMatches = function(viewValue) { + values.push(viewValue); + return $scope.source; + }; + var element = prepareInputEl("
"); + + changeInputValueTo(element, 'first'); + $timeout.flush(); + + expect(values).toContain('first'); + + changeInputValueTo(element, 'second'); + $timeout.flush(); + + expect(values).toContain('second'); + })); }); describe('selecting a match', function () { diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 0bf5d004fc..b4541def17 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -133,19 +133,20 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) //we need to propagate user's query so we can higlight matches scope.query = undefined; + //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later + var timeoutPromise; + //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue modelCtrl.$parsers.push(function (inputValue) { - var timeoutId; - resetMatches(); if (inputValue && inputValue.length >= minSearch) { if (waitTime > 0) { - if (timeoutId) { - $timeout.cancel(timeoutId);//cancel previous timeout + if (timeoutPromise) { + $timeout.cancel(timeoutPromise);//cancel previous timeout } - timeoutId = $timeout(function () { + timeoutPromise = $timeout(function () { getMatchesAsync(inputValue); }, waitTime); } else { From 4da17a44181c2277901b5595d00a1a80e1383ecb Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Fri, 5 Jul 2013 20:31:56 +0200 Subject: [PATCH 0002/1761] docs(typeahead): demo how to limit number of matches --- src/typeahead/docs/demo.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typeahead/docs/demo.html b/src/typeahead/docs/demo.html index 1e698af354..5cc8f5ec83 100644 --- a/src/typeahead/docs/demo.html +++ b/src/typeahead/docs/demo.html @@ -1,4 +1,4 @@
Model: {{selected| json}}
- +
\ No newline at end of file From 624fd5f5659cb75fee5f51c23c15753f413e06c8 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sat, 6 Jul 2013 17:29:26 +0200 Subject: [PATCH 0003/1761] fix(typeahead): correctly close popup on match selection --- src/typeahead/test/typeahead.spec.js | 3 +++ src/typeahead/typeahead.js | 1 + 2 files changed, 4 insertions(+) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index 8f1bd3fdf7..89c893dea8 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -232,6 +232,7 @@ describe('typeahead tests', function () { expect($scope.result).toEqual('bar'); expect(inputEl.val()).toEqual('bar'); + expect(element).toBeClosed(); }); it('should select a match on tab', function () { @@ -244,6 +245,7 @@ describe('typeahead tests', function () { expect($scope.result).toEqual('bar'); expect(inputEl.val()).toEqual('bar'); + expect(element).toBeClosed(); }); it('should select match on click', function () { @@ -259,6 +261,7 @@ describe('typeahead tests', function () { expect($scope.result).toEqual('baz'); expect(inputEl.val()).toEqual('baz'); + expect(element).toBeClosed(); }); it('should invoke select callback on select', function () { diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index b4541def17..8a128fce14 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -195,6 +195,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) }); //return focus to the input element if a mach was selected via a mouse click event + resetMatches(); element[0].focus(); }; From e2238174d282c8e71684bcbd8d8f5a88763b9eab Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sat, 6 Jul 2013 16:56:51 +0200 Subject: [PATCH 0004/1761] feat(typeahead): support custom templates for matched items Closes #182 --- src/typeahead/test/typeahead-popup.spec.js | 3 ++- src/typeahead/test/typeahead.spec.js | 15 +++++++++++- src/typeahead/typeahead.js | 24 ++++++++++++++++++- template/typeahead/typeahead-match.html | 1 + .../{typeahead.html => typeahead-popup.html} | 4 ++-- 5 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 template/typeahead/typeahead-match.html rename template/typeahead/{typeahead.html => typeahead-popup.html} (54%) diff --git a/src/typeahead/test/typeahead-popup.spec.js b/src/typeahead/test/typeahead-popup.spec.js index dd42423437..9ab1b18373 100644 --- a/src/typeahead/test/typeahead-popup.spec.js +++ b/src/typeahead/test/typeahead-popup.spec.js @@ -3,7 +3,8 @@ describe('typeaheadPopup - result rendering', function () { var scope, $rootScope, $compile; beforeEach(module('ui.bootstrap.typeahead')); - beforeEach(module('template/typeahead/typeahead.html')); + beforeEach(module('template/typeahead/typeahead-popup.html')); + beforeEach(module('template/typeahead/typeahead-match.html')); beforeEach(inject(function (_$rootScope_, _$compile_) { $rootScope = _$rootScope_; scope = $rootScope.$new(); diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index 89c893dea8..e76e511c92 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -4,7 +4,8 @@ describe('typeahead tests', function () { var changeInputValueTo; beforeEach(module('ui.bootstrap.typeahead')); - beforeEach(module('template/typeahead/typeahead.html')); + beforeEach(module('template/typeahead/typeahead-popup.html')); + beforeEach(module('template/typeahead/typeahead-match.html')); beforeEach(module(function($compileProvider) { $compileProvider.directive('formatter', function () { return { @@ -218,6 +219,18 @@ describe('typeahead tests', function () { expect(values).toContain('second'); })); + + it('should support custom templates for matched items', inject(function ($templateCache) { + + $templateCache.put('custom.html', '

{{ match.label }}

'); + + var element = prepareInputEl("
"); + var inputEl = findInput(element); + + changeInputValueTo(element, 'Al'); + + expect(findMatches(element).eq(0).find('p').text()).toEqual('Alaska'); + })); }); describe('selecting a match', function () { diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 8a128fce14..2e864f01e0 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -74,6 +74,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) query: 'query', position: 'position' }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } //create a child scope for the typeahead directive so we are not polluting original scope //with typeahead-specific data (matches, query etc.) @@ -252,9 +256,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) select:'&' }, replace:true, - templateUrl:'template/typeahead/typeahead.html', + templateUrl:'template/typeahead/typeahead-popup.html', link:function (scope, element, attrs) { + scope.templateUrl = attrs.templateUrl; + scope.isOpen = function () { return scope.matches.length > 0; }; @@ -274,6 +280,22 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) }; }) + .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { + return { + restrict:'E', + scope:{ + match:'=', + query:'=' + }, + link:function (scope, element, attrs) { + var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; + $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ + element.replaceWith($compile(tplContent.trim())(scope)); + }); + } + }; + }]) + .filter('typeaheadHighlight', function() { function escapeRegexp(queryToEscape) { diff --git a/template/typeahead/typeahead-match.html b/template/typeahead/typeahead-match.html new file mode 100644 index 0000000000..5a660df0f1 --- /dev/null +++ b/template/typeahead/typeahead-match.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/template/typeahead/typeahead.html b/template/typeahead/typeahead-popup.html similarity index 54% rename from template/typeahead/typeahead.html rename to template/typeahead/typeahead-popup.html index 2a7b9d7725..f0efda19fa 100644 --- a/template/typeahead/typeahead.html +++ b/template/typeahead/typeahead-popup.html @@ -1,5 +1,5 @@ \ No newline at end of file From f5b37f68deb5b7cc56a857e2f448f1f5220c64dc Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Mon, 8 Jul 2013 19:08:17 +0200 Subject: [PATCH 0005/1761] refactor(dialog): remove dead code --- src/dialog/dialog.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/dialog/dialog.js b/src/dialog/dialog.js index 7e20076e86..0e0413963c 100644 --- a/src/dialog/dialog.js +++ b/src/dialog/dialog.js @@ -92,10 +92,6 @@ dialogModule.provider("$dialog", function(){ e.preventDefault(); self.$scope.$apply(); }; - - this.handleLocationChange = function() { - self.close(); - }; } // The `isOpen()` method returns wether the dialog is currently visible. From 224bc2f599cbc2b2b8bad442d4bb2dbe6f683ecb Mon Sep 17 00:00:00 2001 From: Andy Joslin Date: Mon, 8 Jul 2013 14:35:23 -0400 Subject: [PATCH 0006/1761] fix(tabs): fix tab content compiling wrong (Closes #599, #631, #574) * Before, tab content was being transcluded before the tab content area was ready. This forced us to disconnect the tab contents from the DOM temporarily, then reocnnect them later. This caused a lot of problems. * Now, neither the tab content or header are transcluded until both the heading and content areas are loaded. This is simpler and fixes many weird compilation bugs. --- src/tabs/tabs.js | 78 ++++++++++++++++----------------------- src/tabs/test/tabsSpec.js | 39 ++++++++++++++++++++ 2 files changed, 70 insertions(+), 47 deletions(-) diff --git a/src/tabs/tabs.js b/src/tabs/tabs.js index 9c67c179f4..202fecafd8 100644 --- a/src/tabs/tabs.js +++ b/src/tabs/tabs.js @@ -15,20 +15,16 @@ angular.module('ui.bootstrap.tabs', []) }; }) -.controller('TabsetController', ['$scope', '$element', +.controller('TabsetController', ['$scope', '$element', function TabsetCtrl($scope, $element) { - //Expose the outer scope for tab content compiling, so it can compile - //on outer scope like it should - this.$outerScope = $scope.$parent; - var ctrl = this, tabs = ctrl.tabs = $scope.tabs = []; ctrl.select = function(tab) { angular.forEach(tabs, function(tab) { tab.active = false; - }); + }); tab.active = true; }; @@ -39,7 +35,7 @@ function TabsetCtrl($scope, $element) { } }; - ctrl.removeTab = function removeTab(tab) { + ctrl.removeTab = function removeTab(tab) { var index = tabs.indexOf(tab); //Select a new tab if the tab to be removed is selected if (tab.active && tabs.length > 1) { @@ -101,7 +97,7 @@ function TabsetCtrl($scope, $element) { * @param {boolean=} active A binding, telling whether or not this tab is selected. * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. * - * @description + * @description * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. * * @example @@ -235,37 +231,11 @@ function($parse, $http, $templateCache, $compile) { //value won't overwrite what is initially set by the tabset if (scope.active) { setActive(scope.$parent, true); - } + } - //Transclude the collection of sibling elements. Use forEach to find - //the heading if it exists. We don't use a directive for tab-heading - //because it is problematic. Discussion @ http://git.io/MSNPwQ - transclude(scope.$parent, function(clone) { - //Look at every element in the clone collection. If it's tab-heading, - //mark it as that. If it's not tab-heading, mark it as tab contents - var contents = [], heading; - angular.forEach(clone, function(el) { - //See if it's a tab-heading attr or element directive - //First make sure it's a normal element, one that has a tagName - if (el.tagName && - (el.hasAttribute("tab-heading") || - el.hasAttribute("data-tab-heading") || - el.tagName.toLowerCase() == "tab-heading" || - el.tagName.toLowerCase() == "data-tab-heading" - )) { - heading = el; - } else { - contents.push(el); - } - }); - //Share what we found on the scope, so our tabHeadingTransclude and - //tabContentTransclude directives can find out what the heading and - //contents are. - if (heading) { - scope.headingElement = angular.element(heading); - } - scope.contentElement = angular.element(contents); - }); + //We need to transclude later, once the content container is ready. + //when this link happens, we're inside a tab heading. + scope.$transcludeFn = transclude; }; } }; @@ -274,7 +244,7 @@ function($parse, $http, $templateCache, $compile) { .directive('tabHeadingTransclude', [function() { return { restrict: 'A', - require: '^tab', + require: '^tab', link: function(scope, elm, attrs, tabCtrl) { scope.$watch('headingElement', function updateHeadingElement(heading) { if (heading) { @@ -290,17 +260,31 @@ function($parse, $http, $templateCache, $compile) { return { restrict: 'A', require: '^tabset', - link: function(scope, elm, attrs, tabsetCtrl) { - var outerScope = tabsetCtrl.$outerScope; - scope.$watch($parse(attrs.tabContentTransclude), function(tab) { - elm.html(''); - if (tab) { - elm.append(tab.contentElement); - $compile(tab.contentElement)(outerScope); - } + link: function(scope, elm, attrs) { + var tab = scope.$eval(attrs.tabContentTransclude); + + //Now our tab is ready to be transcluded: both the tab heading area + //and the tab content area are loaded. Transclude 'em both. + tab.$transcludeFn(tab.$parent, function(contents) { + angular.forEach(contents, function(node) { + if (isTabHeading(node)) { + //Let tabHeadingTransclude know. + tab.headingElement = node; + } else { + elm.append(node); + } + }); }); } }; + function isTabHeading(node) { + return node.tagName && ( + node.hasAttribute('tab-heading') || + node.hasAttribute('data-tab-heading') || + node.tagName.toLowerCase() === 'tab-heading' || + node.tagName.toLowerCase() === 'data-tab-heading' + ); + } }]) ; diff --git a/src/tabs/test/tabsSpec.js b/src/tabs/test/tabsSpec.js index 7f47d7fe69..2068e1117f 100644 --- a/src/tabs/test/tabsSpec.js +++ b/src/tabs/test/tabsSpec.js @@ -495,4 +495,43 @@ describe('tabs', function() { expect(tabChild.inheritedData('$tabsetController')).toBeTruthy(); }); }); + + //https://github.com/angular-ui/bootstrap/issues/631 + describe('ng-options in content', function() { + var elm; + it('should render correct amount of options', inject(function($compile, $rootScope) { + var scope = $rootScope.$new(); + elm = $compile('' + ); + $compile(elmBody)(scope); + scope.$digest(); + + elm = elmBody.find('input'); + + elm.trigger( 'mouseenter' ); + elm.trigger( 'mouseleave' ); + expect(scope.clicked).toBeFalsy(); + + elm.click(); + expect(scope.clicked).toBeTruthy(); + })); }); diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index 5504e581f3..a8e57669a2 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -276,8 +276,8 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) }); attrs.$observe( prefix+'Trigger', function ( val ) { - element.unbind( triggers.show ); - element.unbind( triggers.hide ); + element.unbind( triggers.show, showTooltipBind ); + element.unbind( triggers.hide, hideTooltipBind ); triggers = setTriggers( val ); From 93a82af0e4573ef0cac1caa431b971d03197b91c Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Tue, 23 Jul 2013 19:36:15 +0200 Subject: [PATCH 0013/1761] fix(popover): correctly position popovers appended to body Closes #682 --- src/position/test/test.html | 6 +++--- src/tooltip/tooltip.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/position/test/test.html b/src/position/test/test.html index 0ea75acc6a..b21948d697 100644 --- a/src/position/test/test.html +++ b/src/position/test/test.html @@ -67,14 +67,14 @@

Within relative-positioned DIV

Within absolute-positioned DIV

-
-
Content
+
+
Content - absolute

Within overflowing absolute-positioned DIV

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur non velit nulla. Suspendisse sit amet tempus diam. Sed at ultricies neque. Suspendisse id felis a sem placerat ornare. Donec auctor, purus at molestie tempor, arcu enim molestie lacus, ac imperdiet massa urna eu massa. Praesent velit tellus, scelerisque a fermentum ut, ornare in diam. Phasellus egestas molestie feugiat. Vivamus sit amet viverra metus. -
Content
+
Content absolute overflow

diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index a8e57669a2..96403d7f04 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -181,7 +181,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) } // Get the position of the directive element. - position = options.appendToBody ? $position.offset( element ) : $position.position( element ); + position = appendToBody ? $position.offset( element ) : $position.position( element ); // Get the height and width of the tooltip so we can center it. ttWidth = tooltip.prop( 'offsetWidth' ); From c74684fe9bfca46ab3f5fc1d986ec9d0eb29031b Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Tue, 23 Jul 2013 21:38:59 +0200 Subject: [PATCH 0014/1761] docs(accordion): demonstrate usage of accordion-heading Closes #690 --- src/accordion/docs/demo.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/accordion/docs/demo.html b/src/accordion/docs/demo.html index 3f423d9179..f8627f51ef 100644 --- a/src/accordion/docs/demo.html +++ b/src/accordion/docs/demo.html @@ -16,5 +16,11 @@
{{item}}
+ + + I can have markup, too! + + This is just some content to illustrate fancy headings. +
\ No newline at end of file From b21f70aa59d09c5732fdc59bf26223b13762ee99 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Tue, 23 Jul 2013 21:48:11 +0200 Subject: [PATCH 0015/1761] chore(build): add demo as a valid commit msg qualifier --- misc/validate-commit-msg.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/misc/validate-commit-msg.js b/misc/validate-commit-msg.js index 8866228c26..1a8df7fa32 100644 --- a/misc/validate-commit-msg.js +++ b/misc/validate-commit-msg.js @@ -16,14 +16,15 @@ var MAX_LENGTH = 70; var PATTERN = /^(?:fixup!\s*)?(\w*)(\((\w+)\))?\: (.*)$/; var IGNORED = /^WIP\:/; var TYPES = { + chore: true, + demo: true, + docs: true, feat: true, fix: true, - docs: true, - style: true, refactor: true, - test: true, - chore: true, - revert: true + revert: true, + style: true, + test: true }; From 8278f53b881e4b8389fd5e9ee615d4dad5387d6c Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Thu, 25 Jul 2013 20:58:54 +0200 Subject: [PATCH 0016/1761] test(typeahead): verify initial rendeirng with complex labels Closes #706 --- src/typeahead/test/typeahead.spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index e76e511c92..b586e3e83c 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -102,6 +102,16 @@ describe('typeahead tests', function () { expect(inputEl.val()).toEqual('Alaska'); }); + it('should default to bound model for initial rendering if there is not enough info to render label', function () { + + $scope.result = $scope.states[0].code; + + var element = prepareInputEl("
"); + var inputEl = findInput(element); + + expect(inputEl.val()).toEqual('AL'); + }); + it('should not get open on model change', function () { var element = prepareInputEl("
"); $scope.$apply(function () { From 8d904867f1ac620968f07be7556b902203910759 Mon Sep 17 00:00:00 2001 From: Nick Serebrennikov Date: Thu, 25 Jul 2013 01:07:29 -0700 Subject: [PATCH 0017/1761] docs(README): fix typo --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ccd9ae133a..02fe1524f4 100644 --- a/README.md +++ b/README.md @@ -67,15 +67,15 @@ This will start Karma server and will continously watch files in the project, ex ### Release * Bump up version number in `package.json` -* Commit the version change with the following message: `chore(release): [versio number]` +* Commit the version change with the following message: `chore(release): [version number]` * tag * push changes and a tag (`git push --tags`) * switch to the `gh-pages` branch: `git checkout gh-pages` * copy content of the dist folder to the main folder -* Commit the version change with the following message: `chore(release): [versio number]` +* Commit the version change with the following message: `chore(release): [version number]` * push changes * switch back to the `main branch` and modify `package.json` to bump up version for the next iteration -* commit (`chore(release): starting [versio number]`) and push +* commit (`chore(release): starting [version number]`) and push * publish Bower and NuGet packages Well done! (If you don't like repeating yourself open a PR with a grunt task taking care of the above!) From 682ae66e3685c16305880304629ae05fd693e127 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Tue, 23 Jul 2013 23:12:30 +0300 Subject: [PATCH 0018/1761] refactor(pagination): keep it DRY between `pagination` & `pager` --- src/pagination/pagination.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/pagination/pagination.js b/src/pagination/pagination.js index b3bc121f92..1c4165d660 100644 --- a/src/pagination/pagination.js +++ b/src/pagination/pagination.js @@ -15,6 +15,15 @@ angular.module('ui.bootstrap.pagination', []) return this.currentPage === page; }; + this.reset = function() { + $scope.pages = []; + this.currentPage = parseInt($scope.currentPage, 10); + + if ( this.currentPage > $scope.numPages ) { + $scope.selectPage($scope.numPages); + } + }; + var self = this; $scope.selectPage = function(page) { if ( ! self.isActive(page) && page > 0 && page <= $scope.numPages) { @@ -68,8 +77,7 @@ angular.module('ui.bootstrap.pagination', []) } scope.$watch('numPages + currentPage + maxSize', function() { - scope.pages = []; - paginationCtrl.currentPage = parseInt(scope.currentPage, 10); + paginationCtrl.reset(); // Default page limits var startPage = 1, endPage = scope.numPages; @@ -132,10 +140,6 @@ angular.module('ui.bootstrap.pagination', []) var lastPage = makePage(scope.numPages, lastText, false, paginationCtrl.noNext()); scope.pages.push(lastPage); } - - if ( paginationCtrl.currentPage > scope.numPages ) { - scope.selectPage(scope.numPages); - } }); } }; @@ -177,8 +181,7 @@ angular.module('ui.bootstrap.pagination', []) } scope.$watch('numPages + currentPage', function() { - scope.pages = []; - paginationCtrl.currentPage = parseInt(scope.currentPage, 10); + paginationCtrl.reset(); // Add previous & next links var previousPage = makePage(paginationCtrl.currentPage - 1, previousText, paginationCtrl.noPrevious(), true, false); @@ -186,10 +189,6 @@ angular.module('ui.bootstrap.pagination', []) var nextPage = makePage(paginationCtrl.currentPage + 1, nextText, paginationCtrl.noNext(), false, true); scope.pages.push(nextPage); - - if ( paginationCtrl.currentPage > scope.numPages ) { - scope.selectPage(scope.numPages); - } }); } }; From fe47c9bb61c4e20d5f10ff46f2efb1a0663dea34 Mon Sep 17 00:00:00 2001 From: Vicki Date: Tue, 23 Jul 2013 13:44:45 -0700 Subject: [PATCH 0019/1761] feat(tabs): added onDeselect callback, used similarly as onSelect --- src/tabs/tabs.js | 8 ++++++-- src/tabs/test/tabsSpec.js | 20 +++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/tabs/tabs.js b/src/tabs/tabs.js index d5b9ed12ae..b0444ba4d7 100644 --- a/src/tabs/tabs.js +++ b/src/tabs/tabs.js @@ -177,8 +177,9 @@ function($parse, $http, $templateCache, $compile) { transclude: true, scope: { heading: '@', - onSelect: '&select' //This callback is called in contentHeadingTransclude + onSelect: '&select', //This callback is called in contentHeadingTransclude //once it inserts the tab's content into the dom + onDeselect: '&deselect' }, controller: function() { //Empty controller so other directives can require being 'under' a tab @@ -202,6 +203,9 @@ function($parse, $http, $templateCache, $compile) { tabsetCtrl.select(scope); scope.onSelect(); } + else { + scope.onDeselect(); + } }); scope.disabled = false; @@ -227,7 +231,7 @@ function($parse, $http, $templateCache, $compile) { //We need to transclude later, once the content container is ready. - //when this link happens, we're inside a tab heading. + //when this link happens, we're inside a tab heading. scope.$transcludeFn = transclude; }; } diff --git a/src/tabs/test/tabsSpec.js b/src/tabs/test/tabsSpec.js index d0f0d9d842..c5310e05f7 100644 --- a/src/tabs/test/tabsSpec.js +++ b/src/tabs/test/tabsSpec.js @@ -24,7 +24,7 @@ describe('tabs', function() { } } - + describe('basics', function() { beforeEach(inject(function($compile, $rootScope) { @@ -33,14 +33,16 @@ describe('tabs', function() { scope.second = '2'; scope.actives = {}; scope.selectFirst = jasmine.createSpy(); - scope.selectSecond = jasmine.createSpy(); + scope.selectSecond = jasmine.createSpy(); + scope.deselectFirst = jasmine.createSpy(); + scope.deselectSecond = jasmine.createSpy(); elm = $compile([ '
', ' ', - ' ', + ' ', ' first content is {{first}}', ' ', - ' ', + ' ', ' Second Tab {{second}}', ' second content is {{second}}', ' ', @@ -90,6 +92,14 @@ describe('tabs', function() { expect(scope.selectFirst).toHaveBeenCalled(); }); + it('should call deselect callback on deselect', function() { + titles().eq(1).find('a').click(); + titles().eq(0).find('a').click(); + expect(scope.deselectSecond).toHaveBeenCalled(); + titles().eq(1).find('a').click(); + expect(scope.deselectFirst).toHaveBeenCalled(); + }); + }); describe('ng-repeat', function() { @@ -208,7 +218,7 @@ describe('tabs', function() { expect(heading().eq(2).text()).toBe('2'); expect(heading().eq(3).text()).toBe('3'); }); - + }); //Tests that http://git.io/lG6I9Q is fixed From 5ffae83d664af738c17b3f89e64e048a9d119590 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Fri, 26 Jul 2013 19:35:39 +0200 Subject: [PATCH 0020/1761] feat(typeahead): expose index to custom templates Closes #699 --- src/typeahead/test/typeahead.spec.js | 4 ++-- src/typeahead/typeahead.js | 1 + template/typeahead/typeahead-popup.html | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index b586e3e83c..f942ed909e 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -232,14 +232,14 @@ describe('typeahead tests', function () { it('should support custom templates for matched items', inject(function ($templateCache) { - $templateCache.put('custom.html', '

{{ match.label }}

'); + $templateCache.put('custom.html', '

{{ index }} {{ match.label }}

'); var element = prepareInputEl("
"); var inputEl = findInput(element); changeInputValueTo(element, 'Al'); - expect(findMatches(element).eq(0).find('p').text()).toEqual('Alaska'); + expect(findMatches(element).eq(0).find('p').text()).toEqual('0 Alaska'); })); }); diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 2e864f01e0..4274b0a00c 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -284,6 +284,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) return { restrict:'E', scope:{ + index:'=', match:'=', query:'=' }, diff --git a/template/typeahead/typeahead-popup.html b/template/typeahead/typeahead-popup.html index f0efda19fa..69707cefe5 100644 --- a/template/typeahead/typeahead-popup.html +++ b/template/typeahead/typeahead-popup.html @@ -1,5 +1,5 @@ \ No newline at end of file From d9e5bc9e8d043d79860b4f78061d3e6b87756b26 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sat, 27 Jul 2013 10:01:21 +0200 Subject: [PATCH 0021/1761] demo(all): sort directives alphabetically --- Gruntfile.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index c97aab42e4..63a92facf7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -293,9 +293,16 @@ module.exports = function(grunt) { var modules = grunt.config('modules'); grunt.config('srcModules', _.pluck(modules, 'moduleName')); grunt.config('tplModules', _.pluck(modules, 'tplModules').filter(function(tpls) { return tpls.length > 0;} )); - grunt.config('demoModules', modules.filter(function(module) { - return module.docs.md && module.docs.js && module.docs.html; - })); + grunt.config('demoModules', modules + .filter(function(module) { + return module.docs.md && module.docs.js && module.docs.html; + }) + .sort(function(a, b) { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }) + ); var srcFiles = _.pluck(modules, 'srcFiles'); var tpljsFiles = _.pluck(modules, 'tpljsFiles'); From 220e7b60124105aca25e57c3f01a22e12fa77cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Galfas=C3=B3?= Date: Fri, 12 Jul 2013 15:10:44 -0300 Subject: [PATCH 0022/1761] feat(tabs): add the ability to set the direction of the tabs Add the ability to set the direction of the tabs, the possible values are 'right', 'left' and 'below'. If no direction is defined the tabs are rendered on the top as they do now Closes #659 --- src/tabs/tabs.js | 34 +++++++++++++++++++-- src/tabs/test/tabsSpec.js | 52 +++++++++++++++++++++++++++++++- template/tabs/tabset-titles.html | 2 ++ template/tabs/tabset.html | 6 ++-- 4 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 template/tabs/tabset-titles.html diff --git a/src/tabs/tabs.js b/src/tabs/tabs.js index b0444ba4d7..c8c4b37322 100644 --- a/src/tabs/tabs.js +++ b/src/tabs/tabs.js @@ -56,6 +56,8 @@ function TabsetCtrl($scope, $element) { * Tabset is the outer container for the tabs directive * * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. + * @param {string=} direction What direction the tabs should be rendered. Available: + * 'right', 'left', 'below'. * * @example @@ -77,12 +79,19 @@ function TabsetCtrl($scope, $element) { restrict: 'EA', transclude: true, replace: true, + require: '^tabset', scope: {}, controller: 'TabsetController', templateUrl: 'template/tabs/tabset.html', - link: function(scope, element, attrs) { - scope.vertical = angular.isDefined(attrs.vertical) ? scope.$eval(attrs.vertical) : false; - scope.type = angular.isDefined(attrs.type) ? scope.$parent.$eval(attrs.type) : 'tabs'; + compile: function(elm, attrs, transclude) { + return function(scope, element, attrs, tabsetCtrl) { + scope.vertical = angular.isDefined(attrs.vertical) ? scope.$eval(attrs.vertical) : false; + scope.type = angular.isDefined(attrs.type) ? scope.$parent.$eval(attrs.type) : 'tabs'; + scope.direction = angular.isDefined(attrs.direction) ? scope.$parent.$eval(attrs.direction) : 'top'; + scope.tabsAbove = (scope.direction != 'below'); + tabsetCtrl.$scope = scope; + tabsetCtrl.$transcludeFn = transclude; + }; } }; }) @@ -284,5 +293,24 @@ function($parse, $http, $templateCache, $compile) { } }]) +.directive('tabsetTitles', function($http) { + return { + restrict: 'A', + require: '^tabset', + templateUrl: 'template/tabs/tabset-titles.html', + replace: true, + link: function(scope, elm, attrs, tabsetCtrl) { + if (!scope.$eval(attrs.tabsetTitles)) { + elm.remove(); + } else { + //now that tabs location has been decided, transclude the tab titles in + tabsetCtrl.$transcludeFn(tabsetCtrl.$scope.$parent, function(node) { + elm.append(node); + }); + } + } + }; +}) + ; diff --git a/src/tabs/test/tabsSpec.js b/src/tabs/test/tabsSpec.js index c5310e05f7..93c0e51cd4 100644 --- a/src/tabs/test/tabsSpec.js +++ b/src/tabs/test/tabsSpec.js @@ -1,5 +1,5 @@ describe('tabs', function() { - beforeEach(module('ui.bootstrap.tabs', 'template/tabs/tabset.html', 'template/tabs/tab.html')); + beforeEach(module('ui.bootstrap.tabs', 'template/tabs/tabset.html', 'template/tabs/tab.html', 'template/tabs/tabset-titles.html')); var elm, scope; function titles() { @@ -493,6 +493,56 @@ describe('tabs', function() { }); }); + describe('direction', function() { + it('should not have `tab-left`, `tab-right` nor `tabs-below` classes if the direction is undefined', inject(function($compile, $rootScope) { + scope = $rootScope.$new(); + scope.direction = undefined; + + elm = $compile('')(scope); + scope.$apply(); + expect(elm).not.toHaveClass('tabs-left'); + expect(elm).not.toHaveClass('tabs-right'); + expect(elm).not.toHaveClass('tabs-below'); + expect(elm.find('.nav + .tab-content').length).toBe(1); + })); + + it('should only have the `tab-left` direction class if the direction is "left"', inject(function($compile, $rootScope) { + scope = $rootScope.$new(); + scope.direction = 'left'; + + elm = $compile('')(scope); + scope.$apply(); + expect(elm).toHaveClass('tabs-left'); + expect(elm).not.toHaveClass('tabs-right'); + expect(elm).not.toHaveClass('tabs-below'); + expect(elm.find('.nav + .tab-content').length).toBe(1); + })); + + it('should only have the `tab-right direction class if the direction is "right"', inject(function($compile, $rootScope) { + scope = $rootScope.$new(); + scope.direction = 'right'; + + elm = $compile('')(scope); + scope.$apply(); + expect(elm).not.toHaveClass('tabs-left'); + expect(elm).toHaveClass('tabs-right'); + expect(elm).not.toHaveClass('tabs-below'); + expect(elm.find('.nav + .tab-content').length).toBe(1); + })); + + it('should only have the `tab-below direction class if the direction is "below"', inject(function($compile, $rootScope) { + scope = $rootScope.$new(); + scope.direction = 'below'; + + elm = $compile('')(scope); + scope.$apply(); + expect(elm).not.toHaveClass('tabs-left'); + expect(elm).not.toHaveClass('tabs-right'); + expect(elm).toHaveClass('tabs-below'); + expect(elm.find('.tab-content + .nav').length).toBe(1); + })); + }); + //https://github.com/angular-ui/bootstrap/issues/524 describe('child compilation', function() { diff --git a/template/tabs/tabset-titles.html b/template/tabs/tabset-titles.html new file mode 100644 index 0000000000..560e0f743f --- /dev/null +++ b/template/tabs/tabset-titles.html @@ -0,0 +1,2 @@ + diff --git a/template/tabs/tabset.html b/template/tabs/tabset.html index ffe289f882..5e9798b2c8 100644 --- a/template/tabs/tabset.html +++ b/template/tabs/tabset.html @@ -1,7 +1,6 @@ -
- +
+
+
From a51c309ea81788443adcb31295281e91526f8392 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sat, 27 Jul 2013 19:42:46 +0200 Subject: [PATCH 0023/1761] docs(typeahead): clarify subset of select's syntax used Closes #715 Closes #506 --- src/typeahead/docs/readme.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/typeahead/docs/readme.md b/src/typeahead/docs/readme.md index e7918e3378..22fcf0ba8e 100644 --- a/src/typeahead/docs/readme.md +++ b/src/typeahead/docs/readme.md @@ -1,8 +1,12 @@ -Typeahead is a AngularJS version of [Twitter Bootstrap typeahead plugin](http://twitter.github.com/bootstrap/javascript.html#typeahead) - +Typeahead is a AngularJS version of [Twitter Bootstrap typeahead plugin](http://twitter.github.com/bootstrap/javascript.html#typeahead). This directive can be used to quickly create elegant typeheads with any form text input. -It is very well integrated into the AngularJS as: +It is very well integrated into the AngularJS as it uses subset of the +[select directive](http://docs.angularjs.org/api/ng.directive:select) syntax, which is very flexible. Supported expressions: + +* _label_ for _value_ in _sourceArray_ +* _select_ as _label_ for _value_ in _sourceArray_ + +The `sourceArray` expression can use a special `$viewValue` variable that corresponds to a value entered inside input by a user. -* it uses the same, flexible syntax as the [select directive](http://docs.angularjs.org/api/ng.directive:select) -* works with promises and it means that you can retrieve matches using the `$http` service with minimal effort \ No newline at end of file +Also this directive works with promises and it means that you can retrieve matches using the `$http` service with minimal effort. \ No newline at end of file From ba1f741dff4b6eb3c624f7020e663e562129cc79 Mon Sep 17 00:00:00 2001 From: Andy Joslin Date: Sat, 27 Jul 2013 14:23:18 -0400 Subject: [PATCH 0024/1761] fix(tabs): if tab is active at start, always select it Closes #648, #676 --- src/tabs/tabs.js | 6 +++--- src/tabs/test/tabsSpec.js | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/tabs/tabs.js b/src/tabs/tabs.js index c8c4b37322..125c7c3a89 100644 --- a/src/tabs/tabs.js +++ b/src/tabs/tabs.js @@ -30,7 +30,7 @@ function TabsetCtrl($scope, $element) { ctrl.addTab = function addTab(tab) { tabs.push(tab); - if (tabs.length == 1 || tab.active) { + if (tabs.length === 1 || tab.active) { ctrl.select(tab); } }; @@ -202,6 +202,7 @@ function($parse, $http, $templateCache, $compile) { scope.$parent.$watch(getActive, function updateActive(value) { scope.active = !!value; }); + scope.active = getActive(scope.$parent); } else { setActive = getActive = angular.noop; } @@ -211,8 +212,7 @@ function($parse, $http, $templateCache, $compile) { if (active) { tabsetCtrl.select(scope); scope.onSelect(); - } - else { + } else { scope.onDeselect(); } }); diff --git a/src/tabs/test/tabsSpec.js b/src/tabs/test/tabsSpec.js index 93c0e51cd4..e8a9822fef 100644 --- a/src/tabs/test/tabsSpec.js +++ b/src/tabs/test/tabsSpec.js @@ -107,14 +107,14 @@ describe('tabs', function() { beforeEach(inject(function($compile, $rootScope) { scope = $rootScope.$new(); - function makeTab() { + function makeTab(active) { return { - active: false, + active: !!active, select: jasmine.createSpy() }; } scope.tabs = [ - makeTab(), makeTab(), makeTab(), makeTab() + makeTab(), makeTab(), makeTab(true), makeTab() ]; elm = $compile([ '', @@ -140,7 +140,7 @@ describe('tabs', function() { if (activeTab === tab) { expect(tab.active).toBe(true); //It should only call select ONCE for each select - expect(tab.select.callCount).toBe(1); + expect(tab.select).toHaveBeenCalled(); expect(_titles.eq(i)).toHaveClass('active'); expect(contents().eq(i).text().trim()).toBe('content ' + i); expect(contents().eq(i)).toHaveClass('active'); @@ -151,9 +151,9 @@ describe('tabs', function() { }); } - it('should make tab titles with first content and first active', function() { + it('should make tab titles and set active tab active', function() { expect(titles().length).toBe(scope.tabs.length); - expectTabActive(scope.tabs[0]); + expectTabActive(scope.tabs[2]); }); it('should switch active when clicking', function() { From 25caf5fb8379a7a21d6808e66e25b7b5b357003e Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Wed, 31 Jul 2013 15:17:18 +0300 Subject: [PATCH 0025/1761] fix(datepicker): add type attribute for buttons --- template/datepicker/datepicker.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/template/datepicker/datepicker.html b/template/datepicker/datepicker.html index 891013e9e3..708f4f7577 100644 --- a/template/datepicker/datepicker.html +++ b/template/datepicker/datepicker.html @@ -1,9 +1,9 @@ - - - + + + @@ -14,7 +14,7 @@ From f45815cb114ebb044dad91caa97afde66e46cda7 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sun, 28 Jul 2013 02:34:03 +0300 Subject: [PATCH 0026/1761] fix(pagination): use interpolation for text attributes Closes #696 BREAKING CHANGE: The 'first-text', 'previous-text', 'next-text' and 'last-text' attributes are now interpolated. To migrate your code, remove quotes for constant attributes and/or interpolate scope variables. Before: and/or $scope.var1 = '<<'; After: and/or $scope.var1 = '<<'; --- src/pagination/pagination.js | 28 +++++++++++++++----------- src/pagination/test/pager.spec.js | 14 +++++++++++-- src/pagination/test/pagination.spec.js | 26 ++++++++++-------------- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/pagination/pagination.js b/src/pagination/pagination.js index 1c4165d660..f4becfcd2c 100644 --- a/src/pagination/pagination.js +++ b/src/pagination/pagination.js @@ -1,6 +1,6 @@ angular.module('ui.bootstrap.pagination', []) -.controller('PaginationController', ['$scope', function ($scope) { +.controller('PaginationController', ['$scope', '$interpolate', function ($scope, $interpolate) { this.currentPage = 1; @@ -31,6 +31,10 @@ angular.module('ui.bootstrap.pagination', []) $scope.onSelectPage({ page: page }); } }; + + this.getAttributeValue = function(attribute, defaultValue, interpolate) { + return angular.isDefined(attribute) ? (interpolate ? $interpolate(attribute)($scope.$parent) : $scope.$parent.$eval(attribute)) : defaultValue; + }; }]) .constant('paginationConfig', { @@ -43,7 +47,7 @@ angular.module('ui.bootstrap.pagination', []) rotate: true }) -.directive('pagination', ['paginationConfig', function(paginationConfig) { +.directive('pagination', ['paginationConfig', function(config) { return { restrict: 'EA', scope: { @@ -58,13 +62,13 @@ angular.module('ui.bootstrap.pagination', []) link: function(scope, element, attrs, paginationCtrl) { // Setup configuration parameters - var boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; - var directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$eval(attrs.directionLinks) : paginationConfig.directionLinks; - var firstText = angular.isDefined(attrs.firstText) ? scope.$parent.$eval(attrs.firstText) : paginationConfig.firstText; - var previousText = angular.isDefined(attrs.previousText) ? scope.$parent.$eval(attrs.previousText) : paginationConfig.previousText; - var nextText = angular.isDefined(attrs.nextText) ? scope.$parent.$eval(attrs.nextText) : paginationConfig.nextText; - var lastText = angular.isDefined(attrs.lastText) ? scope.$parent.$eval(attrs.lastText) : paginationConfig.lastText; - var rotate = angular.isDefined(attrs.rotate) ? scope.$eval(attrs.rotate) : paginationConfig.rotate; + var boundaryLinks = paginationCtrl.getAttributeValue(attrs.boundaryLinks, config.boundaryLinks ), + directionLinks = paginationCtrl.getAttributeValue(attrs.directionLinks, config.directionLinks ), + firstText = paginationCtrl.getAttributeValue(attrs.firstText, config.firstText, true), + previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true), + nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true), + lastText = paginationCtrl.getAttributeValue(attrs.lastText, config.lastText, true), + rotate = paginationCtrl.getAttributeValue(attrs.rotate, config.rotate); // Create page object used in template function makePage(number, text, isActive, isDisabled) { @@ -165,9 +169,9 @@ angular.module('ui.bootstrap.pagination', []) link: function(scope, element, attrs, paginationCtrl) { // Setup configuration parameters - var previousText = angular.isDefined(attrs.previousText) ? scope.$parent.$eval(attrs.previousText) : config.previousText; - var nextText = angular.isDefined(attrs.nextText) ? scope.$parent.$eval(attrs.nextText) : config.nextText; - var align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : config.align; + var previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true), + nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true), + align = paginationCtrl.getAttributeValue(attrs.align, config.align); // Create page object used in template function makePage(number, text, isDisabled, isPrevious, isNext) { diff --git a/src/pagination/test/pager.spec.js b/src/pagination/test/pager.spec.js index de863a507d..91874d6e8c 100644 --- a/src/pagination/test/pager.spec.js +++ b/src/pagination/test/pager.spec.js @@ -153,7 +153,7 @@ describe('setting pagerConfig', function() { }); -describe('pagination bypass configuration from attributes', function () { +describe('pager bypass configuration from attributes', function () { var $rootScope, element; beforeEach(module('ui.bootstrap.pagination')); beforeEach(module('template/pagination/pager.html')); @@ -162,7 +162,7 @@ describe('pagination bypass configuration from attributes', function () { $rootScope = _$rootScope_; $rootScope.numPages = 5; $rootScope.currentPage = 3; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); @@ -180,4 +180,14 @@ describe('pagination bypass configuration from attributes', function () { expect(element.find('li').eq(-1).hasClass('next')).toBe(false); }); + it('changes "previous" & "next" text from interpolated attributes', function() { + $rootScope.previousText = '<<'; + $rootScope.nextText = '>>'; + element = $compile('')($rootScope); + $rootScope.$digest(); + + expect(element.find('li').eq(0).text()).toBe('<<'); + expect(element.find('li').eq(-1).text()).toBe('>>'); + }); + }); \ No newline at end of file diff --git a/src/pagination/test/pagination.spec.js b/src/pagination/test/pagination.spec.js index 8ef067f648..764cf2f5ac 100644 --- a/src/pagination/test/pagination.spec.js +++ b/src/pagination/test/pagination.spec.js @@ -222,7 +222,8 @@ describe('pagination directive with max size option & no rotate', function () { $rootScope.numPages = 12; $rootScope.currentPage = 7; $rootScope.maxSize = 5; - element = $compile('')($rootScope); + $rootScope.rotate = false; + element = $compile('')($rootScope); $rootScope.$digest(); })); @@ -296,7 +297,6 @@ describe('pagination directive with added first & last links', function () { $rootScope.$digest(); })); - it('contains one ul and num-pages + 4 li elements', function() { expect(element.find('ul').length).toBe(1); expect(element.find('li').length).toBe(9); @@ -331,7 +331,6 @@ describe('pagination directive with added first & last links', function () { expect(element.find('li').eq(-1).hasClass('disabled')).toBe(true); }); - it('changes currentPage if the "first" link is clicked', function() { var first = element.find('li').eq(0).find('a').eq(0); first.click(); @@ -365,7 +364,7 @@ describe('pagination directive with added first & last links', function () { }); it('changes "first" & "last" text from attributes', function() { - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); expect(element.find('li').eq(0).text()).toBe('<<<'); @@ -373,27 +372,27 @@ describe('pagination directive with added first & last links', function () { }); it('changes "previous" & "next" text from attributes', function() { - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); expect(element.find('li').eq(1).text()).toBe('<<'); expect(element.find('li').eq(-2).text()).toBe('>>'); }); - it('changes "first" & "last" text from attribute variables', function() { + it('changes "first" & "last" text from interpolated attributes', function() { $rootScope.myfirstText = '<<<'; $rootScope.mylastText = '>>>'; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); expect(element.find('li').eq(0).text()).toBe('<<<'); expect(element.find('li').eq(-1).text()).toBe('>>>'); }); - it('changes "previous" & "next" text from attribute variables', function() { + it('changes "previous" & "next" text from interpolated attributes', function() { $rootScope.previousText = '<<'; $rootScope.nextText = '>>'; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); expect(element.find('li').eq(1).text()).toBe('<<'); @@ -461,7 +460,6 @@ describe('pagination directive with just number links', function () { expect($rootScope.currentPage).toBe(2); }); - it('executes the onSelectPage expression when the current page changes', function() { $rootScope.selectPageHandler = jasmine.createSpy('selectPageHandler'); element = $compile('')($rootScope); @@ -541,11 +539,11 @@ describe('pagination directive with first, last & number links', function () { $rootScope = _$rootScope_; $rootScope.numPages = 5; $rootScope.currentPage = 3; - element = $compile('')($rootScope); + $rootScope.directions = false; + element = $compile('')($rootScope); $rootScope.$digest(); })); - it('contains one ul and num-pages + 2 li elements', function() { expect(element.find('ul').length).toBe(1); expect(element.find('li').length).toBe(7); @@ -555,7 +553,6 @@ describe('pagination directive with first, last & number links', function () { expect(element.find('li').eq(-1).text()).toBe('Last'); }); - it('disables the "first" & activates "1" link if current-page is 1', function() { $rootScope.currentPage = 1; $rootScope.$digest(); @@ -572,7 +569,6 @@ describe('pagination directive with first, last & number links', function () { expect(element.find('li').eq(-1).hasClass('disabled')).toBe(true); }); - it('changes currentPage if the "first" link is clicked', function() { var first = element.find('li').eq(0).find('a').eq(0); first.click(); @@ -598,7 +594,7 @@ describe('pagination bypass configuration from attributes', function () { $rootScope = _$rootScope_; $rootScope.numPages = 5; $rootScope.currentPage = 3; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); From 58e8ef4f87649910fc24d36f0db0fe80aa122d7e Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sat, 3 Aug 2013 12:59:28 +0200 Subject: [PATCH 0027/1761] fix(tooltip): triggers should be local to tooltip instances Closes #692 --- src/tooltip/test/tooltip.spec.js | 26 ++++++++++++++++++++++++++ src/tooltip/tooltip.js | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/tooltip/test/tooltip.spec.js b/src/tooltip/test/tooltip.spec.js index b5c9c35b17..ae4179caac 100644 --- a/src/tooltip/test/tooltip.spec.js +++ b/src/tooltip/test/tooltip.spec.js @@ -209,6 +209,32 @@ describe('tooltip', function() { elm.trigger('fakeTriggerAttr'); expect( elmScope.tt_isOpen ).toBeFalsy(); })); + + it('should not share triggers among different element instances - issue 692', inject( function ($compile) { + + scope.test = true; + elmBody = angular.element( + '
' + + '' + + '' + + '
' + ); + + $compile(elmBody)(scope); + scope.$apply(); + var elm1 = elmBody.find('input').eq(0); + var elm2 = elmBody.find('input').eq(1); + var elmScope1 = elm1.scope(); + var elmScope2 = elm2.scope(); + + scope.$apply('test = false'); + + elm2.trigger('mouseenter'); + expect( elmScope2.tt_isOpen ).toBeFalsy(); + + elm2.click(); + expect( elmScope2.tt_isOpen ).toBeTruthy(); + })); }); describe( 'with an append-to-body attribute', function() { diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index 96403d7f04..07039dce05 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -99,7 +99,6 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) } var directiveName = snake_case( type ); - var triggers = setTriggers( undefined ); var startSym = $interpolate.startSymbol(); var endSym = $interpolate.endSymbol(); @@ -122,6 +121,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) var popupTimeout; var $body; var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; + var triggers = setTriggers( undefined ); // By default, the tooltip is not open. // TODO add ability to start tooltip opened From 4fd5bf43459df7bdd0ebf070b6137826669f8c5d Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sat, 3 Aug 2013 15:20:50 +0200 Subject: [PATCH 0028/1761] fix(tooltip): correctly handle initial events unbinding Closes #750 --- src/tooltip/tooltip.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index 07039dce05..2b65f3a931 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -122,6 +122,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) var $body; var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; var triggers = setTriggers( undefined ); + var hasRegisteredTriggers = false; // By default, the tooltip is not open. // TODO add ability to start tooltip opened @@ -276,8 +277,11 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) }); attrs.$observe( prefix+'Trigger', function ( val ) { - element.unbind( triggers.show, showTooltipBind ); - element.unbind( triggers.hide, hideTooltipBind ); + + if (hasRegisteredTriggers) { + element.unbind( triggers.show, showTooltipBind ); + element.unbind( triggers.hide, hideTooltipBind ); + } triggers = setTriggers( val ); @@ -287,6 +291,8 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) element.bind( triggers.show, showTooltipBind ); element.bind( triggers.hide, hideTooltipBind ); } + + hasRegisteredTriggers = true; }); attrs.$observe( prefix+'AppendToBody', function ( val ) { From d50b0547f8d75b550da5a9aa382d42c75d8eda79 Mon Sep 17 00:00:00 2001 From: Paul Gibbs Date: Sun, 21 Jul 2013 08:52:19 -0700 Subject: [PATCH 0029/1761] fix(tooltip): bind correct 'hide' event handler If a user defines a tooltip-trigger attribute, this ensures the correct event handlers is used to hide the tooltip. Fixes a bug where if a user sets both a default trigger using the tooltip provider, and then tries to override with an attribute, the wrong 'hide' event was being used. --- src/tooltip/test/tooltip.spec.js | 18 ++++++++++++++++++ src/tooltip/tooltip.js | 17 +++++------------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/tooltip/test/tooltip.spec.js b/src/tooltip/test/tooltip.spec.js index ae4179caac..e392124adc 100644 --- a/src/tooltip/test/tooltip.spec.js +++ b/src/tooltip/test/tooltip.spec.js @@ -493,6 +493,24 @@ describe( '$tooltipProvider', function() { elm.trigger('blur'); expect( elmScope.tt_isOpen ).toBeFalsy(); })); + + it( 'should override the show and hide triggers if there is an attribute', inject( function ( $rootScope, $compile ) { + elmBody = angular.element( + '
' + ); + + scope = $rootScope; + $compile(elmBody)(scope); + scope.$digest(); + elm = elmBody.find('input'); + elmScope = elm.scope(); + + expect( elmScope.tt_isOpen ).toBeFalsy(); + elm.trigger('mouseenter'); + expect( elmScope.tt_isOpen ).toBeTruthy(); + elm.trigger('mouseleave'); + expect( elmScope.tt_isOpen ).toBeFalsy(); + })); }); describe( 'triggers with a custom mapped value', function() { diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index 2b65f3a931..28eb9e513e 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -82,16 +82,9 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) * undefined; otherwise, it uses the `triggerMap` value of the show * trigger; else it will just use the show trigger. */ - function setTriggers ( trigger ) { - var show, hide; - - show = trigger || options.trigger || defaultTriggerShow; - if ( angular.isDefined ( options.trigger ) ) { - hide = triggerMap[options.trigger] || show; - } else { - hide = triggerMap[show] || show; - } - + function getTriggers ( trigger ) { + var show = trigger || options.trigger || defaultTriggerShow; + var hide = triggerMap[show] || show; return { show: show, hide: hide @@ -121,7 +114,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) var popupTimeout; var $body; var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; - var triggers = setTriggers( undefined ); + var triggers = getTriggers( undefined ); var hasRegisteredTriggers = false; // By default, the tooltip is not open. @@ -283,7 +276,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) element.unbind( triggers.hide, hideTooltipBind ); } - triggers = setTriggers( val ); + triggers = getTriggers( val ); if ( triggers.show === triggers.hide ) { element.bind( triggers.show, toggleTooltipBind ); From a742690b65dcf6a4c6d94caf238497561b4fa0e7 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sun, 4 Aug 2013 13:13:22 +0200 Subject: [PATCH 0030/1761] demo(pagination): correct quotes in pagination demo --- src/pagination/docs/demo.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pagination/docs/demo.html b/src/pagination/docs/demo.html index 4c5f1f9745..e37e615cfa 100644 --- a/src/pagination/docs/demo.html +++ b/src/pagination/docs/demo.html @@ -2,7 +2,7 @@

Default

- + From dab18336e4d3459cd027d475ed5c535153ced0b4 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sat, 3 Aug 2013 14:38:12 +0300 Subject: [PATCH 0031/1761] feat(datepikcer): `ngModelController` plug & new `datepikcerPopup` * `ngModelController` integration * `datepikcerPopup` directive ti use with inputs * invalid & disabled validation * add `min` / `max` into configuration Closes #612 --- src/datepicker/datepicker.js | 486 +++++++++++----- src/datepicker/docs/demo.html | 16 +- src/datepicker/docs/demo.js | 13 +- src/datepicker/test/datepicker.spec.js | 770 ++++++++++++++++++------- template/datepicker/datepicker.html | 4 +- template/datepicker/popup.html | 12 + 6 files changed, 959 insertions(+), 342 deletions(-) create mode 100644 template/datepicker/popup.html diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index fdfd63e8c6..2b89878a28 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -1,4 +1,4 @@ -angular.module('ui.bootstrap.datepicker', []) +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) .constant('datepickerConfig', { dayFormat: 'dd', @@ -9,31 +9,139 @@ angular.module('ui.bootstrap.datepicker', []) monthTitleFormat: 'yyyy', showWeeks: true, startingDay: 0, - yearRange: 20 + yearRange: 20, + minDate: null, + maxDate: null }) -.directive( 'datepicker', ['dateFilter', '$parse', 'datepickerConfig', function (dateFilter, $parse, datepickerConfig) { +.controller('DatepickerController', ['$scope', '$attrs', 'dateFilter', 'datepickerConfig', function($scope, $attrs, dateFilter, dtConfig) { + var format = { + day: getValue($attrs.dayFormat, dtConfig.dayFormat), + month: getValue($attrs.monthFormat, dtConfig.monthFormat), + year: getValue($attrs.yearFormat, dtConfig.yearFormat), + dayHeader: getValue($attrs.dayHeaderFormat, dtConfig.dayHeaderFormat), + dayTitle: getValue($attrs.dayTitleFormat, dtConfig.dayTitleFormat), + monthTitle: getValue($attrs.monthTitleFormat, dtConfig.monthTitleFormat) + }, + startingDay = getValue($attrs.startingDay, dtConfig.startingDay), + yearRange = getValue($attrs.yearRange, dtConfig.yearRange); + + this.minDate = dtConfig.minDate ? new Date(dtConfig.minDate) : null; + this.maxDate = dtConfig.maxDate ? new Date(dtConfig.maxDate) : null; + + function getValue(value, defaultValue) { + return angular.isDefined(value) ? $scope.$parent.$eval(value) : defaultValue; + } + + function getDaysInMonth( year, month ) { + return new Date(year, month, 0).getDate(); + } + + function getDates(startDate, n) { + var dates = new Array(n); + var current = startDate, i = 0; + while (i < n) { + dates[i++] = new Date(current); + current.setDate( current.getDate() + 1 ); + } + return dates; + } + + function makeDate(date, format, isSelected, isSecondary) { + return { date: date, label: dateFilter(date, format), selected: !!isSelected, secondary: !!isSecondary }; + } + + this.modes = [ + { + name: 'day', + getVisibleDates: function(date, selected) { + var year = date.getFullYear(), month = date.getMonth(), firstDayOfMonth = new Date(year, month, 1); + var difference = startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth), numDates = 0; + + if ( numDisplayedFromPreviousMonth > 0 ) { + firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); + numDates += numDisplayedFromPreviousMonth; // Previous + } + numDates += getDaysInMonth(year, month + 1); // Current + numDates += (7 - numDates % 7) % 7; // Next + + var days = getDates(firstDate, numDates), labels = new Array(7); + for (var i = 0; i < numDates; i ++) { + var dt = new Date(days[i]); + days[i] = makeDate(dt, format.day, (selected && selected.getDate() === dt.getDate() && selected.getMonth() === dt.getMonth() && selected.getFullYear() === dt.getFullYear()), dt.getMonth() !== month); + } + for (var j = 0; j < 7; j++) { + labels[j] = dateFilter(days[j].date, format.dayHeader); + } + return { objects: days, title: dateFilter(date, format.dayTitle), labels: labels }; + }, + compare: function(date1, date2) { + return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); + }, + split: 7, + step: { months: 1 } + }, + { + name: 'month', + getVisibleDates: function(date, selected) { + var months = new Array(12), year = date.getFullYear(); + for ( var i = 0; i < 12; i++ ) { + var dt = new Date(year, i, 1); + months[i] = makeDate(dt, format.month, (selected && selected.getMonth() === i && selected.getFullYear() === year)); + } + return { objects: months, title: dateFilter(date, format.monthTitle) }; + }, + compare: function(date1, date2) { + return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); + }, + split: 3, + step: { years: 1 } + }, + { + name: 'year', + getVisibleDates: function(date, selected) { + var years = new Array(yearRange), year = date.getFullYear(), startYear = parseInt((year - 1) / yearRange, 10) * yearRange + 1; + for ( var i = 0; i < yearRange; i++ ) { + var dt = new Date(startYear + i, 0, 1); + years[i] = makeDate(dt, format.year, (selected && selected.getFullYear() === dt.getFullYear())); + } + return { objects: years, title: [years[0].label, years[yearRange - 1].label].join(' - ') }; + }, + compare: function(date1, date2) { + return date1.getFullYear() - date2.getFullYear(); + }, + split: 5, + step: { years: yearRange } + } + ]; + + this.isDisabled = function(date, mode) { + var currentMode = this.modes[mode || 0]; + return ((this.minDate && currentMode.compare(date, this.minDate) < 0) || (this.maxDate && currentMode.compare(date, this.maxDate) > 0) || ($scope.dateDisabled && $scope.dateDisabled({date: date, mode: currentMode.name}))); + }; +}]) + +.directive( 'datepicker', ['dateFilter', '$parse', 'datepickerConfig', '$log', function (dateFilter, $parse, datepickerConfig, $log) { return { restrict: 'EA', replace: true, + templateUrl: 'template/datepicker/datepicker.html', scope: { - model: '=ngModel', dateDisabled: '&' }, - templateUrl: 'template/datepicker/datepicker.html', - link: function(scope, element, attrs) { - scope.mode = 'day'; // Initial mode + require: ['datepicker', '?^ngModel'], + controller: 'DatepickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModel = ctrls[1]; + + if (!ngModel) { + return; // do nothing if no ng-model + } // Configuration parameters - var selected = new Date(), showWeeks, minDate, maxDate, format = {}; - format.day = angular.isDefined(attrs.dayFormat) ? scope.$eval(attrs.dayFormat) : datepickerConfig.dayFormat; - format.month = angular.isDefined(attrs.monthFormat) ? scope.$eval(attrs.monthFormat) : datepickerConfig.monthFormat; - format.year = angular.isDefined(attrs.yearFormat) ? scope.$eval(attrs.yearFormat) : datepickerConfig.yearFormat; - format.dayHeader = angular.isDefined(attrs.dayHeaderFormat) ? scope.$eval(attrs.dayHeaderFormat) : datepickerConfig.dayHeaderFormat; - format.dayTitle = angular.isDefined(attrs.dayTitleFormat) ? scope.$eval(attrs.dayTitleFormat) : datepickerConfig.dayTitleFormat; - format.monthTitle = angular.isDefined(attrs.monthTitleFormat) ? scope.$eval(attrs.monthTitleFormat) : datepickerConfig.monthTitleFormat; - var startingDay = angular.isDefined(attrs.startingDay) ? scope.$eval(attrs.startingDay) : datepickerConfig.startingDay; - var yearRange = angular.isDefined(attrs.yearRange) ? scope.$eval(attrs.yearRange) : datepickerConfig.yearRange; + var mode = 0, selected = new Date(), showWeeks = datepickerConfig.showWeeks; if (attrs.showWeeks) { scope.$parent.$watch($parse(attrs.showWeeks), function(value) { @@ -41,174 +149,282 @@ angular.module('ui.bootstrap.datepicker', []) updateShowWeekNumbers(); }); } else { - showWeeks = datepickerConfig.showWeeks; updateShowWeekNumbers(); } if (attrs.min) { scope.$parent.$watch($parse(attrs.min), function(value) { - minDate = value ? new Date(value) : null; + datepickerCtrl.minDate = value ? new Date(value) : null; refill(); }); } if (attrs.max) { scope.$parent.$watch($parse(attrs.max), function(value) { - maxDate = value ? new Date(value) : null; + datepickerCtrl.maxDate = value ? new Date(value) : null; refill(); }); } - function updateCalendar (rows, labels, title) { - scope.rows = rows; - scope.labels = labels; - scope.title = title; + function updateShowWeekNumbers() { + scope.showWeekNumbers = mode === 0 && showWeeks; } - // Define whether the week number are visible - function updateShowWeekNumbers() { - scope.showWeekNumbers = ( scope.mode === 'day' && showWeeks ); + // Split array into smaller arrays + function split(arr, size) { + var arrays = []; + while (arr.length > 0) { + arrays.push(arr.splice(0, size)); + } + return arrays; } - function compare( date1, date2 ) { - if ( scope.mode === 'year') { - return date2.getFullYear() - date1.getFullYear(); - } else if ( scope.mode === 'month' ) { - return new Date( date2.getFullYear(), date2.getMonth() ) - new Date( date1.getFullYear(), date1.getMonth() ); - } else if ( scope.mode === 'day' ) { - return (new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) - new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) ); + function refill( updateSelected ) { + var date = null, valid = true; + + if ( ngModel.$modelValue ) { + date = new Date( ngModel.$modelValue ); + + if ( isNaN(date) ) { + valid = false; + $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + } else if ( updateSelected ) { + selected = date; + } } + ngModel.$setValidity('date', valid); + + var currentMode = datepickerCtrl.modes[mode], data = currentMode.getVisibleDates(selected, date); + angular.forEach(data.objects, function(obj) { + obj.disabled = datepickerCtrl.isDisabled(obj.date, mode); + }); + + ngModel.$setValidity('date-disabled', (!date || !datepickerCtrl.isDisabled(date))); + + scope.rows = split(data.objects, currentMode.split); + scope.labels = data.labels || []; + scope.title = data.title; } - function isDisabled(date) { - return ((minDate && compare(date, minDate) > 0) || (maxDate && compare(date, maxDate) < 0) || (scope.dateDisabled && scope.dateDisabled({ date: date, mode: scope.mode }))); + function setMode(value) { + mode = value; + updateShowWeekNumbers(); + refill(); } - // Split array into smaller arrays - var split = function(a, size) { - var arrays = []; - while (a.length > 0) { - arrays.push(a.splice(0, size)); + ngModel.$render = function() { + refill( true ); + }; + + scope.select = function( date ) { + if ( mode === 0 ) { + var dt = new Date( ngModel.$modelValue ); + dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); + ngModel.$setViewValue( dt ); + refill( true ); + } else { + selected = date; + setMode( mode - 1 ); } - return arrays; }; - var getDaysInMonth = function( year, month ) { - return new Date(year, month + 1, 0).getDate(); + scope.move = function(direction) { + var step = datepickerCtrl.modes[mode].step; + selected.setMonth( selected.getMonth() + direction * (step.months || 0) ); + selected.setFullYear( selected.getFullYear() + direction * (step.years || 0) ); + refill(); + }; + scope.toggleMode = function() { + setMode( (mode + 1) % datepickerCtrl.modes.length ); + }; + scope.getWeekNumber = function(row) { + return ( mode === 0 && scope.showWeekNumbers && row.length === 7 ) ? getISO8601WeekNumber(row[0].date) : null; }; - var fill = { - day: function() { - var days = [], labels = [], lastDate = null; + function getISO8601WeekNumber(date) { + var checkDate = new Date(date); + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday + var time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + } + } + }; +}]) - function addDays( dt, n, isCurrentMonth ) { - for (var i =0; i < n; i ++) { - days.push( {date: new Date(dt), isCurrent: isCurrentMonth, isSelected: isSelected(dt), label: dateFilter(dt, format.day), disabled: isDisabled(dt) } ); - dt.setDate( dt.getDate() + 1 ); - } - lastDate = dt; - } +.constant('datepickerPopupConfig', { + dateFormat: 'yyyy-MM-dd', + closeOnDateSelection: true +}) + +.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'datepickerPopupConfig', +function ($compile, $parse, $document, $position, dateFilter, datepickerPopupConfig) { + return { + restrict: 'EA', + require: 'ngModel', + link: function(originalScope, element, attrs, ngModel) { - var d = new Date(selected); - d.setDate(1); + var closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection; + var dateFormat = attrs.datepickerPopup || datepickerPopupConfig.dateFormat; - var difference = startingDay - d.getDay(); - var numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference; + // create a child scope for the datepicker directive so we are not polluting original scope + var scope = originalScope.$new(); + originalScope.$on('$destroy', function() { + scope.$destroy(); + }); - if ( numDisplayedFromPreviousMonth > 0 ) { - d.setDate( - numDisplayedFromPreviousMonth + 1 ); - addDays(d, numDisplayedFromPreviousMonth, false); - } - addDays(lastDate || d, getDaysInMonth(selected.getFullYear(), selected.getMonth()), true); - addDays(lastDate, (7 - days.length % 7) % 7, false); + function formatDate(value) { + return (value) ? dateFilter(value, dateFormat) : null; + } + ngModel.$formatters.push(formatDate); - // Day labels - for (i = 0; i < 7; i++) { - labels.push( dateFilter(days[i].date, format.dayHeader) ); - } - updateCalendar( split( days, 7 ), labels, dateFilter(selected, format.dayTitle) ); - }, - month: function() { - var months = [], i = 0, year = selected.getFullYear(); - while ( i < 12 ) { - var dt = new Date(year, i++, 1); - months.push( {date: dt, isCurrent: true, isSelected: isSelected(dt), label: dateFilter(dt, format.month), disabled: isDisabled(dt)} ); - } - updateCalendar( split( months, 3 ), [], dateFilter(selected, format.monthTitle) ); - }, - year: function() { - var years = [], year = parseInt((selected.getFullYear() - 1) / yearRange, 10) * yearRange + 1; - for ( var i = 0; i < yearRange; i++ ) { - var dt = new Date(year + i, 0, 1); - years.push( {date: dt, isCurrent: true, isSelected: isSelected(dt), label: dateFilter(dt, format.year), disabled: isDisabled(dt)} ); + // TODO: reverse from dateFilter string to Date object + function parseDate(value) { + if ( value ) { + var date = new Date(value); + if (!isNaN(date)) { + return date; } - var title = years[0].label + ' - ' + years[years.length - 1].label; - updateCalendar( split( years, 5 ), [], title ); + } + return value; + } + ngModel.$parsers.push(parseDate); + + var getIsOpen, setIsOpen; + if ( attrs.open ) { + getIsOpen = $parse(attrs.open); + setIsOpen = getIsOpen.assign; + + originalScope.$watch(getIsOpen, function updateOpen(value) { + scope.isOpen = !! value; + }); + } + scope.isOpen = getIsOpen ? getIsOpen(originalScope) : false; // Initial state + + function setOpen( value ) { + if (setIsOpen) { + setIsOpen(originalScope, !!value); + } else { + scope.isOpen = !!value; + } + } + + var documentClickBind = function(event) { + if (scope.isOpen && event.target !== element[0]) { + scope.$apply(function() { + setOpen(false); + }); } }; - var refill = function() { - fill[scope.mode](); + + var elementFocusBind = function() { + scope.$apply(function() { + setOpen( true ); + }); }; - var isSelected = function( dt ) { - if ( scope.model && scope.model.getFullYear() === dt.getFullYear() ) { - if ( scope.mode === 'year' ) { - return true; - } - if ( scope.model.getMonth() === dt.getMonth() ) { - return ( scope.mode === 'month' || (scope.mode === 'day' && scope.model.getDate() === dt.getDate()) ); - } + + // popup element used to display calendar + var popupEl = angular.element(''); + popupEl.attr({ + 'ng-model': 'date', + 'ng-change': 'dateSelection()' + }); + var datepickerEl = popupEl.find('datepicker'); + if (attrs.datepickerOptions) { + datepickerEl.attr(angular.extend({}, originalScope.$eval(attrs.datepickerOptions))); + } + + var $setModelValue = $parse(attrs.ngModel).assign; + + // Inner change + scope.dateSelection = function() { + $setModelValue(originalScope, scope.date); + if (closeOnDateSelection) { + setOpen( false ); } - return false; }; - scope.$watch('model', function ( dt, olddt ) { - if ( angular.isDate(dt) ) { - selected = new Date(dt); - } + // Outter change + scope.$watch(function() { + return ngModel.$modelValue; + }, function(value) { + if (angular.isString(value)) { + var date = parseDate(value); - if ( ! angular.equals(dt, olddt) ) { - refill(); + if (value && !date) { + $setModelValue(originalScope, null); + throw new Error(value + ' cannot be parsed to a date object.'); + } else { + value = date; + } } + scope.date = value; + updatePosition(); }); - scope.$watch('mode', function() { - updateShowWeekNumbers(); - refill(); - }); - scope.select = function( dt ) { - selected = new Date(dt); + function addWatchableAttribute(attribute, scopeProperty, datepickerAttribute) { + if (attribute) { + originalScope.$watch($parse(attribute), function(value){ + scope[scopeProperty] = value; + }); + datepickerEl.attr(datepickerAttribute || scopeProperty, scopeProperty); + } + } + addWatchableAttribute(attrs.min, 'min'); + addWatchableAttribute(attrs.max, 'max'); + if (attrs.showWeeks) { + addWatchableAttribute(attrs.showWeeks, 'showWeeks', 'show-weeks'); + } else { + scope.showWeeks = true; + datepickerEl.attr('show-weeks', 'showWeeks'); + } + if (attrs.dateDisabled) { + datepickerEl.attr('date-disabled', attrs.dateDisabled); + } + + function updatePosition() { + scope.position = $position.position(element); + scope.position.top = scope.position.top + element.prop('offsetHeight'); + } - if ( scope.mode === 'year' ) { - scope.mode = 'month'; - selected.setFullYear( dt.getFullYear() ); - } else if ( scope.mode === 'month' ) { - scope.mode = 'day'; - selected.setMonth( dt.getMonth() ); - } else if ( scope.mode === 'day' ) { - scope.model = new Date(selected); + scope.$watch('isOpen', function(value) { + if (value) { + updatePosition(); + $document.bind('click', documentClickBind); + element.unbind('focus', elementFocusBind); + element.focus(); + } else { + $document.unbind('click', documentClickBind); + element.bind('focus', elementFocusBind); } - }; - scope.move = function(step) { - if (scope.mode === 'day') { - selected.setMonth( selected.getMonth() + step ); - } else if (scope.mode === 'month') { - selected.setFullYear( selected.getFullYear() + step ); - } else if (scope.mode === 'year') { - selected.setFullYear( selected.getFullYear() + step * yearRange ); + + if ( setIsOpen ) { + setIsOpen(originalScope, value); } - refill(); + }); + + scope.today = function() { + $setModelValue(originalScope, new Date()); }; - scope.toggleMode = function() { - scope.mode = ( scope.mode === 'day' ) ? 'month' : ( scope.mode === 'month' ) ? 'year' : 'day'; + scope.clear = function() { + $setModelValue(originalScope, null); }; - scope.getWeekNumber = function(row) { - if ( scope.mode !== 'day' || ! scope.showWeekNumbers || row.length !== 7 ) { - return; - } - var index = ( startingDay > 4 ) ? 11 - startingDay : 4 - startingDay; // Thursday - var d = new Date( row[ index ].date ); - d.setHours(0, 0, 0); - return Math.ceil((((d - new Date(d.getFullYear(), 0, 1)) / 86400000) + 1) / 7); // 86400000 = 1000*60*60*24; - }; + element.after($compile(popupEl)(scope)); + } + }; +}]) + +.directive('datepickerPopupWrap', [function() { + return { + restrict:'E', + replace: true, + transclude: true, + templateUrl: 'template/datepicker/popup.html', + link:function (scope, element, attrs) { + element.bind('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + }); } }; }]); \ No newline at end of file diff --git a/src/datepicker/docs/demo.html b/src/datepicker/docs/demo.html index fed55e9725..526b2ab62b 100644 --- a/src/datepicker/docs/demo.html +++ b/src/datepicker/docs/demo.html @@ -1,9 +1,19 @@
-
Selected date is: {{dt | date:'fullDate' }}
+
+ +
+ +
+ + +
+ +
- + + - +
\ No newline at end of file diff --git a/src/datepicker/docs/demo.js b/src/datepicker/docs/demo.js index cf7a4df191..18eb51ec83 100644 --- a/src/datepicker/docs/demo.js +++ b/src/datepicker/docs/demo.js @@ -1,4 +1,4 @@ -var DatepickerDemoCtrl = function ($scope) { +var DatepickerDemoCtrl = function ($scope, $timeout) { $scope.today = function() { $scope.dt = new Date(); }; @@ -22,4 +22,15 @@ var DatepickerDemoCtrl = function ($scope) { $scope.minDate = ( $scope.minDate ) ? null : new Date(); }; $scope.toggleMin(); + + $scope.open = function() { + $timeout(function() { + $scope.opened = true; + }); + }; + + $scope.dateOptions = { + 'year-format': "'yy'", + 'starting-day': 1 + }; }; diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index 7e1a77f680..79ae2c0e10 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -2,11 +2,12 @@ describe('datepicker directive', function () { var $rootScope, element; beforeEach(module('ui.bootstrap.datepicker')); beforeEach(module('template/datepicker/datepicker.html')); + beforeEach(module('template/datepicker/popup.html')); beforeEach(inject(function(_$compile_, _$rootScope_) { $compile = _$compile_; $rootScope = _$rootScope_; $rootScope.date = new Date("September 30, 2010 15:30:00"); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); @@ -59,19 +60,54 @@ describe('datepicker directive', function () { return weeks; } - function getOptions(rowIndex) { - var cols = element.find('tbody').find('tr').eq(rowIndex).find('td'); - var days = []; - for (var i = 1, n = cols.length; i < n; i++) { - days.push( cols.eq(i).find('button').text() ); + function getOptions() { + 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 = 1, n = cols.length; i < n; i++) { + days.push( cols.eq(i).find('button').text() ); + } + rows.push(days); } - return days; + return rows; } - function getOptionsEl(rowIndex, colIndex) { + function _getOptionEl(rowIndex, colIndex) { return element.find('tbody').find('tr').eq(rowIndex).find('td').eq(colIndex + 1); } + function clickOption(rowIndex, colIndex) { + _getOptionEl(rowIndex, colIndex).find('button').click(); + } + + function isDisabledOption(rowIndex, colIndex) { + return _getOptionEl(rowIndex, colIndex).find('button').prop('disabled'); + } + + function getAllOptionsEl() { + var tr = element.find('tbody').find('tr'), rows = []; + for (var i = 0; i < tr.length; i++) { + var td = tr.eq(i).find('td'), cols = []; + for (var j = 0; j < td.length; j++) { + cols.push( td.eq(j + 1) ); + } + rows.push(cols); + } + return rows; + } + + function expectSelectedElement( row, col ) { + var options = getAllOptionsEl(); + for (var i = 0, n = options.length; i < n; i ++) { + var optionsRow = options[i]; + for (var j = 0; j < optionsRow.length; j ++) { + expect(optionsRow[j].find('button').hasClass('btn-info')).toBe( i === row && j === col ); + } + } + } + it('is a `
#
{{ getWeekNumber(row) }} - +
` element', function() { expect(element.prop('tagName')).toBe('TABLE'); expect(element.find('thead').find('tr').length).toBe(2); @@ -87,15 +123,17 @@ describe('datepicker directive', function () { }); it('renders the calendar days correctly', function() { - expect(getOptions(0)).toEqual(['29', '30', '31', '01', '02', '03', '04']); - expect(getOptions(1)).toEqual(['05', '06', '07', '08', '09', '10', '11']); - expect(getOptions(2)).toEqual(['12', '13', '14', '15', '16', '17', '18']); - expect(getOptions(3)).toEqual(['19', '20', '21', '22', '23', '24', '25']); - expect(getOptions(4)).toEqual(['26', '27', '28', '29', '30', '01', '02']); + expect(getOptions()).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'] + ]); }); - it('renders the week numbers correctly', function() { - expect(getWeeks()).toEqual(['35', '36', '37', '38', '39']); + it('renders the week numbers based on ISO 8601', function() { + expect(getWeeks()).toEqual(['34', '35', '36', '37', '38']); }); it('value is correct', function() { @@ -103,37 +141,36 @@ describe('datepicker directive', function () { }); it('has `selected` only the correct day', function() { - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( ( i === 4 && j === 4) ); - } - } + expectSelectedElement( 4, 4 ); }); - it('has no `selected` day when model is nulled', function() { + it('has no `selected` day when model is cleared', function() { $rootScope.date = null; $rootScope.$digest(); expect($rootScope.date).toBe(null); + expectSelectedElement( null, null ); + }); - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( false ); - } - } + it('does not change current view when model is cleared', function() { + $rootScope.date = null; + $rootScope.$digest(); + + expect($rootScope.date).toBe(null); + expect(getTitle()).toBe('September 2010'); }); it('`disables` visible dates from other months', function() { + var options = getAllOptionsEl(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').find('span').hasClass('muted')).toBe( ((i === 0 && j < 3) || (i === 4 && j > 4)) ); + expect(options[i][j].find('button').find('span').hasClass('muted')).toBe( ((i === 0 && j < 3) || (i === 4 && j > 4)) ); } } }); it('updates the model when a day is clicked', function() { - var el = getOptionsEl(2, 3).find('button'); - el.click(); + clickOption(2, 3); expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); }); @@ -142,24 +179,22 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('August 2010'); expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); - expect(getOptions(0)).toEqual(['01', '02', '03', '04', '05', '06', '07']); - expect(getOptions(1)).toEqual(['08', '09', '10', '11', '12', '13', '14']); - expect(getOptions(2)).toEqual(['15', '16', '17', '18', '19', '20', '21']); - expect(getOptions(3)).toEqual(['22', '23', '24', '25', '26', '27', '28']); - expect(getOptions(4)).toEqual(['29', '30', '31', '01', '02', '03', '04']); - - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( false ); - } - } + expect(getOptions()).toEqual([ + ['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'] + ]); + + expectSelectedElement( null, null ); }); it('updates the model only when when a day is clicked in the `previous` month', function() { clickPreviousButton(); expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); - getOptionsEl(2, 3).find('button').click(); + clickOption(2, 3); expect($rootScope.date).toEqual(new Date('August 18, 2010 15:30:00')); }); @@ -168,61 +203,90 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('October 2010'); expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); - expect(getOptions(0)).toEqual(['26', '27', '28', '29', '30', '01', '02']); - expect(getOptions(1)).toEqual(['03', '04', '05', '06', '07', '08', '09']); - expect(getOptions(2)).toEqual(['10', '11', '12', '13', '14', '15', '16']); - expect(getOptions(3)).toEqual(['17', '18', '19', '20', '21', '22', '23']); - expect(getOptions(4)).toEqual(['24', '25', '26', '27', '28', '29', '30']); - - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( (i === 0 && j === 4) ); - } - } + expect(getOptions()).toEqual([ + ['26', '27', '28', '29', '30', '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', '06'] + ]); + + expectSelectedElement( 0, 4 ); }); it('updates the model only when when a day is clicked in the `next` month', function() { clickNextButton(); expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); - getOptionsEl(2, 3).find('button').click(); + clickOption(2, 3); expect($rootScope.date).toEqual(new Date('October 13, 2010 15:30:00')); }); it('updates the calendar when a day of another month is selected', function() { - getOptionsEl(4, 5).find('button').click(); + clickOption(4, 5); expect($rootScope.date).toEqual(new Date('October 01, 2010 15:30:00')); expect(getTitle()).toBe('October 2010'); expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); - expect(getOptions(0)).toEqual(['26', '27', '28', '29', '30', '01', '02']); - expect(getOptions(1)).toEqual(['03', '04', '05', '06', '07', '08', '09']); - expect(getOptions(2)).toEqual(['10', '11', '12', '13', '14', '15', '16']); - expect(getOptions(3)).toEqual(['17', '18', '19', '20', '21', '22', '23']); - expect(getOptions(4)).toEqual(['24', '25', '26', '27', '28', '29', '30']); - - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( (i === 0 && j === 5) ); - } - } + expect(getOptions()).toEqual([ + ['26', '27', '28', '29', '30', '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', '06'] + ]); + + expectSelectedElement( 0, 5 ); }); - it('updates calendar when `model` changes', function() { - $rootScope.date = new Date('November 7, 2005 23:30:00'); - $rootScope.$digest(); + describe('when `model` changes', function () { + function testCalendar() { + expect(getTitle()).toBe('November 2005'); + expect(getOptions()).toEqual([ + ['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'] + ]); + + expectSelectedElement( 1, 1 ); + } - expect(getTitle()).toBe('November 2005'); - expect(getOptions(0)).toEqual(['30', '31', '01', '02', '03', '04', '05']); - expect(getOptions(1)).toEqual(['06', '07', '08', '09', '10', '11', '12']); - expect(getOptions(2)).toEqual(['13', '14', '15', '16', '17', '18', '19']); - expect(getOptions(3)).toEqual(['20', '21', '22', '23', '24', '25', '26']); - expect(getOptions(4)).toEqual(['27', '28', '29', '30', '01', '02', '03']); + describe('to a Date object', function() { + it('updates', function() { + $rootScope.date = new Date('November 7, 2005 23:30:00'); + $rootScope.$digest(); + testCalendar(); + expect(angular.isDate($rootScope.date)).toBe(true); + }); + }); - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( (i === 1 && j === 1) ); - } - } + describe('not to a Date object', function() { + + it('to a Number, it updates calendar', function() { + $rootScope.date = parseInt((new Date('November 7, 2005 23:30:00')).getTime(), 10); + $rootScope.$digest(); + testCalendar(); + expect(angular.isNumber($rootScope.date)).toBe(true); + }); + + it('to a string that can be parsed by Date, it updates calendar', function() { + $rootScope.date = 'November 7, 2005 23:30:00'; + $rootScope.$digest(); + testCalendar(); + expect(angular.isString($rootScope.date)).toBe(true); + }); + + it('to a string that cannot be parsed by Date, it gets invalid', function() { + $rootScope.date = 'pizza'; + $rootScope.$digest(); + expect(element.hasClass('ng-invalid')).toBeTruthy(); + expect(element.hasClass('ng-invalid-date')).toBeTruthy(); + expect($rootScope.date).toBe('pizza'); + }); + }); }); it('loops between different modes', function() { @@ -249,10 +313,12 @@ describe('datepicker directive', function () { it('shows months as options', function() { expect(getLabels()).toEqual([]); - expect(getOptions(0)).toEqual(['January', 'February', 'March']); - expect(getOptions(1)).toEqual(['April', 'May', 'June']); - expect(getOptions(2)).toEqual(['July', 'August', 'September']); - expect(getOptions(3)).toEqual(['October', 'November', 'December']); + expect(getOptions()).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); }); it('does not change the model', function() { @@ -260,11 +326,7 @@ describe('datepicker directive', function () { }); it('has `selected` only the correct month', function() { - for (var i = 0; i < 4; i ++) { - for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( ( i === 2 && j === 2) ); - } - } + expectSelectedElement( 2, 2 ); }); it('moves to the previous year when `previous` button is clicked', function() { @@ -272,16 +334,14 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('2009'); expect(getLabels()).toEqual([]); - expect(getOptions(0)).toEqual(['January', 'February', 'March']); - expect(getOptions(1)).toEqual(['April', 'May', 'June']); - expect(getOptions(2)).toEqual(['July', 'August', 'September']); - expect(getOptions(3)).toEqual(['October', 'November', 'December']); - - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( false ); - } - } + expect(getOptions()).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); + + expectSelectedElement( null, null ); }); it('moves to the next year when `next` button is clicked', function() { @@ -289,31 +349,33 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('2011'); expect(getLabels()).toEqual([]); - expect(getOptions(0)).toEqual(['January', 'February', 'March']); - expect(getOptions(1)).toEqual(['April', 'May', 'June']); - expect(getOptions(2)).toEqual(['July', 'August', 'September']); - expect(getOptions(3)).toEqual(['October', 'November', 'December']); - - for (var i = 0; i < 4; i ++) { - for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( false ); - } - } + expect(getOptions()).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); + + expectSelectedElement( null, null ); }); it('renders correctly when a month is clicked', function() { clickPreviousButton(5); expect(getTitle()).toBe('2005'); - var monthNovEl = getOptionsEl(3, 1).find('button'); - monthNovEl.click(); + clickOption(3, 1); expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); expect(getTitle()).toBe('November 2005'); - expect(getOptions(0)).toEqual(['30', '31', '01', '02', '03', '04', '05']); - expect(getOptions(1)).toEqual(['06', '07', '08', '09', '10', '11', '12']); - expect(getOptions(2)).toEqual(['13', '14', '15', '16', '17', '18', '19']); - expect(getOptions(3)).toEqual(['20', '21', '22', '23', '24', '25', '26']); - expect(getOptions(4)).toEqual(['27', '28', '29', '30', '01', '02', '03']); + expect(getOptions()).toEqual([ + ['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'] + ]); + + clickOption(2, 3); + expect($rootScope.date).toEqual(new Date('November 16, 2005 15:30:00')); }); }); @@ -328,10 +390,12 @@ describe('datepicker directive', function () { it('shows years as options', function() { expect(getLabels()).toEqual([]); - expect(getOptions(0)).toEqual(['2001', '2002', '2003', '2004', '2005']); - expect(getOptions(1)).toEqual(['2006', '2007', '2008', '2009', '2010']); - expect(getOptions(2)).toEqual(['2011', '2012', '2013', '2014', '2015']); - expect(getOptions(3)).toEqual(['2016', '2017', '2018', '2019', '2020']); + expect(getOptions()).toEqual([ + ['2001', '2002', '2003', '2004', '2005'], + ['2006', '2007', '2008', '2009', '2010'], + ['2011', '2012', '2013', '2014', '2015'], + ['2016', '2017', '2018', '2019', '2020'] + ]); }); it('does not change the model', function() { @@ -339,11 +403,7 @@ describe('datepicker directive', function () { }); it('has `selected` only the selected year', function() { - for (var i = 0; i < 4; i ++) { - for (var j = 0; j < 5; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( ( i === 1 && j === 4) ); - } - } + expectSelectedElement( 1, 4 ); }); it('moves to the previous year set when `previous` button is clicked', function() { @@ -351,16 +411,13 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('1981 - 2000'); expect(getLabels()).toEqual([]); - expect(getOptions(0)).toEqual(['1981', '1982', '1983', '1984', '1985']); - expect(getOptions(1)).toEqual(['1986', '1987', '1988', '1989', '1990']); - expect(getOptions(2)).toEqual(['1991', '1992', '1993', '1994', '1995']); - expect(getOptions(3)).toEqual(['1996', '1997', '1998', '1999', '2000']); - - for (var i = 0; i < 4; i ++) { - for (var j = 0; j < 5; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( false ); - } - } + expect(getOptions()).toEqual([ + ['1981', '1982', '1983', '1984', '1985'], + ['1986', '1987', '1988', '1989', '1990'], + ['1991', '1992', '1993', '1994', '1995'], + ['1996', '1997', '1998', '1999', '2000'] + ]); + expectSelectedElement( null, null ); }); it('moves to the next year set when `next` button is clicked', function() { @@ -368,22 +425,21 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('2021 - 2040'); expect(getLabels()).toEqual([]); - expect(getOptions(0)).toEqual(['2021', '2022', '2023', '2024', '2025']); - expect(getOptions(1)).toEqual(['2026', '2027', '2028', '2029', '2030']); - expect(getOptions(2)).toEqual(['2031', '2032', '2033', '2034', '2035']); - expect(getOptions(3)).toEqual(['2036', '2037', '2038', '2039', '2040']); - - for (var i = 0; i < 4; i ++) { - for (var j = 0; j < 5; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( false ); - } - } + expect(getOptions()).toEqual([ + ['2021', '2022', '2023', '2024', '2025'], + ['2026', '2027', '2028', '2029', '2030'], + ['2031', '2032', '2033', '2034', '2035'], + ['2036', '2037', '2038', '2039', '2040'] + ]); + + expectSelectedElement( null, null ); }); }); describe('attribute `starting-day`', function () { beforeEach(function() { - element = $compile('')($rootScope); + $rootScope.startingDay = 1; + element = $compile('')($rootScope); $rootScope.$digest(); }); @@ -392,11 +448,13 @@ describe('datepicker directive', function () { }); it('renders the calendar days correctly', function() { - expect(getOptions(0)).toEqual(['30', '31', '01', '02', '03', '04', '05']); - expect(getOptions(1)).toEqual(['06', '07', '08', '09', '10', '11', '12']); - expect(getOptions(2)).toEqual(['13', '14', '15', '16', '17', '18', '19']); - expect(getOptions(3)).toEqual(['20', '21', '22', '23', '24', '25', '26']); - expect(getOptions(4)).toEqual(['27', '28', '29', '30', '01', '02', '03']); + expect(getOptions()).toEqual([ + ['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'] + ]); }); it('renders the week numbers correctly', function() { @@ -408,7 +466,7 @@ describe('datepicker directive', function () { var weekHeader, weekElement; beforeEach(function() { $rootScope.showWeeks = false; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); weekHeader = getLabelsRow().find('th').eq(0); @@ -439,14 +497,14 @@ describe('datepicker directive', function () { describe('min attribute', function () { beforeEach(function() { $rootScope.mindate = new Date("September 12, 2010"); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); }); it('disables appropriate days in current month', function() { for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( (i < 2) ); + expect(isDisabledOption(i, j)).toBe( (i < 2) ); } } }); @@ -456,16 +514,24 @@ describe('datepicker directive', function () { $rootScope.$digest(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( (i < 1) ); + expect(isDisabledOption(i, j)).toBe( (i < 1) ); } } }); + it('invalidates when model is a disabled date', function() { + $rootScope.mindate = new Date("September 5, 2010"); + $rootScope.date = new Date("September 2, 2010"); + $rootScope.$digest(); + expect(element.hasClass('ng-invalid')).toBeTruthy(); + expect(element.hasClass('ng-invalid-date-disabled')).toBeTruthy(); + }); + it('disables all days in previous month', function() { clickPreviousButton(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( true ); + expect(isDisabledOption(i, j)).toBe( true ); } } }); @@ -474,7 +540,7 @@ describe('datepicker directive', function () { clickNextButton(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( false ); + expect(isDisabledOption(i, j)).toBe( false ); } } }); @@ -483,7 +549,7 @@ describe('datepicker directive', function () { clickTitleButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( (i < 2 || (i === 2 && j < 2)) ); + expect(isDisabledOption(i, j)).toBe( (i < 2 || (i === 2 && j < 2)) ); } } }); @@ -493,7 +559,7 @@ describe('datepicker directive', function () { clickPreviousButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( true ); + expect(isDisabledOption(i, j)).toBe( true ); } } }); @@ -503,7 +569,7 @@ describe('datepicker directive', function () { clickNextButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( false ); + expect(isDisabledOption(i, j)).toBe( false ); } } }); @@ -516,7 +582,7 @@ describe('datepicker directive', function () { clickTitleButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( false ); + expect(isDisabledOption(i, j)).toBe( false ); } } }); @@ -526,14 +592,14 @@ describe('datepicker directive', function () { describe('max attribute', function () { beforeEach(function() { $rootScope.maxdate = new Date("September 25, 2010"); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); }); it('disables appropriate days in current month', function() { for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( (i === 4) ); + expect(isDisabledOption(i, j)).toBe( (i === 4) ); } } }); @@ -543,16 +609,23 @@ describe('datepicker directive', function () { $rootScope.$digest(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( (i > 2) ); + expect(isDisabledOption(i, j)).toBe( (i > 2) ); } } }); + it('invalidates when model is a disabled date', function() { + $rootScope.maxdate = new Date("September 18, 2010"); + $rootScope.$digest(); + expect(element.hasClass('ng-invalid')).toBeTruthy(); + expect(element.hasClass('ng-invalid-date-disabled')).toBeTruthy(); + }); + it('disables no days in previous month', function() { clickPreviousButton(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( false ); + expect(isDisabledOption(i, j)).toBe( false ); } } }); @@ -561,7 +634,7 @@ describe('datepicker directive', function () { clickNextButton(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( true ); + expect(isDisabledOption(i, j)).toBe( true ); } } }); @@ -570,7 +643,7 @@ describe('datepicker directive', function () { clickTitleButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( (i > 2 || (i === 2 && j > 2)) ); + expect(isDisabledOption(i, j)).toBe( (i > 2 || (i === 2 && j > 2)) ); } } }); @@ -580,7 +653,7 @@ describe('datepicker directive', function () { clickPreviousButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( false ); + expect(isDisabledOption(i, j)).toBe( false ); } } }); @@ -590,7 +663,7 @@ describe('datepicker directive', function () { clickNextButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( true ); + expect(isDisabledOption(i, j)).toBe( true ); } } }); @@ -600,7 +673,7 @@ describe('datepicker directive', function () { $rootScope.$digest(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( false ); + expect(isDisabledOption(i, j)).toBe( false ); } } }); @@ -609,28 +682,31 @@ describe('datepicker directive', function () { describe('date-disabled expression', function () { beforeEach(function() { $rootScope.dateDisabledHandler = jasmine.createSpy('dateDisabledHandler'); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); }); - it('executes the dateDisabled expression for each visible date', function() { - expect($rootScope.dateDisabledHandler.calls.length).toEqual(35); + it('executes the dateDisabled expression for each visible day plus one for validation', function() { + expect($rootScope.dateDisabledHandler.calls.length).toEqual(35 + 1); }); - it('executes the dateDisabled expression for each visible date & each month when mode changes', function() { + it('executes the dateDisabled expression for each visible month plus one for validation', function() { + $rootScope.dateDisabledHandler.reset(); clickTitleButton(); - expect($rootScope.dateDisabledHandler.calls.length).toEqual(35 + 12); + expect($rootScope.dateDisabledHandler.calls.length).toEqual(12 + 1); }); - it('executes the dateDisabled expression for each visible date, month & year when mode changes', function() { - clickTitleButton(2); - expect($rootScope.dateDisabledHandler.calls.length).toEqual(35 + 12 + 20); + it('executes the dateDisabled expression for each visible year plus one for validation', function() { + clickTitleButton(); + $rootScope.dateDisabledHandler.reset(); + clickTitleButton(); + expect($rootScope.dateDisabledHandler.calls.length).toEqual(20 + 1); }); }); describe('formatting attributes', function () { beforeEach(function() { - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); }); @@ -642,18 +718,22 @@ describe('datepicker directive', function () { clickTitleButton(); expect(getTitle()).toBe('10'); - expect(getOptions(0)).toEqual(['Jan', 'Feb', 'Mar']); - expect(getOptions(1)).toEqual(['Apr', 'May', 'Jun']); - expect(getOptions(2)).toEqual(['Jul', 'Aug', 'Sep']); - expect(getOptions(3)).toEqual(['Oct', 'Nov', 'Dec']); + expect(getOptions()).toEqual([ + ['Jan', 'Feb', 'Mar'], + ['Apr', 'May', 'Jun'], + ['Jul', 'Aug', 'Sep'], + ['Oct', 'Nov', 'Dec'] + ]); }); it('changes the title, year format & range in `year` mode', function() { clickTitleButton(2); expect(getTitle()).toBe('01 - 10'); - expect(getOptions(0)).toEqual(['01', '02', '03', '04', '05']); - expect(getOptions(1)).toEqual(['06', '07', '08', '09', '10']); + expect(getOptions()).toEqual([ + ['01', '02', '03', '04', '05'], + ['06', '07', '08', '09', '10'] + ]); }); it('shows day labels', function() { @@ -661,15 +741,19 @@ describe('datepicker directive', function () { }); it('changes the day format', function() { - expect(getOptions(0)).toEqual(['29', '30', '31', '1', '2', '3', '4']); - expect(getOptions(1)).toEqual(['5', '6', '7', '8', '9', '10', '11']); - expect(getOptions(4)).toEqual(['26', '27', '28', '29', '30', '1', '2']); + expect(getOptions()).toEqual([ + ['29', '30', '31', '1', '2', '3', '4'], + ['5', '6', '7', '8', '9', '10', '11'], + ['12', '13', '14', '15', '16', '17', '18'], + ['19', '20', '21', '22', '23', '24', '25'], + ['26', '27', '28', '29', '30', '1', '2'] + ]); }); }); describe('setting datepickerConfig', function() { var originalConfig = {}; - beforeEach(inject(function(_$compile_, _$rootScope_, datepickerConfig) { + beforeEach(inject(function(datepickerConfig) { angular.extend(originalConfig, datepickerConfig); datepickerConfig.startingDay = 6; datepickerConfig.showWeeks = false; @@ -681,7 +765,7 @@ describe('datepicker directive', function () { datepickerConfig.dayTitleFormat = 'MMMM, yy'; datepickerConfig.monthTitleFormat = 'yy'; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); afterEach(inject(function(datepickerConfig) { @@ -689,7 +773,7 @@ describe('datepicker directive', function () { angular.extend(datepickerConfig, originalConfig); })); - it('changes the title format in day mode', function() { + it('changes the title format in `day` mode', function() { expect(getTitle()).toBe('September, 10'); }); @@ -697,35 +781,319 @@ describe('datepicker directive', function () { clickTitleButton(); expect(getTitle()).toBe('10'); - expect(getOptions(0)).toEqual(['Jan', 'Feb', 'Mar']); - expect(getOptions(1)).toEqual(['Apr', 'May', 'Jun']); - expect(getOptions(2)).toEqual(['Jul', 'Aug', 'Sep']); - expect(getOptions(3)).toEqual(['Oct', 'Nov', 'Dec']); + expect(getOptions()).toEqual([ + ['Jan', 'Feb', 'Mar'], + ['Apr', 'May', 'Jun'], + ['Jul', 'Aug', 'Sep'], + ['Oct', 'Nov', 'Dec'] + ]); }); it('changes the title, year format & range in `year` mode', function() { clickTitleButton(2); expect(getTitle()).toBe('01 - 10'); - expect(getOptions(0)).toEqual(['01', '02', '03', '04', '05']); - expect(getOptions(1)).toEqual(['06', '07', '08', '09', '10']); + expect(getOptions()).toEqual([ + ['01', '02', '03', '04', '05'], + ['06', '07', '08', '09', '10'] + ]); }); it('changes the `starting-day` & day headers & format', function() { expect(getLabels()).toEqual(['Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']); - expect(getOptions(0)).toEqual(['28', '29', '30', '31', '1', '2', '3']); - expect(getOptions(1)).toEqual(['4', '5', '6', '7', '8', '9', '10']); - expect(getOptions(4)).toEqual(['25', '26', '27', '28', '29', '30', '1']); + expect(getOptions()).toEqual([ + ['28', '29', '30', '31', '1', '2', '3'], + ['4', '5', '6', '7', '8', '9', '10'], + ['11', '12', '13', '14', '15', '16', '17'], + ['18', '19', '20', '21', '22', '23', '24'], + ['25', '26', '27', '28', '29', '30', '1'] + ]); }); it('changes initial visibility for weeks', function() { expect(getLabelsRow().find('th').eq(0).css('display')).toBe('none'); + var tr = element.find('tbody').find('tr'); for (var i = 0; i < 5; i++) { - expect(element.find('tbody').find('tr').eq(i).find('td').eq(0).css('display')).toBe('none'); + expect(tr.eq(i).find('td').eq(0).css('display')).toBe('none'); } }); }); + + describe('controller', function () { + var ctrl, $attrs; + beforeEach(inject(function($controller) { + $rootScope.dateDisabled = null; + $attrs = {}; + ctrl = $controller('DatepickerController', { $scope: $rootScope, $attrs: $attrs }); + })); + + describe('modes', function() { + var currentMode; + + it('to be an array', function() { + expect(ctrl.modes.length).toBe(3); + }); + + describe('`day`', function() { + beforeEach(inject(function() { + currentMode = ctrl.modes[0]; + })); + + it('has the appropriate name', function() { + expect(currentMode.name).toBe('day'); + }); + + it('returns the correct date objects', function() { + var objs = currentMode.getVisibleDates(new Date('September 1, 2010'), new Date('September 30, 2010')).objects; + expect(objs.length).toBe(35); + expect(objs[1].selected).toBeFalsy(); + expect(objs[32].selected).toBeTruthy(); + }); + + it('can compare two dates', function() { + expect(currentMode.compare(new Date('September 30, 2010'), new Date('September 1, 2010'))).toBeGreaterThan(0); + expect(currentMode.compare(new Date('September 1, 2010'), new Date('September 30, 2010'))).toBeLessThan(0); + expect(currentMode.compare(new Date('September 30, 2010 15:30:00'), new Date('September 30, 2010 20:30:00'))).toBe(0); + }); + }); + + describe('`month`', function() { + beforeEach(inject(function() { + currentMode = ctrl.modes[1]; + })); + + it('has the appropriate name', function() { + expect(currentMode.name).toBe('month'); + }); + + it('returns the correct date objects', function() { + var objs = currentMode.getVisibleDates(new Date('September 1, 2010'), new Date('September 30, 2010')).objects; + expect(objs.length).toBe(12); + expect(objs[1].selected).toBeFalsy(); + expect(objs[8].selected).toBeTruthy(); + }); + + it('can compare two dates', function() { + expect(currentMode.compare(new Date('October 30, 2010'), new Date('September 01, 2010'))).toBeGreaterThan(0); + expect(currentMode.compare(new Date('September 01, 2010'), new Date('October 30, 2010'))).toBeLessThan(0); + expect(currentMode.compare(new Date('September 01, 2010'), new Date('September 30, 2010'))).toBe(0); + }); + }); + + describe('`year`', function() { + beforeEach(inject(function() { + currentMode = ctrl.modes[2]; + })); + + it('has the appropriate name', function() { + expect(currentMode.name).toBe('year'); + }); + + it('returns the correct date objects', function() { + var objs = currentMode.getVisibleDates(new Date('September 1, 2010'), new Date('September 01, 2010')).objects; + expect(objs.length).toBe(20); + expect(objs[1].selected).toBeFalsy(); + expect(objs[9].selected).toBeTruthy(); + }); + + it('can compare two dates', function() { + expect(currentMode.compare(new Date('September 1, 2011'), new Date('October 30, 2010'))).toBeGreaterThan(0); + expect(currentMode.compare(new Date('October 30, 2010'), new Date('September 1, 2011'))).toBeLessThan(0); + expect(currentMode.compare(new Date('November 9, 2010'), new Date('September 30, 2010'))).toBe(0); + }); + }); + }); + + describe('`isDisabled` function', function() { + var date = new Date("September 30, 2010 15:30:00"); + + it('to return false if no limit is set', function() { + expect(ctrl.isDisabled(date, 0)).toBeFalsy(); + }); + + it('to handle correctly the `min` date', function() { + ctrl.minDate = new Date('October 1, 2010'); + expect(ctrl.isDisabled(date, 0)).toBeTruthy(); + expect(ctrl.isDisabled(date)).toBeTruthy(); + + ctrl.minDate = new Date('September 1, 2010'); + expect(ctrl.isDisabled(date, 0)).toBeFalsy(); + }); + + it('to handle correctly the `max` date', function() { + ctrl.maxDate = new Date('October 1, 2010'); + expect(ctrl.isDisabled(date, 0)).toBeFalsy(); + + ctrl.maxDate = new Date('September 1, 2010'); + expect(ctrl.isDisabled(date, 0)).toBeTruthy(); + expect(ctrl.isDisabled(date)).toBeTruthy(); + }); + + it('to handle correctly the scope `dateDisabled` expression', function() { + $rootScope.dateDisabled = function() { + return false; + }; + $rootScope.$digest(); + expect(ctrl.isDisabled(date, 0)).toBeFalsy(); + + $rootScope.dateDisabled = function() { + return true; + }; + $rootScope.$digest(); + expect(ctrl.isDisabled(date, 0)).toBeTruthy(); + }); + }); + }); + + describe('as popup', function () { + var divElement, inputEl, dropdownEl, changeInputValueTo, $document; + + function assignElements(wrapElement) { + inputEl = wrapElement.find('input'); + dropdownEl = wrapElement.find('ul'); + element = dropdownEl.find('table'); + } + + beforeEach(inject(function(_$document_, $sniffer) { + $document = _$document_; + $rootScope.date = new Date("September 30, 2010 15:30:00"); + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + + changeInputValueTo = function (el, value) { + el.val(value); + el.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); + $rootScope.$digest(); + }; + })); + + it('to display the correct value in input', function() { + expect(inputEl.val()).toBe('2010-09-30'); + }); + + it('does not to display datepicker initially', function() { + expect(dropdownEl.css('display')).toBe('none'); + }); + + it('displays datepicker on input focus', function() { + inputEl.focus(); + expect(dropdownEl.css('display')).not.toBe('none'); + }); + + it('renders the calendar correctly', function() { + expect(getLabelsRow().css('display')).not.toBe('none'); + expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); + expect(getOptions()).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'] + ]); + }); + + it('updates the input when a day is clicked', function() { + clickOption(2, 3); + expect(inputEl.val()).toBe('2010-09-15'); + 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('1983-01-10'); + }); + + it('closes the dropdown when a day is clicked', function() { + inputEl.focus(); + expect(dropdownEl.css('display')).not.toBe('none'); + + clickOption(2, 3); + expect(dropdownEl.css('display')).toBe('none'); + }); + + it('updates the model when input value changes', function() { + changeInputValueTo(inputEl, 'March 5, 1980'); + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(2); + expect($rootScope.date.getDate()).toEqual(5); + }); + + it('closes when click outside of calendar', function() { + $document.find('body').click(); + expect(dropdownEl.css('display')).toBe('none'); + }); + + describe('toggles programatically by `open` attribute', function () { + beforeEach(inject(function() { + $rootScope.open = true; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('to display initially', function() { + expect(dropdownEl.css('display')).not.toBe('none'); + }); + + it('to close / open from scope variable', function() { + expect(dropdownEl.css('display')).not.toBe('none'); + $rootScope.open = false; + $rootScope.$digest(); + expect(dropdownEl.css('display')).toBe('none'); + + $rootScope.open = true; + $rootScope.$digest(); + expect(dropdownEl.css('display')).not.toBe('none'); + }); + }); + + 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(2, 3); + 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('use with ng-required directive', function() { + beforeEach(inject(function() { + $rootScope.date = ''; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('should be invalid initially', 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(); + }); + }); + + }); + }); describe('datepicker directive with empty initial state', function () { @@ -736,7 +1104,7 @@ describe('datepicker directive with empty initial state', function () { $compile = _$compile_; $rootScope = _$rootScope_; $rootScope.date = null; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); diff --git a/template/datepicker/datepicker.html b/template/datepicker/datepicker.html index 708f4f7577..bc54142737 100644 --- a/template/datepicker/datepicker.html +++ b/template/datepicker/datepicker.html @@ -1,4 +1,4 @@ -
+
@@ -14,7 +14,7 @@ diff --git a/template/datepicker/popup.html b/template/datepicker/popup.html new file mode 100644 index 0000000000..77b04a263e --- /dev/null +++ b/template/datepicker/popup.html @@ -0,0 +1,12 @@ + \ No newline at end of file From ceb396f71652730405c7ac0d07abf16262ff88eb Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sun, 4 Aug 2013 17:31:19 +0200 Subject: [PATCH 0032/1761] chore(release): v0.5.0 --- CHANGELOG.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 135f2ac659..e22ed72603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,86 @@ +# 0.5.0 (2013-08-04) + +## Features + +- **buttons:** + - support dynamic true / false values in btn-checkbox ([3e30cd94](http://github.com/angular-ui/bootstrap/commit/3e30cd94)) +- **datepikcer:** + - `ngModelController` plug & new `datepikcerPopup` ([dab18336](http://github.com/angular-ui/bootstrap/commit/dab18336)) +- **rating:** + - added onHover and onLeave. ([5b1115e3](http://github.com/angular-ui/bootstrap/commit/5b1115e3)) +- **tabs:** + - added onDeselect callback, used similarly as onSelect ([fe47c9bb](http://github.com/angular-ui/bootstrap/commit/fe47c9bb)) + - add the ability to set the direction of the tabs ([220e7b60](http://github.com/angular-ui/bootstrap/commit/220e7b60)) +- **typeahead:** + - support custom templates for matched items ([e2238174](http://github.com/angular-ui/bootstrap/commit/e2238174)) + - expose index to custom templates ([5ffae83d](http://github.com/angular-ui/bootstrap/commit/5ffae83d)) + +## Bug Fixes + +- **datepicker:** + - handle correctly `min`/`max` when cleared ([566bdd16](http://github.com/angular-ui/bootstrap/commit/566bdd16)) + - add type attribute for buttons ([25caf5fb](http://github.com/angular-ui/bootstrap/commit/25caf5fb)) +- **pagination:** + - handle `currentPage` number as string ([b1fa7bb8](http://github.com/angular-ui/bootstrap/commit/b1fa7bb8)) + - use interpolation for text attributes ([f45815cb](http://github.com/angular-ui/bootstrap/commit/f45815cb)) +- **popover:** + - don't unbind event handlers created by other directives ([56f624a2](http://github.com/angular-ui/bootstrap/commit/56f624a2)) + - correctly position popovers appended to body ([93a82af0](http://github.com/angular-ui/bootstrap/commit/93a82af0)) +- **rating:** + - evaluate `max` attribute on parent scope ([60619d51](http://github.com/angular-ui/bootstrap/commit/60619d51)) +- **tabs:** + - make tab contents be correctly connected to parent (#524) ([be7ecff0](http://github.com/angular-ui/bootstrap/commit/be7ecff0)) + - Make tabset template correctly use tabset attributes (#584) ([8868f236](http://github.com/angular-ui/bootstrap/commit/8868f236)) + - fix tab content compiling wrong (Closes #599, #631, #574) ([224bc2f5](http://github.com/angular-ui/bootstrap/commit/224bc2f5)) + - make tabs added with active=true be selected ([360cd5ca](http://github.com/angular-ui/bootstrap/commit/360cd5ca)) + - if tab is active at start, always select it ([ba1f741d](http://github.com/angular-ui/bootstrap/commit/ba1f741d)) +- **timepicker:** + - prevent date change ([ee741707](http://github.com/angular-ui/bootstrap/commit/ee741707)) + - added wheel event to enable mousewheel on Firefox ([8dc92afa](http://github.com/angular-ui/bootstrap/commit/8dc92afa)) +- **tooltip:** + - fix positioning inside scrolling element ([63ae7e12](http://github.com/angular-ui/bootstrap/commit/63ae7e12)) + - triggers should be local to tooltip instances ([58e8ef4f](http://github.com/angular-ui/bootstrap/commit/58e8ef4f)) + - correctly handle initial events unbinding ([4fd5bf43](http://github.com/angular-ui/bootstrap/commit/4fd5bf43)) + - bind correct 'hide' event handler ([d50b0547](http://github.com/angular-ui/bootstrap/commit/d50b0547)) +- **typeahead:** + - play nicelly with existing formatters ([d2df0b35](http://github.com/angular-ui/bootstrap/commit/d2df0b35)) + - properly render initial input value ([c4e169cb](http://github.com/angular-ui/bootstrap/commit/c4e169cb)) + - separate text field rendering and drop down rendering ([ea1e858a](http://github.com/angular-ui/bootstrap/commit/ea1e858a)) + - fixed waitTime functionality ([90a8aa79](http://github.com/angular-ui/bootstrap/commit/90a8aa79)) + - correctly close popup on match selection ([624fd5f5](http://github.com/angular-ui/bootstrap/commit/624fd5f5)) + +## Breaking Changes + +- **pagination:** + The 'first-text', 'previous-text', 'next-text' and 'last-text' + attributes are now interpolated. + + To migrate your code, remove quotes for constant attributes and/or + interpolate scope variables. + + Before: + +```html + +``` + and/or + +```html + $scope.var1 = '<<'; + +``` + After: + +```html + +``` + and/or + +```html + $scope.var1 = '<<'; + +``` + # 0.4.0 (2013-06-24) ## Features diff --git a/package.json b/package.json index 21360dddee..cdb9a17168 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "https://github.com/angular-ui/bootstrap/graphs/contributors", "name": "angular-ui-bootstrap", - "version": "0.5.0-SNAPSHOT", + "version": "0.5.0", "dependencies": {}, "devDependencies": { "grunt": "~0.4.1", From 290b268384e297b68555c54e491abb84f8f59b8a Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sun, 4 Aug 2013 18:29:29 +0200 Subject: [PATCH 0033/1761] chore(release): Starting v0.6.0-SNAPSHOT --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cdb9a17168..9e88e7df57 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "https://github.com/angular-ui/bootstrap/graphs/contributors", "name": "angular-ui-bootstrap", - "version": "0.5.0", + "version": "0.6.0-SNAPSHOT", "dependencies": {}, "devDependencies": { "grunt": "~0.4.1", From f6aa26bd07f662038400c2137cf886ffb077ea64 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Mon, 5 Aug 2013 19:12:33 +0200 Subject: [PATCH 0034/1761] demo(datepicker): fix tag name in an example --- src/datepicker/docs/demo.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datepicker/docs/demo.html b/src/datepicker/docs/demo.html index 526b2ab62b..12ed9b5927 100644 --- a/src/datepicker/docs/demo.html +++ b/src/datepicker/docs/demo.html @@ -2,7 +2,7 @@
Selected date is: {{dt | date:'fullDate' }}
- +
From d474824b7ce666b0663be5cf348ac5c8cd4ecb6d Mon Sep 17 00:00:00 2001 From: Dan Wahlin Date: Sun, 4 Aug 2013 20:14:12 -0700 Subject: [PATCH 0035/1761] fix(datepicker): correctly manage focus without jQuery present The element.focus() will throw an error since the object needs to be unwrapped first. Should be: element[0].focus() at a minimum to unwrap the jqlite object since it doesn't expose focus(). Closes #758 --- src/datepicker/datepicker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index 2b89878a28..68dc7d74d3 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -391,7 +391,7 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon updatePosition(); $document.bind('click', documentClickBind); element.unbind('focus', elementFocusBind); - element.focus(); + element[0].focus(); } else { $document.unbind('click', documentClickBind); element.bind('focus', elementFocusBind); @@ -427,4 +427,4 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon }); } }; -}]); \ No newline at end of file +}]); From a89be345762cd71183fd5e0dc2c8143b26b8b47f Mon Sep 17 00:00:00 2001 From: Sergey R Date: Tue, 6 Aug 2013 19:35:27 +0400 Subject: [PATCH 0036/1761] docs(CHANGELOG): typo fix --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e22ed72603..64c18aff37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ - **buttons:** - support dynamic true / false values in btn-checkbox ([3e30cd94](http://github.com/angular-ui/bootstrap/commit/3e30cd94)) -- **datepikcer:** - - `ngModelController` plug & new `datepikcerPopup` ([dab18336](http://github.com/angular-ui/bootstrap/commit/dab18336)) +- **datepicker:** + - `ngModelController` plug & new `datepickerPopup` ([dab18336](http://github.com/angular-ui/bootstrap/commit/dab18336)) - **rating:** - added onHover and onLeave. ([5b1115e3](http://github.com/angular-ui/bootstrap/commit/5b1115e3)) - **tabs:** From 7fdf73016b09a89651759582c7a1bb1d3741964d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20DECOOL?= Date: Tue, 6 Aug 2013 00:50:12 +0200 Subject: [PATCH 0037/1761] docs(typeahead): document all allowed attributes --- src/typeahead/docs/readme.md | 40 +++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/typeahead/docs/readme.md b/src/typeahead/docs/readme.md index 22fcf0ba8e..6c6ce507e1 100644 --- a/src/typeahead/docs/readme.md +++ b/src/typeahead/docs/readme.md @@ -9,4 +9,42 @@ It is very well integrated into the AngularJS as it uses subset of the The `sourceArray` expression can use a special `$viewValue` variable that corresponds to a value entered inside input by a user. -Also this directive works with promises and it means that you can retrieve matches using the `$http` service with minimal effort. \ No newline at end of file +Also this directive works with promises and it means that you can retrieve matches using the `$http` service with minimal effort. + +The typeahead directives provide several attributes: + +* `ng-model` + : + Assignable angular expression to data-bind to + +* `typeahead` + : + Comprehension Angular expression (see [select directive](http://docs.angularjs.org/api/ng.directive:select)) + +* `typeahead-editable` + _(Defaults: false)_ : + Should it restrict model values to the ones selected from the popup only ? + +* `typeahead-input-formatter` + _(Defaults: undefined)_ : + Format the ng-model result after selection + +* `typeahead-loading` + _(Defaults: angular.noop)_ : + Binding to a variable that indicates if matches are being retrieved asynchronously + +* `typeahead-min-length` + _(Defaults: 1)_ : + Minimal no of characters that needs to be entered before typeahead kicks-in + +* `typeahead-on-select` + _(Defaults: null)_ : + A callback executed when a match is selected + +* `typeahead-template-url` + : + Set custom item template + +* `typeahead-wait-ms` + _(Defaults: 0)_ : + Minimal wait time after last character typed before typehead kicks-in From aac4a0dd4a0ea49d47c1433452eff7a1b260a568 Mon Sep 17 00:00:00 2001 From: Arnaud Lachaume Date: Thu, 8 Aug 2013 23:39:24 +1000 Subject: [PATCH 0038/1761] fix(tabs): add DI array-style annotations --- src/tabs/tabs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tabs/tabs.js b/src/tabs/tabs.js index 125c7c3a89..ab7f0b4263 100644 --- a/src/tabs/tabs.js +++ b/src/tabs/tabs.js @@ -293,7 +293,7 @@ function($parse, $http, $templateCache, $compile) { } }]) -.directive('tabsetTitles', function($http) { +.directive('tabsetTitles', ['$http', function($http) { return { restrict: 'A', require: '^tabset', @@ -310,7 +310,7 @@ function($parse, $http, $templateCache, $compile) { } } }; -}) +}]) ; From 64df05e367ad5a1647785f56f6d464a271a01a47 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Thu, 8 Aug 2013 19:24:01 +0200 Subject: [PATCH 0039/1761] docs(typeahead): correct default value for the editable attribute --- src/typeahead/docs/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typeahead/docs/readme.md b/src/typeahead/docs/readme.md index 6c6ce507e1..db0ea04ab4 100644 --- a/src/typeahead/docs/readme.md +++ b/src/typeahead/docs/readme.md @@ -22,7 +22,7 @@ The typeahead directives provide several attributes: Comprehension Angular expression (see [select directive](http://docs.angularjs.org/api/ng.directive:select)) * `typeahead-editable` - _(Defaults: false)_ : + _(Defaults: true)_ : Should it restrict model values to the ones selected from the popup only ? * `typeahead-input-formatter` From 5de71216de5e93af165f8b4477278914823fa3c3 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sat, 10 Aug 2013 12:02:20 +0200 Subject: [PATCH 0040/1761] fix(typeahead): fix label rendering for equal model and items names --- src/typeahead/test/typeahead.spec.js | 8 ++++++++ src/typeahead/typeahead.js | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index f942ed909e..b1cee67618 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -338,6 +338,14 @@ describe('typeahead tests', function () { var inputEl = findInput(prepareInputEl("
")); expect(inputEl.val()).toEqual(''); }); + + it('issue 786 - name of internal model should not conflict with scope model name', function () { + $scope.state = $scope.states[0]; + var element = prepareInputEl("
"); + var inputEl = findInput(element); + + expect(inputEl.val()).toEqual('Alaska'); + }); }); describe('input formatting', function () { diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 4274b0a00c..f03e7383bc 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -172,12 +172,13 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) return inputFormatter(originalScope, locals); } else { - locals[parserResult.itemName] = modelValue; //it might happen that we don't have enough info to properly render input value //we need to check for this situation and simply return model value if we can't apply custom formatting + locals[parserResult.itemName] = modelValue; candidateViewValue = parserResult.viewMapper(originalScope, locals); - emptyViewValue = parserResult.viewMapper(originalScope, {}); + locals[parserResult.itemName] = undefined; + emptyViewValue = parserResult.viewMapper(originalScope, locals); return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; } From b08e993fb472f386c69b713d89c466ca642b1ffe Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Tue, 23 Jul 2013 22:58:25 +0300 Subject: [PATCH 0041/1761] feat(timepicker): plug into `ngModel` controller Closes #773 Closes #785 --- misc/demo/assets/demo.css | 4 + src/timepicker/docs/demo.html | 4 +- src/timepicker/docs/demo.js | 4 + src/timepicker/test/timepicker.spec.js | 142 +++++++++++++++++++-- src/timepicker/timepicker.js | 167 ++++++++++++++----------- template/timepicker/timepicker.html | 4 +- 6 files changed, 235 insertions(+), 90 deletions(-) diff --git a/misc/demo/assets/demo.css b/misc/demo/assets/demo.css index 7c58d86d10..ddad9986b5 100644 --- a/misc/demo/assets/demo.css +++ b/misc/demo/assets/demo.css @@ -9,6 +9,10 @@ body { opacity: 0; } +.ng-invalid { + border: 1px solid red !important; +} + section { padding-top: 30px; } diff --git a/src/timepicker/docs/demo.html b/src/timepicker/docs/demo.html index 172e4b6888..9fdf7452cb 100644 --- a/src/timepicker/docs/demo.html +++ b/src/timepicker/docs/demo.html @@ -1,5 +1,7 @@
- +
+ +
Time is: {{mytime | date:'shortTime' }}
diff --git a/src/timepicker/docs/demo.js b/src/timepicker/docs/demo.js index 1c456b855c..478545b5be 100644 --- a/src/timepicker/docs/demo.js +++ b/src/timepicker/docs/demo.js @@ -21,6 +21,10 @@ var TimepickerDemoCtrl = function ($scope) { $scope.mytime = d; }; + $scope.changed = function () { + console.log('Time changed to: ' + $scope.mytime); + }; + $scope.clear = function() { $scope.mytime = null; }; diff --git a/src/timepicker/test/timepicker.spec.js b/src/timepicker/test/timepicker.spec.js index 4ceeb3c115..797456724b 100644 --- a/src/timepicker/test/timepicker.spec.js +++ b/src/timepicker/test/timepicker.spec.js @@ -8,7 +8,7 @@ describe('timepicker directive', function () { $rootScope = _$rootScope_; $rootScope.time = newTime(14, 40); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); @@ -82,6 +82,15 @@ describe('timepicker directive', function () { expect(getModelState()).toEqual([14, 40]); }); + it('has `selected` current time when model is initially cleared', function() { + $rootScope.time = null; + element = $compile('')($rootScope); + $rootScope.$digest(); + + expect($rootScope.time).toBe(null); + expect(getTimeState()).not.toEqual(['', '', '']); + }); + it('changes inputs when model changes value', function() { $rootScope.time = newTime(11, 50); $rootScope.$digest(); @@ -235,7 +244,7 @@ describe('timepicker directive', function () { }); it('changes only the time part when minutes change', function() { - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.time = newTime(0, 0); $rootScope.$digest(); @@ -367,7 +376,7 @@ describe('timepicker directive', function () { $rootScope.hstep = 2; $rootScope.mstep = 30; $rootScope.time = newTime(14, 0); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); }); @@ -530,7 +539,7 @@ describe('timepicker directive', function () { beforeEach(function() { $rootScope.meridian = false; $rootScope.time = newTime(14, 10); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); }); @@ -559,6 +568,14 @@ describe('timepicker directive', function () { expect(getModelState()).toEqual([14, 10]); expect(getMeridianTd().css('display')).toBe('none'); }); + + it('handles correctly initially empty model on parent element', function() { + $rootScope.time = null; + element = $compile('')($rootScope); + $rootScope.$digest(); + + expect($rootScope.time).toBe(null); + }); }); describe('setting timepickerConfig steps', function() { @@ -568,7 +585,7 @@ describe('timepicker directive', function () { timepickerConfig.hourStep = 2; timepickerConfig.minuteStep = 10; timepickerConfig.showMeridian = false; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); afterEach(inject(function(timepickerConfig) { @@ -614,7 +631,7 @@ describe('timepicker directive', function () { angular.extend(originalConfig, timepickerConfig); timepickerConfig.meridians = ['π.μ.', 'μ.μ.']; timepickerConfig.showMeridian = true; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); afterEach(inject(function(timepickerConfig) { @@ -637,10 +654,9 @@ describe('timepicker directive', function () { }); describe('user input validation', function () { - var changeInputValueTo; - beforeEach(inject(function(_$compile_, _$rootScope_, $sniffer) { + beforeEach(inject(function($sniffer) { changeInputValueTo = function (inputEl, value) { inputEl.val(value); inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); @@ -661,7 +677,7 @@ describe('timepicker directive', function () { expect(getModelState()).toEqual([14, 40]); }); - it('updates hours & pads on input blur', function() { + it('updates hours & pads on input change & pads on blur', function() { var el = getHoursInputEl(); changeInputValueTo(el, 5); @@ -673,7 +689,7 @@ describe('timepicker directive', function () { expect(getModelState()).toEqual([17, 40]); }); - it('updates minutes & pads on input blur', function() { + it('updates minutes & pads on input change & pads on blur', function() { var el = getMinutesInputEl(); changeInputValueTo(el, 9); @@ -691,6 +707,7 @@ describe('timepicker directive', function () { changeInputValueTo(el, 'pizza'); expect($rootScope.time).toBe(null); expect(el.parent().hasClass('error')).toBe(true); + expect(element.hasClass('ng-invalid-time')).toBe(true); changeInputValueTo(el, 8); el.blur(); @@ -698,6 +715,7 @@ describe('timepicker directive', function () { expect(getTimeState()).toEqual(['08', '40', 'PM']); expect(getModelState()).toEqual([20, 40]); expect(el.parent().hasClass('error')).toBe(false); + expect(element.hasClass('ng-invalid-time')).toBe(false); }); it('clears model when input minutes is invalid & alerts the UI', function() { @@ -706,16 +724,18 @@ describe('timepicker directive', function () { changeInputValueTo(el, 'pizza'); expect($rootScope.time).toBe(null); expect(el.parent().hasClass('error')).toBe(true); + expect(element.hasClass('ng-invalid-time')).toBe(true); changeInputValueTo(el, 22); expect(getTimeState()).toEqual(['02', '22', 'PM']); expect(getModelState()).toEqual([14, 22]); expect(el.parent().hasClass('error')).toBe(false); + expect(element.hasClass('ng-invalid-time')).toBe(false); }); it('handles 12/24H mode change', function() { $rootScope.meridian = true; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); var el = getHoursInputEl(); @@ -723,11 +743,111 @@ describe('timepicker directive', function () { changeInputValueTo(el, '16'); expect($rootScope.time).toBe(null); expect(el.parent().hasClass('error')).toBe(true); + expect(element.hasClass('ng-invalid-time')).toBe(true); $rootScope.meridian = false; $rootScope.$digest(); expect(getTimeState(true)).toEqual(['16', '40']); expect(getModelState()).toEqual([16, 40]); + expect(element.hasClass('ng-invalid-time')).toBe(false); + }); + }); + + describe('when model is not a Date', function() { + beforeEach(inject(function() { + eelement = $compile('')($rootScope); + })); + + it('should not be invalid when the model is null', function() { + $rootScope.time = null; + $rootScope.$digest(); + expect(element.hasClass('ng-invalid-time')).toBe(false); + }); + + it('should not be invalid when the model is undefined', function() { + $rootScope.time = undefined; + $rootScope.$digest(); + expect(element.hasClass('ng-invalid-time')).toBe(false); + }); + + it('should not be invalid when the model is a valid string date representation', function() { + $rootScope.time = 'September 30, 2010 15:30:00'; + $rootScope.$digest(); + expect(element.hasClass('ng-invalid-time')).toBe(false); + expect(getTimeState()).toEqual(['03', '30', 'PM']); + }); + + it('should be invalid when the model is not a valid string date representation', function() { + $rootScope.time = 'pizza'; + $rootScope.$digest(); + expect(element.hasClass('ng-invalid-time')).toBe(true); + }); + + it('should return valid when the model becomes valid', function() { + $rootScope.time = 'pizza'; + $rootScope.$digest(); + expect(element.hasClass('ng-invalid-time')).toBe(true); + + $rootScope.time = new Date(); + $rootScope.$digest(); + expect(element.hasClass('ng-invalid-time')).toBe(false); + }); + + it('should return valid when the model is cleared', function() { + $rootScope.time = 'pizza'; + $rootScope.$digest(); + expect(element.hasClass('ng-invalid-time')).toBe(true); + + $rootScope.time = null; + $rootScope.$digest(); + expect(element.hasClass('ng-invalid-time')).toBe(false); + }); + }); + + describe('use with `ng-required` directive', function() { + beforeEach(inject(function() { + $rootScope.time = null; + element = $compile('')($rootScope); + $rootScope.$digest(); + })); + + it('should be invalid initially', function() { + expect(element.hasClass('ng-invalid')).toBe(true); + }); + + it('should be valid if model has been specified', function() { + $rootScope.time = new Date(); + $rootScope.$digest(); + expect(element.hasClass('ng-invalid')).toBe(false); + }); + }); + + describe('use with `ng-change` directive', function() { + beforeEach(inject(function() { + $rootScope.changeHandler = jasmine.createSpy('changeHandler'); + $rootScope.time = new Date(); + element = $compile('')($rootScope); + $rootScope.$digest(); + })); + + it('should not be called initially', function() { + expect($rootScope.changeHandler).not.toHaveBeenCalled(); + }); + + it('should be called when hours / minutes buttons clicked', function() { + var btn1 = getHoursButton(true); + var btn2 = getMinutesButton(false); + + doClick(btn1, 2); + doClick(btn2, 3); + $rootScope.$digest(); + expect($rootScope.changeHandler.callCount).toBe(5); + }); + + it('should not be called when model changes programatically', function() { + $rootScope.time = new Date(); + $rootScope.$digest(); + expect($rootScope.changeHandler).not.toHaveBeenCalled(); }); }); diff --git a/src/timepicker/timepicker.js b/src/timepicker/timepicker.js index db65378f68..3e6f9064f6 100644 --- a/src/timepicker/timepicker.js +++ b/src/timepicker/timepicker.js @@ -1,14 +1,5 @@ angular.module('ui.bootstrap.timepicker', []) -.filter('pad', function() { - return function(input) { - if ( angular.isDefined(input) && input.toString().length < 2 ) { - input = '0' + input; - } - return input; - }; -}) - .constant('timepickerConfig', { hourStep: 1, minuteStep: 1, @@ -18,16 +9,18 @@ angular.module('ui.bootstrap.timepicker', []) mousewheel: true }) -.directive('timepicker', ['padFilter', '$parse', 'timepickerConfig', function (padFilter, $parse, timepickerConfig) { +.directive('timepicker', ['$parse', '$log', 'timepickerConfig', function ($parse, $log, timepickerConfig) { return { restrict: 'EA', - require:'ngModel', + require:'?^ngModel', replace: true, + scope: {}, templateUrl: 'template/timepicker/timepicker.html', - scope: { - model: '=ngModel' - }, - link: function(scope, element, attrs, ngModelCtrl) { + link: function(scope, element, attrs, ngModel) { + if ( !ngModel ) { + return; // do nothing if no ng-model + } + var selected = new Date(), meridians = timepickerConfig.meridians; var hourStep = timepickerConfig.hourStep; @@ -48,28 +41,27 @@ angular.module('ui.bootstrap.timepicker', []) scope.showMeridian = timepickerConfig.showMeridian; if (attrs.showMeridian) { scope.$parent.$watch($parse(attrs.showMeridian), function(value) { - scope.showMeridian = !! value; - - if ( ! scope.model ) { - // Reset - var dt = new Date( selected ); - var hours = getScopeHours(); - if (angular.isDefined( hours )) { - dt.setHours( hours ); + scope.showMeridian = !!value; + + if ( ngModel.$error.time ) { + // Evaluate from template + var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); + if (angular.isDefined( hours ) && angular.isDefined( minutes )) { + selected.setHours( hours ); + refresh(); } - scope.model = new Date( dt ); } else { - refreshTemplate(); + updateTemplate(); } }); } // Get scope.hours in 24H mode if valid - function getScopeHours ( ) { + function getHoursFromTemplate ( ) { var hours = parseInt( scope.hours, 10 ); var valid = ( scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); if ( !valid ) { - return; + return undefined; } if ( scope.showMeridian ) { @@ -83,14 +75,22 @@ angular.module('ui.bootstrap.timepicker', []) return hours; } + function getMinutesFromTemplate() { + var minutes = parseInt(scope.minutes, 10); + return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; + } + + function pad( value ) { + return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; + } + // Input elements - var inputs = element.find('input'); - var hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1); + var inputs = element.find('input'), hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1); // Respond on mousewheel spin var mousewheel = (angular.isDefined(attrs.mousewheel)) ? scope.$eval(attrs.mousewheel) : timepickerConfig.mousewheel; if ( mousewheel ) { - + var isScrollingUp = function(e) { if (e.originalEvent) { e = e.originalEvent; @@ -99,7 +99,7 @@ angular.module('ui.bootstrap.timepicker', []) var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; return (e.detail || delta > 0); }; - + hoursInputEl.bind('mousewheel wheel', function(e) { scope.$apply( (isScrollingUp(e)) ? scope.incrementHours() : scope.decrementHours() ); e.preventDefault(); @@ -111,50 +111,54 @@ angular.module('ui.bootstrap.timepicker', []) }); } - var keyboardChange = false; scope.readonlyInput = (angular.isDefined(attrs.readonlyInput)) ? scope.$eval(attrs.readonlyInput) : timepickerConfig.readonlyInput; if ( ! scope.readonlyInput ) { + + var invalidate = function(invalidHours, invalidMinutes) { + ngModel.$setViewValue( null ); + ngModel.$setValidity('time', false); + if (angular.isDefined(invalidHours)) { + scope.invalidHours = invalidHours; + } + if (angular.isDefined(invalidMinutes)) { + scope.invalidMinutes = invalidMinutes; + } + }; + scope.updateHours = function() { - var hours = getScopeHours(); + var hours = getHoursFromTemplate(); if ( angular.isDefined(hours) ) { - keyboardChange = 'h'; - if ( scope.model === null ) { - scope.model = new Date( selected ); - } - scope.model.setHours( hours ); + selected.setHours( hours ); + refresh( 'h' ); } else { - scope.model = null; - scope.validHours = false; + invalidate(true); } }; hoursInputEl.bind('blur', function(e) { - if ( scope.validHours && scope.hours < 10) { + if ( !scope.validHours && scope.hours < 10) { scope.$apply( function() { - scope.hours = padFilter( scope.hours ); + scope.hours = pad( scope.hours ); }); } }); scope.updateMinutes = function() { - var minutes = parseInt(scope.minutes, 10); - if ( minutes >= 0 && minutes < 60 ) { - keyboardChange = 'm'; - if ( scope.model === null ) { - scope.model = new Date( selected ); - } - scope.model.setMinutes( minutes ); + var minutes = getMinutesFromTemplate(); + + if ( angular.isDefined(minutes) ) { + selected.setMinutes( minutes ); + refresh( 'm' ); } else { - scope.model = null; - scope.validMinutes = false; + invalidate(undefined, true); } }; minutesInputEl.bind('blur', function(e) { - if ( scope.validMinutes && scope.minutes < 10 ) { + if ( !scope.invalidMinutes && scope.minutes < 10 ) { scope.$apply( function() { - scope.minutes = padFilter( scope.minutes ); + scope.minutes = pad( scope.minutes ); }); } }); @@ -163,38 +167,49 @@ angular.module('ui.bootstrap.timepicker', []) scope.updateMinutes = angular.noop; } - scope.$watch( function getModelTimestamp() { - return +scope.model; - }, function( timestamp ) { - if ( !isNaN( timestamp ) && timestamp > 0 ) { - selected = new Date( timestamp ); - refreshTemplate(); - } - }); + ngModel.$render = function() { + var date = ngModel.$modelValue ? new Date( ngModel.$modelValue ) : null; - function refreshTemplate() { - var hours = selected.getHours(); - if ( scope.showMeridian ) { - // Convert 24 to 12 hour system - hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; + if ( isNaN(date) ) { + ngModel.$setValidity('time', false); + $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + } else { + if ( date ) { + selected = date; + } + makeValid(); + updateTemplate(); } - scope.hours = ( keyboardChange === 'h' ) ? hours : padFilter(hours); - scope.validHours = true; + }; - var minutes = selected.getMinutes(); - scope.minutes = ( keyboardChange === 'm' ) ? minutes : padFilter(minutes); - scope.validMinutes = true; + // Call internally when we know that model is valid. + function refresh( keyboardChange ) { + makeValid(); + ngModel.$setViewValue( new Date(selected) ); + updateTemplate( keyboardChange ); + } - scope.meridian = ( scope.showMeridian ) ? (( selected.getHours() < 12 ) ? meridians[0] : meridians[1]) : ''; + function makeValid() { + ngModel.$setValidity('time', true); + scope.invalidHours = false; + scope.invalidMinutes = false; + } - keyboardChange = false; + function updateTemplate( keyboardChange ) { + var hours = selected.getHours(), minutes = selected.getMinutes(); + + if ( scope.showMeridian ) { + hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system + } + scope.hours = keyboardChange === 'h' ? hours : pad(hours); + scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); + scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; } function addMinutes( minutes ) { var dt = new Date( selected.getTime() + minutes * 60000 ); - selected.setHours( dt.getHours() ); - selected.setMinutes( dt.getMinutes() ); - scope.model = new Date( selected ); + selected.setHours( dt.getHours(), dt.getMinutes() ); + refresh(); } scope.incrementHours = function() { diff --git a/template/timepicker/timepicker.html b/template/timepicker/timepicker.html index 5ef9d0083a..56ac1b5ad7 100644 --- a/template/timepicker/timepicker.html +++ b/template/timepicker/timepicker.html @@ -6,9 +6,9 @@
- + - + From bf30898da27272df75f6c7ff26545ed16ebf1978 Mon Sep 17 00:00:00 2001 From: Swiip Date: Wed, 7 Aug 2013 14:26:34 +0200 Subject: [PATCH 0042/1761] fix(datepicker): compatibility with angular 1.1.5 and no jquery Closes #760 --- src/datepicker/datepicker.js | 15 +++++++++++---- src/datepicker/docs/demo.html | 2 +- src/datepicker/test/datepicker.spec.js | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index 68dc7d74d3..8c0af8eeb8 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -290,8 +290,8 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon ngModel.$parsers.push(parseDate); var getIsOpen, setIsOpen; - if ( attrs.open ) { - getIsOpen = $parse(attrs.open); + if ( attrs.isOpen ) { + getIsOpen = $parse(attrs.isOpen); setIsOpen = getIsOpen.assign; originalScope.$watch(getIsOpen, function updateOpen(value) { @@ -386,15 +386,22 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon scope.position.top = scope.position.top + element.prop('offsetHeight'); } + var documentBindingInitialized = false, elementFocusInitialized = false; scope.$watch('isOpen', function(value) { if (value) { updatePosition(); $document.bind('click', documentClickBind); - element.unbind('focus', elementFocusBind); + if(elementFocusInitialized) { + element.unbind('focus', elementFocusBind); + } element[0].focus(); + documentBindingInitialized = true; } else { - $document.unbind('click', documentClickBind); + if(documentBindingInitialized) { + $document.unbind('click', documentClickBind); + } element.bind('focus', elementFocusBind); + elementFocusInitialized = true; } if ( setIsOpen ) { diff --git a/src/datepicker/docs/demo.html b/src/datepicker/docs/demo.html index 12ed9b5927..423245b515 100644 --- a/src/datepicker/docs/demo.html +++ b/src/datepicker/docs/demo.html @@ -6,7 +6,7 @@
- +
diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index 79ae2c0e10..221c85ad04 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -1029,7 +1029,7 @@ describe('datepicker directive', function () { describe('toggles programatically by `open` attribute', function () { beforeEach(inject(function() { $rootScope.open = true; - var wrapElement = $compile('
')($rootScope); + var wrapElement = $compile('
')($rootScope); $rootScope.$digest(); assignElements(wrapElement); })); From d3da8b78fdf83f1c633f468a24acdab3500bf54c Mon Sep 17 00:00:00 2001 From: Louis Sivillo Date: Mon, 1 Jul 2013 09:34:52 -0400 Subject: [PATCH 0043/1761] fix(dialog): reintroduced dialogOpenClass option This option represents class which is added to the body when the dialog is open. It was present before in twitter bootstrap, then removed in 2.3.0, but then recently reintroduced in 3.0 Closes #798 --- src/dialog/dialog.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dialog/dialog.js b/src/dialog/dialog.js index 0e0413963c..42d63f59e2 100644 --- a/src/dialog/dialog.js +++ b/src/dialog/dialog.js @@ -20,6 +20,7 @@ dialogModule.provider("$dialog", function(){ backdropClass: 'modal-backdrop', transitionClass: 'fade', triggerClass: 'in', + dialogOpenClass: 'modal-open', resolve:{}, backdropFade: false, dialogFade:false, @@ -133,7 +134,7 @@ dialogModule.provider("$dialog", function(){ if(self.options.dialogFade){ self.modalEl.addClass(self.options.triggerClass); } if(self.options.backdropFade){ self.backdropEl.addClass(self.options.triggerClass); } }); - + body.addClass(defaults.dialogOpenClass); self._bindEvents(); }); @@ -191,7 +192,7 @@ dialogModule.provider("$dialog", function(){ Dialog.prototype._onCloseComplete = function(result) { this._removeElementsFromDom(); this._unbindEvents(); - + body.removeClass(defaults.dialogOpenClass); this.deferred.resolve(result); }; From d34f2de189d9a55562d2f0abf449f22042871cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Micha=C5=82owski?= Date: Tue, 13 Aug 2013 23:59:16 +0200 Subject: [PATCH 0044/1761] fix(carousel): correct reflow triggering on FFox and Safari --- src/carousel/carousel.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/carousel/carousel.js b/src/carousel/carousel.js index b4681e80de..cb7d585173 100644 --- a/src/carousel/carousel.js +++ b/src/carousel/carousel.js @@ -1,7 +1,7 @@ /** * @ngdoc overview * @name ui.bootstrap.carousel -* +* * @description * AngularJS version of an image carousel. * @@ -32,10 +32,10 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) } function goNext() { //If we have a slide to transition from and we have a transition type and we're allowed, go - if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { + if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime nextSlide.$element.addClass(direction); - nextSlide.$element[0].offsetWidth = nextSlide.$element[0].offsetWidth; //force reflow + var reflow = nextSlide.$element[0].offsetWidth; //force reflow //Set all other slides to stop doing their stuff for the new transition angular.forEach(slides, function(slide) { @@ -74,7 +74,7 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) $scope.next = function() { var newIndex = (currentIndex + 1) % slides.length; - + //Prevent this user-triggered transition from occurring if there is already one in progress if (!$scope.$currentTransition) { return self.select(slides[newIndex], 'next'); @@ -83,7 +83,7 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) $scope.prev = function() { var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; - + //Prevent this user-triggered transition from occurring if there is already one in progress if (!$scope.$currentTransition) { return self.select(slides[newIndex], 'prev'); @@ -300,7 +300,7 @@ function CarouselDemoCtrl($scope) { var lastValue = scope.active = getActive(scope.$parent); scope.$watch(function parentActiveWatch() { var parentActive = getActive(scope.$parent); - + if (parentActive !== scope.active) { // we are out of sync and need to copy if (parentActive !== lastValue) { From 366e0c8a1ff7fee8eb96e1c317a658957785f457 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Thu, 15 Aug 2013 18:09:30 +0200 Subject: [PATCH 0045/1761] fix(typeahead): set validity flag for non-editable inputs Closes #806 --- src/typeahead/test/typeahead.spec.js | 19 ++++++++++++++++++- src/typeahead/typeahead.js | 14 +++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index b1cee67618..e93d5e18b7 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -163,11 +163,28 @@ describe('typeahead tests', function () { }); it('should support the editable property to limit model bindings to matches only', function () { - var element = prepareInputEl("
"); + var element = prepareInputEl("
ng-model='result' typeahead='item for item in source | filter:$viewValue' typeahead-editable='false'>
"); changeInputValueTo(element, 'not in matches'); expect($scope.result).toEqual(undefined); }); + it('should set validation erros for non-editable inputs', function () { + + var element = prepareInputEl( + "
" + + "" + + "
"); + + changeInputValueTo(element, 'not in matches'); + expect($scope.result).toEqual(undefined); + expect($scope.form.input.$error.editable).toBeTruthy(); + + changeInputValueTo(element, 'foo'); + triggerKeyDown(element, 13); + expect($scope.result).toEqual('foo'); + expect($scope.form.input.$error.editable).toBeFalsy(); + }); + it('should bind loading indicator expression', inject(function ($timeout) { $scope.isLoading = false; diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index f03e7383bc..df55c8b2d4 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -29,7 +29,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) }; }]) - .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { + .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', + function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { var HOT_KEYS = [9, 13, 27, 38, 40]; @@ -158,7 +159,12 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) } } - return isEditable ? inputValue : undefined; + if (isEditable) { + return inputValue; + } else { + modelCtrl.$setValidity('editable', false); + return undefined; + } }); modelCtrl.$formatters.push(function (modelValue) { @@ -192,6 +198,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) locals[parserResult.itemName] = item = scope.matches[activeIdx].model; model = parserResult.modelMapper(originalScope, locals); $setModelValue(originalScope, model); + modelCtrl.$setValidity('editable', true); onSelectCallback(originalScope, { $item: item, @@ -199,8 +206,9 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) $label: parserResult.viewMapper(originalScope, locals) }); - //return focus to the input element if a mach was selected via a mouse click event resetMatches(); + + //return focus to the input element if a mach was selected via a mouse click event element[0].focus(); }; From 8620aedba99b05822311c4529a7877e6f62754d6 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Thu, 15 Aug 2013 19:22:39 +0200 Subject: [PATCH 0046/1761] docs(all): add info about styling cursors for tags Closes #752 Closes #816 --- misc/demo/assets/demo.css | 5 +++++ misc/demo/index.html | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/misc/demo/assets/demo.css b/misc/demo/assets/demo.css index ddad9986b5..8a3ee5c909 100644 --- a/misc/demo/assets/demo.css +++ b/misc/demo/assets/demo.css @@ -69,6 +69,11 @@ section { border-top: 1px solid rgba(255,255,255,0.3); border-bottom: 1px solid rgba(221,221,221,0.3); } + +.nav, .pagination, .carousel a { + cursor: pointer; +} + .bs-docs-social-buttons { margin-left: 0; margin-bottom: 0; diff --git a/misc/demo/index.html b/misc/demo/index.html index 7bdabb1ffe..c779e796f3 100644 --- a/misc/demo/index.html +++ b/misc/demo/index.html @@ -156,6 +156,17 @@

Installation

angular.module('myModule', ['ui.bootstrap']);

You can fork one of the plunkers from this page to see a working example of what is described here.

+

CSS

+

Original Bootstrap's CSS depends on empty href attributes to style cursors for several components (pagination, tabs etc.). + But in AngularJS adding empty href attributes to link tags will cause unwanted route changes. + This is why we need to remove empty href attributes from directive templates and as a result + styling is not applied correctly. The remedy is simple, just add the following styling to your application: + + .nav, .pagination, .carousel a { + cursor: pointer; + } + +

From ce226fa65d5ba45df0658699e495eeec424bfe35 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Fri, 16 Aug 2013 04:09:50 +0300 Subject: [PATCH 0047/1761] demo(all): remove jQuery dependency * Replace select2 in custom build with checkbox buttons. --- misc/demo/assets/app.js | 15 +- misc/demo/assets/select2.css | 524 ------- misc/demo/assets/select2.js | 2407 -------------------------------- misc/demo/assets/select2.png | Bin 613 -> 0 bytes misc/demo/assets/ui-select2.js | 117 -- misc/demo/index.html | 16 +- 6 files changed, 16 insertions(+), 3063 deletions(-) delete mode 100644 misc/demo/assets/select2.css delete mode 100644 misc/demo/assets/select2.js delete mode 100644 misc/demo/assets/select2.png delete mode 100644 misc/demo/assets/ui-select2.js diff --git a/misc/demo/assets/app.js b/misc/demo/assets/app.js index 7a5e7a61d0..1aa4fe3f8e 100644 --- a/misc/demo/assets/app.js +++ b/misc/demo/assets/app.js @@ -1,11 +1,12 @@ -angular.module('bootstrapDemoApp', ['ui.directives', 'ui.bootstrap', 'plunker']); +angular.module('bootstrapDemoApp', ['ui.bootstrap', 'plunker']); -function MainCtrl($scope, $http, orderByFilter) { +function MainCtrl($scope, $http, $document, orderByFilter) { var url = "http://50.116.42.77:3001/"; $scope.selectedModules = []; //iFrame for downloading - var $iframe = $(" - -
  • - -
  • -
  • - - -
  • -
  • - -
    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + + +
    • +
    • + +
      - - -
    • -
    -
    + + +
  • +
    -
    +
    - -
    +
    + +
    + +
    +

    Dependencies

    This repository contains a set of native AngularJS directives based on @@ -127,8 +142,8 @@

    Dependencies

    JavaScript is required. The only required dependencies are:

    Files to download

    @@ -144,63 +159,54 @@

    Files to download

    Installation

    As soon as you've got all the files downloaded and included in your page you just need to declare a dependency on the ui.bootstrap module:
    - angular.module('myModule', ['ui.bootstrap']); +

    angular.module('myModule', ['ui.bootstrap']);

    You can fork one of the plunkers from this page to see a working example of what is described here.

    CSS

    Original Bootstrap's CSS depends on empty href attributes to style cursors for several components (pagination, tabs etc.). - But in AngularJS adding empty href attributes to link tags will cause unwanted route changes. - This is why we need to remove empty href attributes from directive templates and as a result - styling is not applied correctly. The remedy is simple, just add the following styling to your application: - - .nav, .pagination, .carousel, .panel-title a { - cursor: pointer; - } - + But in AngularJS adding empty href attributes to link tags will cause unwanted route changes. This is why we need to remove empty href attributes from directive templates and as a result styling is not applied correctly. The remedy is simple, just add the following styling to your application:

    .nav, .pagination, .carousel a { cursor: pointer; }

    -
    -
    -
    - <% demoModules.forEach(function(module) { %> -
    -
    - -
    -
    - <%= module.docs.html %> -
    -
    - <%= module.docs.md %> +
    + <% demoModules.forEach(function(module) { %> +
    + +
    +
    + <%= module.docs.html %> +
    +
    + <%= module.docs.md %> +
    -
    -
    -
    -
    -
    - +
    +
    +
    +
    + +
    + + +
    +
    <%- module.docs.html %>
    +
    +
    + +
    +
    <%- module.docs.js %>
    +
    +
    +
    - - -
    -
    <%- module.docs.html %>
    -
    -
    - -
    -
    <%- module.docs.js %>
    -
    -
    -
    -
    -
    - - - <% }); %> + + + <% }); %> +
    +
    @@ -215,20 +221,40 @@

    <%= module.displayName %> @@ -246,5 +272,6 @@

    {{buildErrorText}}

    })(); + diff --git a/src/popover/docs/demo.html b/src/popover/docs/demo.html index 3b878483fe..8955b1918e 100644 --- a/src/popover/docs/demo.html +++ b/src/popover/docs/demo.html @@ -1,9 +1,5 @@

    Dynamic

    -
    - - -
    @@ -12,7 +8,7 @@

    Dynamic

    - +

    Positional

    diff --git a/src/popover/docs/demo.js b/src/popover/docs/demo.js index 687cce0c3c..9fca7a0f5c 100644 --- a/src/popover/docs/demo.js +++ b/src/popover/docs/demo.js @@ -1,5 +1,4 @@ var PopoverDemoCtrl = function ($scope) { $scope.dynamicPopover = "Hello, World!"; - $scope.dynamicPopoverText = "dynamic"; $scope.dynamicPopoverTitle = "Title"; }; diff --git a/src/tabs/docs/demo.html b/src/tabs/docs/demo.html index 818e316140..d882441739 100644 --- a/src/tabs/docs/demo.html +++ b/src/tabs/docs/demo.html @@ -1,9 +1,12 @@
    - Select a tab by setting active binding to true: -
    - - - +

    Select a tab by setting active binding to true:

    +

    + + +

    +

    + +


    @@ -13,7 +16,7 @@ - Select me for alert! + Alert! I've got an HTML heading, and a select callback. Pretty cool! From dc02ad1d03c6feac3ce8150d41abf822e0a36100 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sat, 28 Dec 2013 21:31:45 +0100 Subject: [PATCH 0191/1761] fix(collapse): fixes after rebase --- src/collapse/collapse.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/collapse/collapse.js b/src/collapse/collapse.js index 1f5f005f97..95009a207d 100644 --- a/src/collapse/collapse.js +++ b/src/collapse/collapse.js @@ -30,13 +30,8 @@ angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) initialAnimSkip = false; expandDone(); } else { - var targetElHeight = element[0].scrollHeight; - if (targetElHeight) { - element.removeClass('collapse').addClass('collapsing'); - doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); - } else { - expandDone(); - } + element.removeClass('collapse').addClass('collapsing'); + doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); } } From 15e255d7a9f8e753a7ec23182c4c6b66884552cc Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sat, 28 Dec 2013 21:36:26 +0100 Subject: [PATCH 0192/1761] chore(release): v0.9.0 --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eeda294ac..84c49dee09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ +# 0.9.0 (2013-12-28) + +_This release adds Bootstrap3 support_ + +## Features + +- **accordion:** + - convert to bootstrap3 panel styling ([458a9bd3](http://github.com/angular-ui/bootstrap/commit/458a9bd3)) +- **carousel:** + - some changes for Bootstrap3 ([1f632b65](http://github.com/angular-ui/bootstrap/commit/1f632b65)) +- **collapse:** + - make collapse work with bootstrap3 ([517dff6e](http://github.com/angular-ui/bootstrap/commit/517dff6e)) +- **datepicker:** + - update to Bootstrap 3 ([37684330](http://github.com/angular-ui/bootstrap/commit/37684330)) +- **modal:** + - added bootstrap3 support ([444c488d](http://github.com/angular-ui/bootstrap/commit/444c488d)) +- **pagination:** + - support bootstrap3 ([3db699d7](http://github.com/angular-ui/bootstrap/commit/3db699d7)) +- **progressbar:** + - update to bootstrap3 ([5bcff623](http://github.com/angular-ui/bootstrap/commit/5bcff623)) +- **rating:** + - update rating to bootstrap3 ([7e60284e](http://github.com/angular-ui/bootstrap/commit/7e60284e)) +- **tabs:** + - add nav-justified ([3199dd88](http://github.com/angular-ui/bootstrap/commit/3199dd88)) +- **timepicker:** + - restyled for bootstrap 3 ([6724a721](http://github.com/angular-ui/bootstrap/commit/6724a721)) +- **typeahead:** + - update to Bootstrap 3 ([eadf934a](http://github.com/angular-ui/bootstrap/commit/eadf934a)) + +## Bug Fixes + +- **alert:** + - update template to Bootstrap 3 ([dfc3b0bd](http://github.com/angular-ui/bootstrap/commit/dfc3b0bd)) +- **collapse:** + - Prevent consecutive transitions & tidy up code ([b0032d68](http://github.com/angular-ui/bootstrap/commit/b0032d68)) + - fixes after rebase ([dc02ad1d](http://github.com/angular-ui/bootstrap/commit/dc02ad1d)) +- **rating:** + - user glyhicon classes ([d221d517](http://github.com/angular-ui/bootstrap/commit/d221d517)) +- **timepicker:** + - fix look with bootstrap3 ([9613b61b](http://github.com/angular-ui/bootstrap/commit/9613b61b)) +- **tooltip:** + - re-position tooltip after draw ([a99b3608](http://github.com/angular-ui/bootstrap/commit/a99b3608)) + # 0.8.0 (2013-12-28) ## Features diff --git a/package.json b/package.json index df8937adcf..57ce3310ae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "https://github.com/angular-ui/bootstrap/graphs/contributors", "name": "angular-ui-bootstrap", - "version": "0.9.0-SNAPSHOT", + "version": "0.9.0", "dependencies": {}, "devDependencies": { "grunt": "~0.4.1", From 6b1ca32e04ceb1cd1b76c142d46abc4e5dd59a49 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sat, 28 Dec 2013 22:23:39 +0100 Subject: [PATCH 0193/1761] chore(release): Starting v0.10.0-SNAPSHOT --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 57ce3310ae..86ae0221b6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "https://github.com/angular-ui/bootstrap/graphs/contributors", "name": "angular-ui-bootstrap", - "version": "0.9.0", + "version": "0.10.0-SNAPSHOT", "dependencies": {}, "devDependencies": { "grunt": "~0.4.1", From 0d0375d41a75305a8de935fa01e85569eb221648 Mon Sep 17 00:00:00 2001 From: Foxandxss Date: Sat, 28 Dec 2013 22:09:02 +0100 Subject: [PATCH 0194/1761] test(accordion): don't attatch accordion to body As far as I understand, to test sizes like widths or height you need to append your element to the real document. But that `describe` is not running any test with that. My guess is that there was some in the past and that line was forgotten there. --- src/accordion/test/accordion.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/accordion/test/accordion.spec.js b/src/accordion/test/accordion.spec.js index 056fe1f5b3..3222378ddb 100644 --- a/src/accordion/test/accordion.spec.js +++ b/src/accordion/test/accordion.spec.js @@ -130,7 +130,6 @@ describe('accordion', function () { "Content 2" + ""; element = angular.element(tpl); - angular.element(document.body).append(element); $compile(element)(scope); scope.$digest(); groups = element.find('.panel'); From aee5cc8aecfd028d04b72f236d6b5b87b5f62a5d Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sat, 28 Dec 2013 22:19:05 +0100 Subject: [PATCH 0195/1761] demo(accordion): pointer cursor for accordion --- misc/demo/assets/demo.css | 2 +- misc/demo/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/misc/demo/assets/demo.css b/misc/demo/assets/demo.css index 56c49d8ef1..d1cadcddd3 100644 --- a/misc/demo/assets/demo.css +++ b/misc/demo/assets/demo.css @@ -46,7 +46,7 @@ section { } -.nav, .pagination, .carousel a { +.nav, .pagination, .carousel, .panel-title a { cursor: pointer; } diff --git a/misc/demo/index.html b/misc/demo/index.html index e515c61f6a..feb7b87a79 100644 --- a/misc/demo/index.html +++ b/misc/demo/index.html @@ -164,7 +164,7 @@

    Installation

    You can fork one of the plunkers from this page to see a working example of what is described here.

    CSS

    Original Bootstrap's CSS depends on empty href attributes to style cursors for several components (pagination, tabs etc.). - But in AngularJS adding empty href attributes to link tags will cause unwanted route changes. This is why we need to remove empty href attributes from directive templates and as a result styling is not applied correctly. The remedy is simple, just add the following styling to your application:

    .nav, .pagination, .carousel a { cursor: pointer; }
    + But in AngularJS adding empty href attributes to link tags will cause unwanted route changes. This is why we need to remove empty href attributes from directive templates and as a result styling is not applied correctly. The remedy is simple, just add the following styling to your application:
    .nav, .pagination, .carousel, .panel-title a { cursor: pointer; }

    <% demoModules.forEach(function(module) { %> From c61e6974b2a63dda5c2aaecbf6daa4c1bc12be55 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sat, 28 Dec 2013 22:14:20 +0100 Subject: [PATCH 0196/1761] demo(all): fix bootstrap cdn for plunker --- misc/demo/assets/plunker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/demo/assets/plunker.js b/misc/demo/assets/plunker.js index 6f0d5c8823..f740f6beb4 100644 --- a/misc/demo/assets/plunker.js +++ b/misc/demo/assets/plunker.js @@ -18,7 +18,7 @@ angular.module('plunker', []) ' \n' + ' \n' + ' \n' + - ' \n' + + ' \n' + ' \n' + ' \n\n' + content + '\n' + From afd9f3664203dc4fc8979b00878a269bc6299585 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sat, 28 Dec 2013 16:05:13 -0800 Subject: [PATCH 0197/1761] test(tabs): typo in basics test --- src/tabs/test/tabs.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tabs/test/tabs.spec.js b/src/tabs/test/tabs.spec.js index 72f4c3613f..7e712b553f 100644 --- a/src/tabs/test/tabs.spec.js +++ b/src/tabs/test/tabs.spec.js @@ -46,7 +46,7 @@ describe('tabs', function() { ' Second Tab {{second}}', ' second content is {{second}}', ' ', - ' ', + '
    ', '
    ' ].join('\n'))(scope); scope.$apply(); From 28c85de2ec43489bcbd71f8eae3f6ebee6a86814 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sat, 28 Dec 2013 16:25:10 -0800 Subject: [PATCH 0198/1761] chore(tests): fix test output on CI Use reporters in karma.conf.js to avoid confusion in the future. --- Gruntfile.js | 2 +- karma.conf.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index fe8ca72779..2bdabaa636 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -133,7 +133,7 @@ module.exports = function(grunt) { jenkins: { singleRun: true, colors: false, - reporter: ['dots', 'junit'], + reporters: ['dots', 'junit'], browsers: ['Chrome', 'ChromeCanary', 'Firefox', 'Opera', '/Users/jenkins/bin/safari.sh', '/Users/jenkins/bin/ie9.sh'] }, travis: { diff --git a/karma.conf.js b/karma.conf.js index 15d329e4ec..086e2dc916 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -31,7 +31,7 @@ browsers = [ // test results reporter to use // possible values: dots || progress -reporter = 'progress'; +reporters = ['progress']; // web server port port = 9018; From 20d4efb38b14155414b0492c931da97cd38156b3 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sun, 29 Dec 2013 00:13:24 +0100 Subject: [PATCH 0199/1761] refactor(popover): remove unused dependency injections --- src/popover/popover.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/popover/popover.js b/src/popover/popover.js index c38ff9461e..2bea0a3e10 100644 --- a/src/popover/popover.js +++ b/src/popover/popover.js @@ -4,6 +4,7 @@ * just mouse enter/leave, html popovers, and selector delegatation. */ angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) + .directive( 'popoverPopup', function () { return { restrict: 'EA', @@ -12,7 +13,7 @@ angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) templateUrl: 'template/popover/popover.html' }; }) -.directive( 'popover', [ '$compile', '$timeout', '$parse', '$window', '$tooltip', function ( $compile, $timeout, $parse, $window, $tooltip ) { + +.directive( 'popover', [ '$tooltip', function ( $tooltip ) { return $tooltip( 'popover', 'popover', 'click' ); }]); - From 96985290b2d85b7bc5761537950f45932d0387b0 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Wed, 25 Dec 2013 12:56:54 -0800 Subject: [PATCH 0200/1761] chore(tests): add coverage as an option Knowing what exactly isn't being covered by the tests is more important than knowing the statistics. By default, this outputs full HTML reports with indicators of which lines or branches haven't aren't covered. --- .gitignore | 2 ++ Gruntfile.js | 17 +++++++++-------- README.md | 3 +++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 45aee72754..d106a75394 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ pids logs results dist +# test coverage files +coverage/ node_modules npm-debug.log diff --git a/Gruntfile.js b/Gruntfile.js index 2bdabaa636..49df257fa3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -141,14 +141,10 @@ module.exports = function(grunt) { browsers: ['Firefox'] }, coverage: { - singleRun: true, preprocessors: { 'src/*/*.js': 'coverage' }, - reporters: ['progress', 'coverage'], - coverageReporter: { - type : 'text' - } + reporters: ['progress', 'coverage'] } }, changelog: { @@ -330,14 +326,19 @@ module.exports = function(grunt) { grunt.task.run(['concat', 'uglify']); }); - grunt.registerTask('test', 'Run tests on singleRun karma server', function (subTask) { + grunt.registerTask('test', 'Run tests on singleRun karma server', function () { //this task can be executed in 3 different environments: local, Travis-CI and Jenkins-CI //we need to take settings for each one into account if (process.env.TRAVIS) { grunt.task.run('karma:travis'); - } else if (subTask === 'coverage') { - grunt.task.run('karma:coverage'); } else { + var isToRunJenkinsTask = !!this.args.length; + if(grunt.option('coverage')) { + var karmaOptions = grunt.config.get('karma.options'), + coverageOpts = grunt.config.get('karma.coverage'); + grunt.util._.extend(karmaOptions, coverageOpts); + grunt.config.set('karma.options', karmaOptions); + } grunt.task.run(this.args.length ? 'karma:jenkins' : 'karma:continuous'); } }); diff --git a/README.md b/README.md index 761d1c007b..f1524eb5d8 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ Check the Grunt build file for other tasks that are defined for this project. This will start Karma server and will continously watch files in the project, executing tests upon every change. +#### Test coverage +Add the `--coverage` option (e.g. `grunt test --coverage`, `grunt watch --coverage`) to see reports on the test coverage. These coverage reports are found in the coverage folder. + ### Customize templates As mentioned directives from this repository have all the markup externalized in templates. You might want to customize default From 32f0f63872e683a7e176c6e1dbbbf57d23d213dc Mon Sep 17 00:00:00 2001 From: Foxandxss Date: Sun, 29 Dec 2013 13:16:49 +0100 Subject: [PATCH 0201/1761] style(collapse): typo in a comment s/relaizes/realizes --- src/collapse/collapse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/collapse/collapse.js b/src/collapse/collapse.js index 95009a207d..240e15a0b5 100644 --- a/src/collapse/collapse.js +++ b/src/collapse/collapse.js @@ -49,7 +49,7 @@ angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) } else { // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value element.css({ height: element[0].scrollHeight + 'px' }); - //trigger reflow so a browser relaizes that height was updated from auto to a specific value + //trigger reflow so a browser realizes that height was updated from auto to a specific value var x = element[0].offsetWidth; element.removeClass('collapse in').addClass('collapsing'); From f56a396dd7009fc2336ca7bd64027c6f26334c02 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sun, 29 Dec 2013 19:54:20 +0100 Subject: [PATCH 0202/1761] test(pagination): add test for full coverage --- src/pagination/test/pagination.spec.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pagination/test/pagination.spec.js b/src/pagination/test/pagination.spec.js index b80197bc51..2479471f45 100644 --- a/src/pagination/test/pagination.spec.js +++ b/src/pagination/test/pagination.spec.js @@ -179,13 +179,18 @@ describe('pagination directive', function () { }); describe('when `page` is not a number', function () { - it('handles string', function() { + it('handles numerical string', function() { updateCurrentPage('2'); expect(getPaginationEl(2)).toHaveClass('active'); updateCurrentPage('04'); expect(getPaginationEl(4)).toHaveClass('active'); }); + + it('defaults to 1 if non-numeric', function() { + updateCurrentPage('pizza'); + expect(getPaginationEl(1)).toHaveClass('active'); + }); }); describe('with `max-size` option', function () { From d9e0f483ae60d14ceeb2ee59d90166baa556e912 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sun, 29 Dec 2013 22:32:06 +0100 Subject: [PATCH 0203/1761] refactor(rating): increase test coverage --- src/rating/rating.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/rating/rating.js b/src/rating/rating.js index 3b3640c06f..56440bdd18 100644 --- a/src/rating/rating.js +++ b/src/rating/rating.js @@ -28,11 +28,9 @@ angular.module('ui.bootstrap.rating', []) $scope.range = angular.isDefined($attrs.ratingStates) ? this.createRateObjects(angular.copy($scope.$parent.$eval($attrs.ratingStates))): this.createRateObjects(new Array(this.maxRange)); $scope.rate = function(value) { - if ( $scope.readonly || $scope.value === value) { - return; + if ( $scope.value !== value && !$scope.readonly ) { + $scope.value = value; } - - $scope.value = value; }; $scope.enter = function(value) { From ba79abe8372e0bd7d561e0a9c8669650e4e8ea41 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Mon, 30 Dec 2013 10:17:24 +0100 Subject: [PATCH 0204/1761] docs(tabs): remove deprecated `direction` & add missing `deselect` --- src/tabs/docs/readme.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tabs/docs/readme.md b/src/tabs/docs/readme.md index daee59f2fd..6a5252c435 100644 --- a/src/tabs/docs/readme.md +++ b/src/tabs/docs/readme.md @@ -16,10 +16,6 @@ AngularJS version of the tabs directive. _(Defaults: 'tabs')_ : Navigation type. Possible values are 'tabs' and 'pills'. - * `direction` - _(Defaults: null)_ : - What direction the tabs should be rendered. Available: 'right', 'left', 'below'. - #### `` #### * `heading` or `` @@ -36,4 +32,8 @@ AngularJS version of the tabs directive. * `select()` _(Defaults: null)_ : - An optional expression called when tab is activated. \ No newline at end of file + An optional expression called when tab is activated. + + * `deselect()` + _(Defaults: null)_ : + An optional expression called when tab is deactivated. \ No newline at end of file From c4d0e2a78e4b8e2c683ed834a76ea5cf4b47f35e Mon Sep 17 00:00:00 2001 From: Jesus Rodriguez Date: Mon, 30 Dec 2013 12:46:56 +0100 Subject: [PATCH 0205/1761] test(alert): add more coverage and other changes Closes #1489 --- src/alert/test/alert.spec.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/alert/test/alert.spec.js b/src/alert/test/alert.spec.js index 6742c5ea0f..ebac9b8338 100644 --- a/src/alert/test/alert.spec.js +++ b/src/alert/test/alert.spec.js @@ -35,6 +35,10 @@ describe("alert", function () { return element.find('.close').eq(index); } + function findContent(index) { + return element.find('span').eq(index); + } + it("should generate alerts using ng-repeat", function () { var alerts = createAlerts(); expect(alerts.length).toEqual(3); @@ -44,11 +48,15 @@ describe("alert", function () { var alerts = createAlerts(); expect(alerts.eq(0)).toHaveClass('alert-success'); expect(alerts.eq(1)).toHaveClass('alert-error'); + expect(alerts.eq(2)).toHaveClass('alert-warning'); + }); - //defaults - expect(alerts.eq(2)).toHaveClass('alert'); - expect(alerts.eq(2)).not.toHaveClass('alert-info'); - expect(alerts.eq(2)).not.toHaveClass('alert-block'); + it('should show the alert content', function() { + var alerts = createAlerts(); + + for (var i = 0, n = alerts.length; i < n; i++) { + expect(findContent(i).text()).toBe(scope.alerts[i].msg); + } }); it("should show close buttons", function () { @@ -79,7 +87,7 @@ describe("alert", function () { expect(findCloseButton(0).css('display')).toBe('none'); }); - it('it should be possible to add additional classes for alert', function () { + it('should be possible to add additional classes for alert', function () { var element = $compile('Default alert!')(scope); scope.$digest(); expect(element).toHaveClass('alert-block'); From c0df32015167646343c44850355fb947593d7bd0 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Mon, 23 Dec 2013 16:10:43 -0800 Subject: [PATCH 0206/1761] fix(tooltip): performance and scope fixes Isolate scope contents should be the same after hiding and showing the tooltip. The isolate scope's parent should also always be set to the directive's scope correctly. fix(tooltip): link on demand - Calling $digest is enough as we only need to digest the watchers in this scope and its children. No need to call $apply. - Set invokeApply to false on $timeout for popUpDelay - No need to test for cached reference when tooltip isn't visible as the tooltip has no scope. Closes #1450 Closes #1191 Closes #1455 --- src/tooltip/test/tooltip.spec.js | 35 ++- src/tooltip/tooltip.js | 395 ++++++++++++++++--------------- 2 files changed, 235 insertions(+), 195 deletions(-) diff --git a/src/tooltip/test/tooltip.spec.js b/src/tooltip/test/tooltip.spec.js index 01f6423fe7..90be4f6907 100644 --- a/src/tooltip/test/tooltip.spec.js +++ b/src/tooltip/test/tooltip.spec.js @@ -98,7 +98,7 @@ describe('tooltip', function() { scope.alt = "Alt Message"; elmBody = $compile( angular.element( - '
    Selector Text
    ' + '
    Selector Text
    ' ) )( scope ); $compile( elmBody )( scope ); @@ -114,6 +114,13 @@ describe('tooltip', function() { expect( ttScope.content ).toBe( scope.tooltipMsg ); elm.trigger( 'mouseleave' ); + + //Isolate scope contents should be the same after hiding and showing again (issue 1191) + elm.trigger( 'mouseenter' ); + + ttScope = angular.element( elmBody.children()[1] ).scope(); + expect( ttScope.placement ).toBe( 'top' ); + expect( ttScope.content ).toBe( scope.tooltipMsg ); })); it('should not show tooltips if there is nothing to show - issue #129', inject(function ($compile) { @@ -136,6 +143,24 @@ describe('tooltip', function() { expect( elmBody.children().length ).toBe( 0 ); })); + it('issue 1191 - isolate scope on the popup should always be child of correct element scope', inject( function ( $compile ) { + var ttScope; + elm.trigger( 'mouseenter' ); + + ttScope = angular.element( elmBody.children()[1] ).scope(); + expect( ttScope.$parent ).toBe( elmScope ); + + elm.trigger( 'mouseleave' ); + + // After leaving and coming back, the scope's parent should be the same + elm.trigger( 'mouseenter' ); + + ttScope = angular.element( elmBody.children()[1] ).scope(); + expect( ttScope.$parent ).toBe( elmScope ); + + elm.trigger( 'mouseleave' ); + })); + describe('with specified enable expression', function() { beforeEach(inject(function ($compile) { @@ -323,18 +348,12 @@ describe('tooltip', function() { elm = elmBody.find('input'); elmScope = elm.scope(); + elm.trigger('fooTrigger'); tooltipScope = elmScope.$$childTail; })); - it( 'should not contain a cached reference', function() { - expect( inCache() ).toBeTruthy(); - elmScope.$destroy(); - expect( inCache() ).toBeFalsy(); - }); - it( 'should not contain a cached reference when visible', inject( function( $timeout ) { expect( inCache() ).toBeTruthy(); - elm.trigger('fooTrigger'); elmScope.$destroy(); expect( inCache() ).toBeFalsy(); })); diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index f6d116647f..c6a610f8f3 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -108,224 +108,245 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap return { restrict: 'EA', scope: true, - link: function link ( scope, element, attrs ) { - var tooltip = $compile( template )( scope ); - var transitionTimeout; - var popupTimeout; - var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; - var triggers = getTriggers( undefined ); - var hasRegisteredTriggers = false; - var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); - - var positionTooltip = function (){ - var position, - ttWidth, - ttHeight, - ttPosition; - // Get the position of the directive element. - position = appendToBody ? $position.offset( element ) : $position.position( element ); - - // Get the height and width of the tooltip so we can center it. - ttWidth = tooltip.prop( 'offsetWidth' ); - ttHeight = tooltip.prop( 'offsetHeight' ); - - // Calculate the tooltip's top and left coordinates to center it with - // this directive. - switch ( scope.tt_placement ) { - case 'right': - ttPosition = { - top: position.top + position.height / 2 - ttHeight / 2, - left: position.left + position.width - }; - break; - case 'bottom': - ttPosition = { - top: position.top + position.height, - left: position.left + position.width / 2 - ttWidth / 2 - }; - break; - case 'left': - ttPosition = { - top: position.top + position.height / 2 - ttHeight / 2, - left: position.left - ttWidth - }; - break; - default: - ttPosition = { - top: position.top - ttHeight, - left: position.left + position.width / 2 - ttWidth / 2 - }; - break; + compile: function (tElem, tAttrs) { + var tooltipLinker = $compile( template ); + + return function link ( scope, element, attrs ) { + var tooltip; + var transitionTimeout; + var popupTimeout; + var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; + var triggers = getTriggers( undefined ); + var hasRegisteredTriggers = false; + var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + + var positionTooltip = function (){ + var position, + ttWidth, + ttHeight, + ttPosition; + // Get the position of the directive element. + position = appendToBody ? $position.offset( element ) : $position.position( element ); + + // Get the height and width of the tooltip so we can center it. + ttWidth = tooltip.prop( 'offsetWidth' ); + ttHeight = tooltip.prop( 'offsetHeight' ); + + // Calculate the tooltip's top and left coordinates to center it with + // this directive. + switch ( scope.tt_placement ) { + case 'right': + ttPosition = { + top: position.top + position.height / 2 - ttHeight / 2, + left: position.left + position.width + }; + break; + case 'bottom': + ttPosition = { + top: position.top + position.height, + left: position.left + position.width / 2 - ttWidth / 2 + }; + break; + case 'left': + ttPosition = { + top: position.top + position.height / 2 - ttHeight / 2, + left: position.left - ttWidth + }; + break; + default: + ttPosition = { + top: position.top - ttHeight, + left: position.left + position.width / 2 - ttWidth / 2 + }; + break; + } + + ttPosition.top += 'px'; + ttPosition.left += 'px'; + + // Now set the calculated positioning. + tooltip.css( ttPosition ); + + }; + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + scope.tt_isOpen = false; + + function toggleTooltipBind () { + if ( ! scope.tt_isOpen ) { + showTooltipBind(); + } else { + hideTooltipBind(); + } } - ttPosition.top += 'px'; - ttPosition.left += 'px'; + // Show the tooltip with delay if specified, otherwise show it immediately + function showTooltipBind() { + if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { + return; + } + if ( scope.tt_popupDelay ) { + popupTimeout = $timeout( show, scope.tt_popupDelay, false ); + popupTimeout.then(function(reposition){reposition();}); + } else { + show()(); + } + } - // Now set the calculated positioning. - tooltip.css( ttPosition ); + function hideTooltipBind () { + scope.$apply(function () { + hide(); + }); + } - }; + // Show the tooltip popup element. + function show() { - // By default, the tooltip is not open. - // TODO add ability to start tooltip opened - scope.tt_isOpen = false; - function toggleTooltipBind () { - if ( ! scope.tt_isOpen ) { - showTooltipBind(); - } else { - hideTooltipBind(); - } - } - - // Show the tooltip with delay if specified, otherwise show it immediately - function showTooltipBind() { - if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { - return; - } - if ( scope.tt_popupDelay ) { - popupTimeout = $timeout( show, scope.tt_popupDelay ); - popupTimeout.then(function(reposition){reposition();}); - } else { - scope.$apply( show )(); - } - } + // Don't show empty tooltips. + if ( ! scope.tt_content ) { + return angular.noop; + } - function hideTooltipBind () { - scope.$apply(function () { - hide(); - }); - } - - // Show the tooltip popup element. - function show() { + createTooltip(); + // If there is a pending remove transition, we must cancel it, lest the + // tooltip be mysteriously removed. + if ( transitionTimeout ) { + $timeout.cancel( transitionTimeout ); + } - // Don't show empty tooltips. - if ( ! scope.tt_content ) { - return angular.noop; - } + // Set the initial positioning. + tooltip.css({ top: 0, left: 0, display: 'block' }); - // If there is a pending remove transition, we must cancel it, lest the - // tooltip be mysteriously removed. - if ( transitionTimeout ) { - $timeout.cancel( transitionTimeout ); - } - - // Set the initial positioning. - tooltip.css({ top: 0, left: 0, display: 'block' }); - - // Now we add it to the DOM because need some info about it. But it's not - // visible yet anyway. - if ( appendToBody ) { - $document.find( 'body' ).append( tooltip ); - } else { - element.after( tooltip ); + // Now we add it to the DOM because need some info about it. But it's not + // visible yet anyway. + if ( appendToBody ) { + $document.find( 'body' ).append( tooltip ); + } else { + element.after( tooltip ); + } + + positionTooltip(); + + // And show the tooltip. + scope.tt_isOpen = true; + scope.$digest(); // digest required as $apply is not called + + // Return positioning function as promise callback for correct + // positioning after draw. + return positionTooltip; } - positionTooltip(); + // Hide the tooltip popup element. + function hide() { + // First things first: we don't show it anymore. + scope.tt_isOpen = false; + + //if tooltip is going to be shown after delay, we must cancel this + $timeout.cancel( popupTimeout ); + + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + if ( scope.tt_animation ) { + transitionTimeout = $timeout(removeTooltip, 500); + } else { + removeTooltip(); + } + } - // And show the tooltip. - scope.tt_isOpen = true; + function createTooltip() { + // There can only be one tooltip element per directive shown at once. + if (tooltip) { + removeTooltip(); + } + tooltip = tooltipLinker(scope, function () {}); - // Return positioning function as promise callback for correct - // positioning after draw. - return positionTooltip; - } - - // Hide the tooltip popup element. - function hide() { - // First things first: we don't show it anymore. - scope.tt_isOpen = false; + // Get contents rendered into the tooltip + scope.$digest(); + } - //if tooltip is going to be shown after delay, we must cancel this - $timeout.cancel( popupTimeout ); - - // And now we remove it from the DOM. However, if we have animation, we - // need to wait for it to expire beforehand. - // FIXME: this is a placeholder for a port of the transitions library. - if ( scope.tt_animation ) { - transitionTimeout = $timeout(function () { + function removeTooltip() { + if (tooltip) { tooltip.remove(); - }, 500); - } else { - tooltip.remove(); + tooltip = null; + } } - } - /** - * Observe the relevant attributes. - */ - attrs.$observe( type, function ( val ) { - scope.tt_content = val; + /** + * Observe the relevant attributes. + */ + attrs.$observe( type, function ( val ) { + scope.tt_content = val; - if (!val && scope.tt_isOpen ) { - hide(); - } - }); + if (!val && scope.tt_isOpen ) { + hide(); + } + }); - attrs.$observe( prefix+'Title', function ( val ) { - scope.tt_title = val; - }); + attrs.$observe( prefix+'Title', function ( val ) { + scope.tt_title = val; + }); - attrs.$observe( prefix+'Placement', function ( val ) { - scope.tt_placement = angular.isDefined( val ) ? val : options.placement; - }); + attrs.$observe( prefix+'Placement', function ( val ) { + scope.tt_placement = angular.isDefined( val ) ? val : options.placement; + }); - attrs.$observe( prefix+'PopupDelay', function ( val ) { - var delay = parseInt( val, 10 ); - scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; - }); + attrs.$observe( prefix+'PopupDelay', function ( val ) { + var delay = parseInt( val, 10 ); + scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; + }); - var unregisterTriggers = function() { - if (hasRegisteredTriggers) { - element.unbind( triggers.show, showTooltipBind ); - element.unbind( triggers.hide, hideTooltipBind ); - } - }; + var unregisterTriggers = function() { + if (hasRegisteredTriggers) { + element.unbind( triggers.show, showTooltipBind ); + element.unbind( triggers.hide, hideTooltipBind ); + } + }; - attrs.$observe( prefix+'Trigger', function ( val ) { - unregisterTriggers(); + attrs.$observe( prefix+'Trigger', function ( val ) { + unregisterTriggers(); - triggers = getTriggers( val ); + triggers = getTriggers( val ); - if ( triggers.show === triggers.hide ) { - element.bind( triggers.show, toggleTooltipBind ); - } else { - element.bind( triggers.show, showTooltipBind ); - element.bind( triggers.hide, hideTooltipBind ); - } + if ( triggers.show === triggers.hide ) { + element.bind( triggers.show, toggleTooltipBind ); + } else { + element.bind( triggers.show, showTooltipBind ); + element.bind( triggers.hide, hideTooltipBind ); + } - hasRegisteredTriggers = true; - }); + hasRegisteredTriggers = true; + }); - var animation = scope.$eval(attrs[prefix + 'Animation']); - scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation; + var animation = scope.$eval(attrs[prefix + 'Animation']); + scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation; - attrs.$observe( prefix+'AppendToBody', function ( val ) { - appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; - }); + attrs.$observe( prefix+'AppendToBody', function ( val ) { + appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; + }); - // if a tooltip is attached to we need to remove it on - // location change as its parent scope will probably not be destroyed - // by the change. - if ( appendToBody ) { - scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { - if ( scope.tt_isOpen ) { - hide(); + // if a tooltip is attached to we need to remove it on + // location change as its parent scope will probably not be destroyed + // by the change. + if ( appendToBody ) { + scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { + if ( scope.tt_isOpen ) { + hide(); + } + }); } - }); - } - - // Make sure tooltip is destroyed and removed. - scope.$on('$destroy', function onDestroyTooltip() { - $timeout.cancel( transitionTimeout ); - $timeout.cancel( popupTimeout ); - unregisterTriggers(); - tooltip.remove(); - tooltip.unbind(); - tooltip = null; - }); + + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTooltip() { + $timeout.cancel( transitionTimeout ); + $timeout.cancel( popupTimeout ); + unregisterTriggers(); + removeTooltip(); + }); + }; } }; }; From 5f8e3e8382bf21e3345fc0a6a67394a020c20ff7 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Mon, 30 Dec 2013 20:12:53 -0800 Subject: [PATCH 0207/1761] refactor(datepickerSpec): speed up tests Tests were compiling the element multiple times unnecessarily as it's done in the root describe's beforeEach. Introduce describe blocks with no name to encapsulate tests that need the element to be compiled and separate them from tests that have their own compilation. Closes #1499 --- src/datepicker/test/datepicker.spec.js | 751 +++++++++++++------------ 1 file changed, 382 insertions(+), 369 deletions(-) diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index 3c4252913a..8a051d03e3 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -7,8 +7,6 @@ describe('datepicker directive', function () { $compile = _$compile_; $rootScope = _$rootScope_; $rootScope.date = new Date("September 30, 2010 15:30:00"); - element = $compile('')($rootScope); - $rootScope.$digest(); })); function getTitle() { @@ -108,332 +106,340 @@ describe('datepicker directive', function () { } } - it('is a `

    {{ getWeekNumber(row) }} - +
    :
    ` element', function() { - expect(element.prop('tagName')).toBe('TABLE'); - expect(element.find('thead').find('tr').length).toBe(2); - }); + describe('', function () { + beforeEach(inject(function(_$compile_, _$rootScope_) { + element = $compile('')($rootScope); + $rootScope.$digest(); + })); - it('shows the correct title', function() { - expect(getTitle()).toBe('September 2010'); - }); + it('is a `
    ` element', function() { + expect(element.prop('tagName')).toBe('TABLE'); + expect(element.find('thead').find('tr').length).toBe(2); + }); - it('shows the label row & the correct day labels', function() { - expect(getLabelsRow().css('display')).not.toBe('none'); - expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); - }); + it('shows the correct title', function() { + expect(getTitle()).toBe('September 2010'); + }); - it('renders the calendar days correctly', function() { - expect(getOptions()).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'] - ]); - }); + it('shows the label row & the correct day labels', function() { + expect(getLabelsRow().css('display')).not.toBe('none'); + expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); + }); - it('renders the week numbers based on ISO 8601', function() { - expect(getWeeks()).toEqual(['34', '35', '36', '37', '38']); - }); + it('renders the calendar days correctly', function() { + expect(getOptions()).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'] + ]); + }); - it('value is correct', function() { - expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); - }); + it('renders the week numbers based on ISO 8601', function() { + expect(getWeeks()).toEqual(['34', '35', '36', '37', '38']); + }); - it('has `selected` only the correct day', function() { - expectSelectedElement( 4, 4 ); - }); + it('value is correct', function() { + expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); + }); - it('has no `selected` day when model is cleared', function() { - $rootScope.date = null; - $rootScope.$digest(); + it('has `selected` only the correct day', function() { + expectSelectedElement( 4, 4 ); + }); - expect($rootScope.date).toBe(null); - expectSelectedElement( null, null ); - }); + it('has no `selected` day when model is cleared', function() { + $rootScope.date = null; + $rootScope.$digest(); - it('does not change current view when model is cleared', function() { - $rootScope.date = null; - $rootScope.$digest(); + expect($rootScope.date).toBe(null); + expectSelectedElement( null, null ); + }); - expect($rootScope.date).toBe(null); - expect(getTitle()).toBe('September 2010'); - }); + it('does not change current view when model is cleared', function() { + $rootScope.date = null; + $rootScope.$digest(); - it('`disables` visible dates from other months', function() { - var options = getAllOptionsEl(); - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(options[i][j].find('button').find('span').hasClass('text-muted')).toBe( ((i === 0 && j < 3) || (i === 4 && j > 4)) ); + expect($rootScope.date).toBe(null); + expect(getTitle()).toBe('September 2010'); + }); + + it('`disables` visible dates from other months', function() { + var options = getAllOptionsEl(); + for (var i = 0; i < 5; i ++) { + for (var j = 0; j < 7; j ++) { + expect(options[i][j].find('button').find('span').hasClass('text-muted')).toBe( ((i === 0 && j < 3) || (i === 4 && j > 4)) ); + } } - } - }); + }); - it('updates the model when a day is clicked', function() { - clickOption(2, 3); - expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); - }); + it('updates the model when a day is clicked', function() { + clickOption(2, 3); + expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); + }); - it('moves to the previous month & renders correctly when `previous` button is clicked', function() { - clickPreviousButton(); + it('moves to the previous month & renders correctly when `previous` button is clicked', function() { + clickPreviousButton(); - expect(getTitle()).toBe('August 2010'); - expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); - expect(getOptions()).toEqual([ - ['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'] - ]); + expect(getTitle()).toBe('August 2010'); + expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); + expect(getOptions()).toEqual([ + ['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'] + ]); - expectSelectedElement( null, null ); - }); + expectSelectedElement( null, null ); + }); - it('updates the model only when when a day is clicked in the `previous` month', function() { - clickPreviousButton(); - expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); + it('updates the model only when when a day is clicked in the `previous` month', function() { + clickPreviousButton(); + expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); - clickOption(2, 3); - expect($rootScope.date).toEqual(new Date('August 18, 2010 15:30:00')); - }); + clickOption(2, 3); + expect($rootScope.date).toEqual(new Date('August 18, 2010 15:30:00')); + }); - it('moves to the next month & renders correctly when `next` button is clicked', function() { - clickNextButton(); - - expect(getTitle()).toBe('October 2010'); - expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); - expect(getOptions()).toEqual([ - ['26', '27', '28', '29', '30', '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', '06'] - ]); - - expectSelectedElement( 0, 4 ); - }); + it('moves to the next month & renders correctly when `next` button is clicked', function() { + clickNextButton(); - it('updates the model only when when a day is clicked in the `next` month', function() { - clickNextButton(); - expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); + expect(getTitle()).toBe('October 2010'); + expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); + expect(getOptions()).toEqual([ + ['26', '27', '28', '29', '30', '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', '06'] + ]); - clickOption(2, 3); - expect($rootScope.date).toEqual(new Date('October 13, 2010 15:30:00')); - }); + expectSelectedElement( 0, 4 ); + }); - it('updates the calendar when a day of another month is selected', function() { - clickOption(4, 5); - expect($rootScope.date).toEqual(new Date('October 01, 2010 15:30:00')); - expect(getTitle()).toBe('October 2010'); - expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); - expect(getOptions()).toEqual([ - ['26', '27', '28', '29', '30', '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', '06'] - ]); - - expectSelectedElement( 0, 5 ); - }); + it('updates the model only when when a day is clicked in the `next` month', function() { + clickNextButton(); + expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); - describe('when `model` changes', function () { - function testCalendar() { - expect(getTitle()).toBe('November 2005'); + clickOption(2, 3); + expect($rootScope.date).toEqual(new Date('October 13, 2010 15:30:00')); + }); + + it('updates the calendar when a day of another month is selected', function() { + clickOption(4, 5); + expect($rootScope.date).toEqual(new Date('October 01, 2010 15:30:00')); + expect(getTitle()).toBe('October 2010'); + expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); expect(getOptions()).toEqual([ - ['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'] + ['26', '27', '28', '29', '30', '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', '06'] ]); - expectSelectedElement( 1, 1 ); - } - - describe('to a Date object', function() { - it('updates', function() { - $rootScope.date = new Date('November 7, 2005 23:30:00'); - $rootScope.$digest(); - testCalendar(); - expect(angular.isDate($rootScope.date)).toBe(true); - }); + expectSelectedElement( 0, 5 ); }); - describe('not to a Date object', function() { + describe('when `model` changes', function () { + function testCalendar() { + expect(getTitle()).toBe('November 2005'); + expect(getOptions()).toEqual([ + ['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'] + ]); - it('to a Number, it updates calendar', function() { - $rootScope.date = parseInt((new Date('November 7, 2005 23:30:00')).getTime(), 10); - $rootScope.$digest(); - testCalendar(); - expect(angular.isNumber($rootScope.date)).toBe(true); - }); + expectSelectedElement( 1, 1 ); + } - it('to a string that can be parsed by Date, it updates calendar', function() { - $rootScope.date = 'November 7, 2005 23:30:00'; - $rootScope.$digest(); - testCalendar(); - expect(angular.isString($rootScope.date)).toBe(true); + describe('to a Date object', function() { + it('updates', function() { + $rootScope.date = new Date('November 7, 2005 23:30:00'); + $rootScope.$digest(); + testCalendar(); + expect(angular.isDate($rootScope.date)).toBe(true); + }); }); - it('to a string that cannot be parsed by Date, it gets invalid', function() { - $rootScope.date = 'pizza'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).toBeTruthy(); - expect(element.hasClass('ng-invalid-date')).toBeTruthy(); - expect($rootScope.date).toBe('pizza'); - }); - }); - }); + describe('not to a Date object', function() { - it('loops between different modes', function() { - expect(getTitle()).toBe('September 2010'); + it('to a Number, it updates calendar', function() { + $rootScope.date = parseInt((new Date('November 7, 2005 23:30:00')).getTime(), 10); + $rootScope.$digest(); + testCalendar(); + expect(angular.isNumber($rootScope.date)).toBe(true); + }); - clickTitleButton(); - expect(getTitle()).toBe('2010'); + it('to a string that can be parsed by Date, it updates calendar', function() { + $rootScope.date = 'November 7, 2005 23:30:00'; + $rootScope.$digest(); + testCalendar(); + expect(angular.isString($rootScope.date)).toBe(true); + }); - clickTitleButton(); - expect(getTitle()).toBe('2001 - 2020'); + it('to a string that cannot be parsed by Date, it gets invalid', function() { + $rootScope.date = 'pizza'; + $rootScope.$digest(); + expect(element.hasClass('ng-invalid')).toBeTruthy(); + expect(element.hasClass('ng-invalid-date')).toBeTruthy(); + expect($rootScope.date).toBe('pizza'); + }); + }); + }); - clickTitleButton(); - expect(getTitle()).toBe('September 2010'); - }); + it('loops between different modes', function() { + expect(getTitle()).toBe('September 2010'); - describe('month selection mode', function () { - beforeEach(function() { clickTitleButton(); - }); - - it('shows the year as title', function() { expect(getTitle()).toBe('2010'); - }); - it('shows months as options', function() { - expect(getLabels()).toEqual([]); - expect(getOptions()).toEqual([ - ['January', 'February', 'March'], - ['April', 'May', 'June'], - ['July', 'August', 'September'], - ['October', 'November', 'December'] - ]); - }); + clickTitleButton(); + expect(getTitle()).toBe('2001 - 2020'); - it('does not change the model', function() { - expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); + clickTitleButton(); + expect(getTitle()).toBe('September 2010'); }); - it('has `selected` only the correct month', function() { - expectSelectedElement( 2, 2 ); - }); + describe('month selection mode', function () { + beforeEach(function() { + clickTitleButton(); + }); - it('moves to the previous year when `previous` button is clicked', function() { - clickPreviousButton(); + it('shows the year as title', function() { + expect(getTitle()).toBe('2010'); + }); - expect(getTitle()).toBe('2009'); - expect(getLabels()).toEqual([]); - expect(getOptions()).toEqual([ - ['January', 'February', 'March'], - ['April', 'May', 'June'], - ['July', 'August', 'September'], - ['October', 'November', 'December'] - ]); + it('shows months as options', function() { + expect(getLabels()).toEqual([]); + expect(getOptions()).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); + }); - expectSelectedElement( null, null ); - }); + it('does not change the model', function() { + expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); + }); - it('moves to the next year when `next` button is clicked', function() { - clickNextButton(); + it('has `selected` only the correct month', function() { + expectSelectedElement( 2, 2 ); + }); - expect(getTitle()).toBe('2011'); - expect(getLabels()).toEqual([]); - expect(getOptions()).toEqual([ - ['January', 'February', 'March'], - ['April', 'May', 'June'], - ['July', 'August', 'September'], - ['October', 'November', 'December'] - ]); + it('moves to the previous year when `previous` button is clicked', function() { + clickPreviousButton(); - expectSelectedElement( null, null ); - }); + expect(getTitle()).toBe('2009'); + expect(getLabels()).toEqual([]); + expect(getOptions()).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); - it('renders correctly when a month is clicked', function() { - clickPreviousButton(5); - expect(getTitle()).toBe('2005'); + expectSelectedElement( null, null ); + }); - clickOption(3, 1); - expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); - expect(getTitle()).toBe('November 2005'); - expect(getOptions()).toEqual([ - ['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'] - ]); + it('moves to the next year when `next` button is clicked', function() { + clickNextButton(); - clickOption(2, 3); - expect($rootScope.date).toEqual(new Date('November 16, 2005 15:30:00')); - }); - }); + expect(getTitle()).toBe('2011'); + expect(getLabels()).toEqual([]); + expect(getOptions()).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); - describe('year selection mode', function () { - beforeEach(function() { - clickTitleButton(2); - }); + expectSelectedElement( null, null ); + }); - it('shows the year range as title', function() { - expect(getTitle()).toBe('2001 - 2020'); - }); + it('renders correctly when a month is clicked', function() { + clickPreviousButton(5); + expect(getTitle()).toBe('2005'); + + clickOption(3, 1); + expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); + expect(getTitle()).toBe('November 2005'); + expect(getOptions()).toEqual([ + ['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'] + ]); - it('shows years as options', function() { - expect(getLabels()).toEqual([]); - expect(getOptions()).toEqual([ - ['2001', '2002', '2003', '2004', '2005'], - ['2006', '2007', '2008', '2009', '2010'], - ['2011', '2012', '2013', '2014', '2015'], - ['2016', '2017', '2018', '2019', '2020'] - ]); + clickOption(2, 3); + expect($rootScope.date).toEqual(new Date('November 16, 2005 15:30:00')); + }); }); - it('does not change the model', function() { - expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); - }); + describe('year selection mode', function () { + beforeEach(function() { + clickTitleButton(2); + }); - it('has `selected` only the selected year', function() { - expectSelectedElement( 1, 4 ); - }); + it('shows the year range as title', function() { + expect(getTitle()).toBe('2001 - 2020'); + }); - it('moves to the previous year set when `previous` button is clicked', function() { - clickPreviousButton(); + it('shows years as options', function() { + expect(getLabels()).toEqual([]); + expect(getOptions()).toEqual([ + ['2001', '2002', '2003', '2004', '2005'], + ['2006', '2007', '2008', '2009', '2010'], + ['2011', '2012', '2013', '2014', '2015'], + ['2016', '2017', '2018', '2019', '2020'] + ]); + }); - expect(getTitle()).toBe('1981 - 2000'); - expect(getLabels()).toEqual([]); - expect(getOptions()).toEqual([ - ['1981', '1982', '1983', '1984', '1985'], - ['1986', '1987', '1988', '1989', '1990'], - ['1991', '1992', '1993', '1994', '1995'], - ['1996', '1997', '1998', '1999', '2000'] - ]); - expectSelectedElement( null, null ); - }); + it('does not change the model', function() { + expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); + }); - it('moves to the next year set when `next` button is clicked', function() { - clickNextButton(); + it('has `selected` only the selected year', function() { + expectSelectedElement( 1, 4 ); + }); - expect(getTitle()).toBe('2021 - 2040'); - expect(getLabels()).toEqual([]); - expect(getOptions()).toEqual([ - ['2021', '2022', '2023', '2024', '2025'], - ['2026', '2027', '2028', '2029', '2030'], - ['2031', '2032', '2033', '2034', '2035'], - ['2036', '2037', '2038', '2039', '2040'] - ]); + it('moves to the previous year set when `previous` button is clicked', function() { + clickPreviousButton(); + + expect(getTitle()).toBe('1981 - 2000'); + expect(getLabels()).toEqual([]); + expect(getOptions()).toEqual([ + ['1981', '1982', '1983', '1984', '1985'], + ['1986', '1987', '1988', '1989', '1990'], + ['1991', '1992', '1993', '1994', '1995'], + ['1996', '1997', '1998', '1999', '2000'] + ]); + expectSelectedElement( null, null ); + }); - expectSelectedElement( null, null ); + it('moves to the next year set when `next` button is clicked', function() { + clickNextButton(); + + expect(getTitle()).toBe('2021 - 2040'); + expect(getLabels()).toEqual([]); + expect(getOptions()).toEqual([ + ['2021', '2022', '2023', '2024', '2025'], + ['2026', '2027', '2028', '2029', '2030'], + ['2031', '2032', '2033', '2034', '2035'], + ['2036', '2037', '2038', '2039', '2040'] + ]); + + expectSelectedElement( null, null ); + }); }); + }); describe('attribute `starting-day`', function () { @@ -955,92 +961,104 @@ describe('datepicker directive', function () { element = dropdownEl.find('table'); } - beforeEach(inject(function(_$document_, $sniffer) { - $document = _$document_; - $rootScope.date = new Date("September 30, 2010 15:30:00"); - var wrapElement = $compile('
    ')($rootScope); - $rootScope.$digest(); - assignElements(wrapElement); - - changeInputValueTo = function (el, value) { - el.val(value); - el.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); + describe('', function () { + beforeEach(inject(function(_$document_, $sniffer) { + $document = _$document_; + $rootScope.date = new Date("September 30, 2010 15:30:00"); + var wrapElement = $compile('
    ')($rootScope); $rootScope.$digest(); - }; - })); + assignElements(wrapElement); - it('to display the correct value in input', function() { - expect(inputEl.val()).toBe('2010-09-30'); - }); + changeInputValueTo = function (el, value) { + el.val(value); + el.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); + $rootScope.$digest(); + }; + })); - it('does not to display datepicker initially', function() { - expect(dropdownEl.css('display')).toBe('none'); - }); + it('to display the correct value in input', function() { + expect(inputEl.val()).toBe('2010-09-30'); + }); - it('displays datepicker on input focus', function() { - inputEl.focus(); - expect(dropdownEl.css('display')).not.toBe('none'); - }); + it('does not to display datepicker initially', function() { + expect(dropdownEl.css('display')).toBe('none'); + }); - it('renders the calendar correctly', function() { - expect(getLabelsRow().css('display')).not.toBe('none'); - expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); - expect(getOptions()).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'] - ]); - }); + it('displays datepicker on input focus', function() { + inputEl.focus(); + expect(dropdownEl.css('display')).not.toBe('none'); + }); - it('updates the input when a day is clicked', function() { - clickOption(2, 3); - expect(inputEl.val()).toBe('2010-09-15'); - expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); - }); + it('renders the calendar correctly', function() { + expect(getLabelsRow().css('display')).not.toBe('none'); + expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); + expect(getOptions()).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'] + ]); + }); - it('should mark the input field dirty when a day is clicked', function() { - expect(inputEl).toHaveClass('ng-pristine'); - clickOption(2, 3); - expect(inputEl).toHaveClass('ng-dirty'); - }); + it('updates the input when a day is clicked', function() { + clickOption(2, 3); + expect(inputEl.val()).toBe('2010-09-15'); + 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('1983-01-10'); - }); + it('should mark the input field dirty when a day is clicked', function() { + expect(inputEl).toHaveClass('ng-pristine'); + clickOption(2, 3); + 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() { - inputEl.focus(); - expect(dropdownEl.css('display')).not.toBe('none'); + it('closes the dropdown when a day is clicked', function() { + inputEl.focus(); + expect(dropdownEl.css('display')).not.toBe('none'); - clickOption(2, 3); - expect(dropdownEl.css('display')).toBe('none'); - }); + clickOption(2, 3); + expect(dropdownEl.css('display')).toBe('none'); + }); - it('updates the model & calendar when input value changes', function() { - changeInputValueTo(inputEl, 'March 5, 1980'); + it('updates the model & calendar when input value changes', function() { + changeInputValueTo(inputEl, 'March 5, 1980'); + + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(2); + expect($rootScope.date.getDate()).toEqual(5); + + expect(getOptions()).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( 1, 3 ); + }); - expect($rootScope.date.getFullYear()).toEqual(1980); - expect($rootScope.date.getMonth()).toEqual(2); - expect($rootScope.date.getDate()).toEqual(5); + it('closes when click outside of calendar', function() { + $document.find('body').click(); + expect(dropdownEl.css('display')).toBe('none'); + }); - expect(getOptions()).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( 1, 3 ); - }); + 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('closes when click outside of calendar', function() { - $document.find('body').click(); - expect(dropdownEl.css('display')).toBe('none'); }); describe('toggles programatically by `open` attribute', function () { @@ -1138,39 +1156,45 @@ describe('datepicker directive', function () { describe('button bar', function() { var buttons, buttonBarElement; - beforeEach(inject(function() { - assignButtonBar(); - })); function assignButtonBar() { buttonBarElement = dropdownEl.find('li').eq(-1); buttons = buttonBarElement.find('button'); } - it('should be visible', function() { - expect(buttonBarElement.css('display')).not.toBe('none'); - }); + describe('', function () { + beforeEach(inject(function() { + var wrapElement = $compile('
    ')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + assignButtonBar(); + })); - it('should have four buttons', function() { - expect(buttons.length).toBe(4); + it('should be visible', function() { + expect(buttonBarElement.css('display')).not.toBe('none'); + }); - expect(buttons.eq(0).text()).toBe('Today'); - expect(buttons.eq(1).text()).toBe('Weeks'); - expect(buttons.eq(2).text()).toBe('Clear'); - expect(buttons.eq(3).text()).toBe('Done'); - }); + it('should have four buttons', function() { + expect(buttons.length).toBe(4); - it('should have a button to clear value', function() { - buttons.eq(2).click(); - expect($rootScope.date).toBe(null); - }); + expect(buttons.eq(0).text()).toBe('Today'); + expect(buttons.eq(1).text()).toBe('Weeks'); + expect(buttons.eq(2).text()).toBe('Clear'); + expect(buttons.eq(3).text()).toBe('Done'); + }); - it('should have a button to close calendar', function() { - inputEl.focus(); - expect(dropdownEl.css('display')).not.toBe('none'); + it('should have a button to clear value', function() { + buttons.eq(2).click(); + expect($rootScope.date).toBe(null); + }); - buttons.eq(3).click(); - expect(dropdownEl.css('display')).toBe('none'); + it('should have a button to close calendar', function() { + inputEl.focus(); + expect(dropdownEl.css('display')).not.toBe('none'); + + buttons.eq(3).click(); + expect(dropdownEl.css('display')).toBe('none'); + }); }); describe('customization', function() { @@ -1270,17 +1294,6 @@ describe('datepicker directive', function () { }); }); - describe('to invalid input', function() { - it('sets `ng-invalid`', function() { - changeInputValueTo(inputEl, 'pizza'); - - expect(inputEl).toHaveClass('ng-invalid'); - expect(inputEl).toHaveClass('ng-invalid-date'); - expect($rootScope.date).toBeUndefined(); - expect(inputEl.val()).toBe('pizza'); - }); - }); - describe('with an append-to-body attribute', function() { beforeEach(inject(function($rootScope) { $rootScope.date = new Date(); From e986485a6e31055980d3c5a279ea8dff84bb35e3 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Mon, 30 Dec 2013 16:01:02 +0100 Subject: [PATCH 0208/1761] chore(build): add banner into build files Closes #1493 --- Gruntfile.js | 19 ++++++++++++++----- misc/demo/index.html | 2 +- package.json | 4 +++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 49df257fa3..b140a35b63 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -26,7 +26,13 @@ module.exports = function(grunt) { meta: { modules: 'angular.module("ui.bootstrap", [<%= srcModules %>]);', tplmodules: 'angular.module("ui.bootstrap.tpls", [<%= tplModules %>]);', - all: 'angular.module("ui.bootstrap", ["ui.bootstrap.tpls", <%= srcModules %>]);' + all: 'angular.module("ui.bootstrap", ["ui.bootstrap.tpls", <%= srcModules %>]);', + banner: ['/*', + ' * <%= pkg.name %>', + ' * <%= pkg.homepage %>\n', + ' * Version: <%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>', + ' * License: <%= pkg.license %>', + ' */\n'].join('\n') }, delta: { docs: { @@ -46,14 +52,14 @@ module.exports = function(grunt) { concat: { dist: { options: { - banner: '<%= meta.modules %>\n' + banner: '<%= meta.banner %><%= meta.modules %>\n' }, src: [], //src filled in by build task dest: '<%= dist %>/<%= filename %>-<%= pkg.version %>.js' }, dist_tpls: { options: { - banner: '<%= meta.all %>\n<%= meta.tplmodules %>\n' + banner: '<%= meta.banner %><%= meta.all %>\n<%= meta.tplmodules %>\n' }, src: [], //src filled in by build task dest: '<%= dist %>/<%= filename %>-tpls-<%= pkg.version %>.js' @@ -83,12 +89,15 @@ module.exports = function(grunt) { } }, uglify: { + options: { + banner: '<%= meta.banner %>' + }, dist:{ - src:['<%= dist %>/<%= filename %>-<%= pkg.version %>.js'], + src:['<%= concat.dist.dest %>'], dest:'<%= dist %>/<%= filename %>-<%= pkg.version %>.min.js' }, dist_tpls:{ - src:['<%= dist %>/<%= filename %>-tpls-<%= pkg.version %>.js'], + src:['<%= concat.dist_tpls.dest %>'], dest:'<%= dist %>/<%= filename %>-tpls-<%= pkg.version %>.min.js' } }, diff --git a/misc/demo/index.html b/misc/demo/index.html index feb7b87a79..4257334153 100644 --- a/misc/demo/index.html +++ b/misc/demo/index.html @@ -212,7 +212,7 @@

    <%= module.displayName %> diff --git a/package.json b/package.json index 86ae0221b6..2daf709421 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "author": "https://github.com/angular-ui/bootstrap/graphs/contributors", "name": "angular-ui-bootstrap", "version": "0.10.0-SNAPSHOT", + "homepage": "http://angular-ui.github.io/bootstrap/", "dependencies": {}, "devDependencies": { "grunt": "~0.4.1", @@ -17,5 +18,6 @@ "node-markdown": "0.1.1", "semver": "~1.1.4", "shelljs": "~0.1.4" - } + }, + "license": "MIT" } From 0754ad7b5c7ef7b018d0caebdb8799f24bd0cd8c Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Mon, 30 Dec 2013 15:53:09 -0800 Subject: [PATCH 0209/1761] fix(modal): leaking watchers due to scope re-use Previously, the backdropScope was being re-used and each time it was linked, it would attach more watchers to the scope. Closes #1491 Closes #1498 --- src/modal/modal.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/modal/modal.js b/src/modal/modal.js index adcc95fe65..f04e02d58d 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -110,8 +110,7 @@ angular.module('ui.bootstrap.modal', []) var OPENED_MODAL_CLASS = 'modal-open'; - var backdropjqLiteEl, backdropDomEl; - var backdropScope = $rootScope.$new(true); + var backdropDomEl, backdropScope; var openedWindows = $$stackedMap.createNew(); var $modalStack = {}; @@ -127,7 +126,9 @@ angular.module('ui.bootstrap.modal', []) } $rootScope.$watch(backdropIndex, function(newBackdropIndex){ - backdropScope.index = newBackdropIndex; + if (backdropScope) { + backdropScope.index = newBackdropIndex; + } }); function removeModalWindow(modalInstance) { @@ -146,6 +147,9 @@ angular.module('ui.bootstrap.modal', []) if (backdropDomEl && backdropIndex() == -1) { backdropDomEl.remove(); backdropDomEl = undefined; + + backdropScope.$destroy(); + backdropScope = undefined; } //destroy scope @@ -174,12 +178,14 @@ angular.module('ui.bootstrap.modal', []) keyboard: modal.keyboard }); - var body = $document.find('body').eq(0); + var body = $document.find('body').eq(0), + currBackdropIndex = backdropIndex(); - if (backdropIndex() >= 0 && !backdropDomEl) { - backdropjqLiteEl = angular.element('
    '); - backdropDomEl = $compile(backdropjqLiteEl)(backdropScope); - body.append(backdropDomEl); + if (currBackdropIndex >= 0 && !backdropDomEl) { + backdropScope = $rootScope.$new(true); + backdropScope.index = currBackdropIndex; + backdropDomEl = $compile('
    ')(backdropScope); + body.append(backdropDomEl); } var angularDomEl = angular.element('
    '); From ab92f5a721d2685f270d3fed9c8345f0c1f8ba77 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Mon, 30 Dec 2013 16:40:55 +0100 Subject: [PATCH 0210/1761] demo(all): use modal for download process Closes #1494 --- misc/demo/assets/app.js | 45 +++++++++++++++++++++++++++++++++++++++-- misc/demo/index.html | 33 +++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/misc/demo/assets/app.js b/misc/demo/assets/app.js index bb47105830..829f4a0b86 100644 --- a/misc/demo/assets/app.js +++ b/misc/demo/assets/app.js @@ -9,6 +9,11 @@ function MainCtrl($scope, $http, $document, $modal, orderByFilter) { var $iframe = angular.element(' + allowtransparency="true" frameborder="0" scrolling="0" width="110" height="20">
  • Add Slide +
    Interval, in milliseconds: diff --git a/template/carousel/carousel.html b/template/carousel/carousel.html index 167996563e..d622e7e98f 100644 --- a/template/carousel/carousel.html +++ b/template/carousel/carousel.html @@ -1,8 +1,8 @@ From 467dd159b364fa034e7d3ec33f421f861cdca12f Mon Sep 17 00:00:00 2001 From: Ricki Runge Date: Sun, 23 Feb 2014 17:58:44 +0100 Subject: [PATCH 0325/1761] fix(datepicker): mark input field as invalid if the date is invalid Closes #1845 --- src/datepicker/datepicker.js | 2 +- src/datepicker/test/datepicker.spec.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index 599f3c3562..b0a3b932a1 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -368,7 +368,7 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon if (!viewValue) { ngModel.$setValidity('date', true); return null; - } else if (angular.isDate(viewValue)) { + } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { ngModel.$setValidity('date', true); return viewValue; } else if (angular.isString(viewValue)) { diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index 9ecec1c69c..575dae700c 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -248,6 +248,15 @@ describe('datepicker directive', function () { testCalendar(); expect(angular.isDate($rootScope.date)).toBe(true); }); + + it('to a date that is invalid, it gets invalid', function() { + $rootScope.date = new Date('pizza'); + $rootScope.$digest(); + expect(element.hasClass('ng-invalid')).toBeTruthy(); + expect(element.hasClass('ng-invalid-date')).toBeTruthy(); + expect(angular.isDate($rootScope.date)).toBe(true); + expect(isNaN($rootScope.date)).toBe(true); + }); }); describe('not to a Date object', function() { From 93da30d559481cc603586420341d32a5fc23bd58 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sat, 15 Feb 2014 18:00:09 -0300 Subject: [PATCH 0326/1761] fix(datepicker): rename `dateFormat` to `datepickerPopup` in datepickerPopupConfig Closes #1810 --- src/datepicker/datepicker.js | 4 ++-- src/datepicker/test/datepicker.spec.js | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index b0a3b932a1..4dae6719e6 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -296,7 +296,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) }) .constant('datepickerPopupConfig', { - dateFormat: 'yyyy-MM-dd', + datepickerPopup: 'yyyy-MM-dd', currentText: 'Today', clearText: 'Clear', closeText: 'Done', @@ -328,7 +328,7 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon }; attrs.$observe('datepickerPopup', function(value) { - dateFormat = value || datepickerPopupConfig.dateFormat; + dateFormat = value || datepickerPopupConfig.datepickerPopup; ngModel.$render(); }); diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index 575dae700c..d8083c3ce3 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -795,6 +795,26 @@ describe('datepicker directive', function () { }); + describe('setting datepickerPopupConfig', function() { + var originalConfig = {}; + beforeEach(inject(function(datepickerPopupConfig) { + angular.extend(originalConfig, datepickerPopupConfig); + datepickerPopupConfig.datepickerPopup = 'MM-dd-yyyy'; + + element = $compile('')($rootScope); + $rootScope.$digest(); + })); + afterEach(inject(function(datepickerPopupConfig) { + // return it to the original state + angular.extend(datepickerPopupConfig, originalConfig); + })); + + it('changes date format', function() { + expect(element.val()).toEqual('09-30-2010'); + }); + + }); + describe('as popup', function () { var inputEl, dropdownEl, changeInputValueTo, $document; From 890e2d37c1b7fba0f4995f45ab0baf9fc727ee2d Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Wed, 26 Feb 2014 22:09:33 +0100 Subject: [PATCH 0327/1761] fix(dropdown): unbind toggle element event on scope destroy Also, change the way disabled dropdownToggle is read from `attrs` instead of element property. Closes #1867 Closes #1870 --- src/dropdown/docs/demo.html | 3 ++- src/dropdown/dropdown.js | 12 +++++++++--- src/dropdown/test/dropdown.spec.js | 29 +++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/dropdown/docs/demo.html b/src/dropdown/docs/demo.html index c58117de1f..27e5e4a7ed 100644 --- a/src/dropdown/docs/demo.html +++ b/src/dropdown/docs/demo.html @@ -14,7 +14,7 @@
    -
    diff --git a/src/dropdown/dropdown.js b/src/dropdown/dropdown.js index c7a24bd7db..53a3c36e23 100644 --- a/src/dropdown/dropdown.js +++ b/src/dropdown/dropdown.js @@ -112,22 +112,28 @@ angular.module('ui.bootstrap.dropdown', []) return; } - element.bind('click', function(event) { + var toggleDropdown = function(event) { event.preventDefault(); event.stopPropagation(); - if ( !element.hasClass('disabled') && !element.prop('disabled') ) { + if ( !element.hasClass('disabled') && !attrs.disabled ) { scope.$apply(function() { dropdownCtrl.toggle(); }); } - }); + }; + + element.bind('click', toggleDropdown); // WAI-ARIA element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { element.attr('aria-expanded', !!isOpen); }); + + scope.$on('$destroy', function() { + element.unbind('click', toggleDropdown); + }); } }; }); diff --git a/src/dropdown/test/dropdown.spec.js b/src/dropdown/test/dropdown.spec.js index a0a4c51f72..eceb6bfcb0 100644 --- a/src/dropdown/test/dropdown.spec.js +++ b/src/dropdown/test/dropdown.spec.js @@ -91,6 +91,35 @@ describe('dropdownToggle', function() { expect(elm.hasClass('open')).toBe(false); }); + it('should not toggle if the element has `ng-disabled` as true', function() { + $rootScope.isdisabled = true; + var elm = $compile('')($rootScope); + $rootScope.$digest(); + elm.find('div').click(); + expect(elm.hasClass('open')).toBe(false); + + $rootScope.isdisabled = false; + $rootScope.$digest(); + elm.find('div').click(); + expect(elm.hasClass('open')).toBe(true); + }); + + it('should unbind events on scope destroy', function() { + var $scope = $rootScope.$new(); + var elm = $compile('')($scope); + $scope.$digest(); + + var buttonEl = elm.find('button'); + buttonEl.click(); + expect(elm.hasClass('open')).toBe(true); + buttonEl.click(); + expect(elm.hasClass('open')).toBe(false); + + $scope.$destroy(); + buttonEl.click(); + expect(elm.hasClass('open')).toBe(false); + }); + // issue 270 it('executes other document click events normally', function() { var checkboxEl = $compile('')($rootScope); From 341b5b588666b2dbe864e6423a3fa60f44fd9014 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Wed, 5 Mar 2014 15:43:13 +0100 Subject: [PATCH 0328/1761] demo(datepicker): add type attribute to buttons Closes #1890 Closes #1894 --- src/datepicker/docs/demo.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/datepicker/docs/demo.html b/src/datepicker/docs/demo.html index 09c2071a23..cc4406dd0c 100644 --- a/src/datepicker/docs/demo.html +++ b/src/datepicker/docs/demo.html @@ -12,7 +12,7 @@

    Popup

    - +

    @@ -24,8 +24,8 @@

    Popup


    - - - - + + + +
    \ No newline at end of file From c6c0e2d9ef39592cf3558e53f5f216daa2f6c215 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sat, 8 Mar 2014 18:09:52 +0100 Subject: [PATCH 0329/1761] chore(ci): switch off IE testing on TravisCI --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index b8697a1837..029f70ecc8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -134,7 +134,7 @@ module.exports = function(grunt) { singleRun: true, colors: false, reporters: ['dots', 'junit'], - browsers: ['Chrome', 'ChromeCanary', 'Firefox', 'Opera', '/Users/jenkins/bin/safari.sh', '/Users/jenkins/bin/ie9.sh', '/Users/jenkins/bin/ie10.sh', '/Users/jenkins/bin/ie11.sh'] + browsers: ['Chrome', 'ChromeCanary', 'Firefox', 'Opera', '/Users/jenkins/bin/safari.sh'] }, travis: { singleRun: true, From 6a830116f25bcedac37dc84b457dff3118a4f4e7 Mon Sep 17 00:00:00 2001 From: Michal Charemza Date: Fri, 7 Mar 2014 16:34:32 +0000 Subject: [PATCH 0330/1761] fix(typeahead): loading callback updates after blur Fixes #1822 Closes #1904 --- src/typeahead/test/typeahead.spec.js | 19 +++++++++++++++++++ src/typeahead/typeahead.js | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index 1e2ec60b68..59f3f661aa 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -507,6 +507,25 @@ describe('typeahead tests', function () { expect(element).toBeClosed(); }); + it('should properly update loading callback if an element is not focused', function () { + + $scope.items = function(viewValue) { + return $timeout(function(){ + return [viewValue]; + }); + }; + var element = prepareInputEl('
    '); + var inputEl = findInput(element); + + changeInputValueTo(element, 'match'); + $scope.$digest(); + + inputEl.blur(); + $timeout.flush(); + + expect($scope.isLoading).toBeFalsy(); + }); + it('issue 1140 - should properly update loading callback when deleting characters', function () { $scope.items = function(viewValue) { diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index e0cb5b9f6a..7148ce3031 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -110,7 +110,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //it might happen that several async queries were in progress if a user were typing fast //but we are interested only in responses that correspond to the current view value - if (inputValue === modelCtrl.$viewValue && hasFocus) { + var onCurrentRequest = (inputValue === modelCtrl.$viewValue); + if (onCurrentRequest && hasFocus) { if (matches.length > 0) { scope.activeIdx = 0; @@ -136,6 +137,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } else { resetMatches(); } + } + if (onCurrentRequest) { isLoadingSetter(originalScope, false); } }, function(){ From d89bbd1008d8c97c46c675a71ee97f833c7dfa16 Mon Sep 17 00:00:00 2001 From: Jesus Rodriguez Date: Wed, 5 Mar 2014 18:41:43 +0100 Subject: [PATCH 0331/1761] test(pagination): clean some injections Closes #1895 --- src/pagination/test/pager.spec.js | 16 +++++------ src/pagination/test/pagination.spec.js | 38 +++++++++++++------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/pagination/test/pager.spec.js b/src/pagination/test/pager.spec.js index 3065ba927c..5a6010b2ad 100644 --- a/src/pagination/test/pager.spec.js +++ b/src/pagination/test/pager.spec.js @@ -97,11 +97,11 @@ describe('pager directive', function () { }); describe('`items-per-page`', function () { - beforeEach(inject(function() { + beforeEach(function() { $rootScope.perpage = 5; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); - })); + }); it('does not change the number of pages', function() { expect(getPaginationBarSize()).toBe(2); @@ -116,7 +116,7 @@ describe('pager directive', function () { expect($rootScope.currentPage).toBe(2); expect(getPaginationBarSize()).toBe(2); expect(getPaginationEl(0)).not.toHaveClass('disabled'); - expect(getPaginationEl(1)).toHaveClass('disabled'); + expect(getPaginationEl(-1)).toHaveClass('disabled'); }); }); @@ -131,11 +131,11 @@ describe('pager directive', function () { }); describe('`num-pages`', function () { - beforeEach(inject(function() { + beforeEach(function() { $rootScope.numpg = null; element = $compile('')($rootScope); $rootScope.$digest(); - })); + }); it('equals to total number of pages', function() { expect($rootScope.numpg).toBe(5); @@ -169,10 +169,10 @@ describe('pager directive', function () { }); describe('override configuration from attributes', function () { - beforeEach(inject(function() { + beforeEach(function() { element = $compile('')($rootScope); $rootScope.$digest(); - })); + }); it('contains 2 li elements', function() { expect(getPaginationBarSize()).toBe(2); diff --git a/src/pagination/test/pagination.spec.js b/src/pagination/test/pagination.spec.js index 7259146a01..70ace520a5 100644 --- a/src/pagination/test/pagination.spec.js +++ b/src/pagination/test/pagination.spec.js @@ -123,11 +123,11 @@ describe('pagination directive', function () { }); describe('`items-per-page`', function () { - beforeEach(inject(function() { + beforeEach(function() { $rootScope.perpage = 5; element = $compile('')($rootScope); $rootScope.$digest(); - })); + }); it('changes the number of pages', function() { expect(getPaginationBarSize()).toBe(12); @@ -166,11 +166,11 @@ describe('pagination directive', function () { }); describe('executes `ng-change` expression', function () { - beforeEach(inject(function() { + beforeEach(function() { $rootScope.selectPageHandler = jasmine.createSpy('selectPageHandler'); element = $compile('')($rootScope); $rootScope.$digest(); - })); + }); it('when an element is clicked', function() { clickPaginationEl(2); @@ -194,13 +194,13 @@ describe('pagination directive', function () { }); describe('with `max-size` option', function () { - beforeEach(inject(function() { + beforeEach(function() { $rootScope.total = 98; // 10 pages $rootScope.currentPage = 3; $rootScope.maxSize = 5; element = $compile('')($rootScope); $rootScope.$digest(); - })); + }); it('contains maxsize + 2 li elements', function() { expect(getPaginationBarSize()).toBe($rootScope.maxSize + 2); @@ -230,7 +230,7 @@ describe('pagination directive', function () { clickPaginationEl(0); expect($rootScope.currentPage).toBe(6); - expect(getPaginationEl(3)).toHaveClass('active'); + expect(getPaginationEl(3)).toHaveClass('active'); expect(getPaginationEl(3).text()).toBe(''+$rootScope.currentPage); }); @@ -262,14 +262,14 @@ describe('pagination directive', function () { }); describe('with `max-size` option & no `rotate`', function () { - beforeEach(inject(function() { + beforeEach(function() { $rootScope.total = 115; // 12 pages $rootScope.currentPage = 7; $rootScope.maxSize = 5; $rootScope.rotate = false; element = $compile('')($rootScope); $rootScope.$digest(); - })); + }); it('contains maxsize + 4 elements', function() { expect(getPaginationBarSize()).toBe($rootScope.maxSize + 4); @@ -325,10 +325,10 @@ describe('pagination directive', function () { }); describe('pagination directive with `boundary-links`', function () { - beforeEach(inject(function() { + beforeEach(function() { element = $compile('')($rootScope); $rootScope.$digest(); - })); + }); it('contains num-pages + 4 li elements', function() { expect(getPaginationBarSize()).toBe(9); @@ -418,10 +418,10 @@ describe('pagination directive', function () { }); describe('pagination directive with just number links', function () { - beforeEach(inject(function() { + beforeEach(function() { element = $compile('')($rootScope); $rootScope.$digest(); - })); + }); it('contains num-pages li elements', function() { expect(getPaginationBarSize()).toBe(5); @@ -469,11 +469,11 @@ describe('pagination directive', function () { }); describe('with just boundary & number links', function () { - beforeEach(inject(function() { + beforeEach(function() { $rootScope.directions = false; element = $compile('')($rootScope); $rootScope.$digest(); - })); + }); it('contains number of pages + 2 li elements', function() { expect(getPaginationBarSize()).toBe(7); @@ -501,11 +501,11 @@ describe('pagination directive', function () { }); describe('`num-pages`', function () { - beforeEach(inject(function() { + beforeEach(function() { $rootScope.numpg = null; element = $compile('')($rootScope); $rootScope.$digest(); - })); + }); it('equals to total number of pages', function() { expect($rootScope.numpg).toBe(5); @@ -573,10 +573,10 @@ describe('pagination directive', function () { }); describe('override configuration from attributes', function () { - beforeEach(inject(function() { + beforeEach(function() { element = $compile('')($rootScope); $rootScope.$digest(); - })); + }); it('contains number of pages + 4 li elements', function() { expect(getPaginationBarSize()).toBe(9); From f715d0522589cbb16bbe7498573da5d7aad8802b Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Fri, 7 Mar 2014 11:47:30 +0100 Subject: [PATCH 0332/1761] feat(dropdown): focus toggle element when opening or closing with Esc` * Improve accessibility. Closes #1908. --- src/dropdown/dropdown.js | 20 +++++++++++++++----- src/dropdown/test/dropdown.spec.js | 25 ++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/dropdown/dropdown.js b/src/dropdown/dropdown.js index 53a3c36e23..c674f44ee0 100644 --- a/src/dropdown/dropdown.js +++ b/src/dropdown/dropdown.js @@ -36,6 +36,7 @@ angular.module('ui.bootstrap.dropdown', []) var escapeKeyBind = function( evt ) { if ( evt.which === 27 ) { + openScope.focusToggleElement(); closeDropdown(); } }; @@ -71,17 +72,24 @@ angular.module('ui.bootstrap.dropdown', []) return scope.isOpen; }; - scope.$watch('isOpen', function( value ) { - $animate[value ? 'addClass' : 'removeClass'](self.$element, openClass); + scope.focusToggleElement = function() { + if ( self.toggleElement ) { + self.toggleElement[0].focus(); + } + }; - if ( value ) { + scope.$watch('isOpen', function( isOpen ) { + $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); + + if ( isOpen ) { + scope.focusToggleElement(); dropdownService.open( scope ); } else { dropdownService.close( scope ); } - setIsOpen($scope, value); - toggleInvoker($scope, { open: !!value }); + setIsOpen($scope, isOpen); + toggleInvoker($scope, { open: !!isOpen }); }); $scope.$on('$locationChangeSuccess', function() { @@ -112,6 +120,8 @@ angular.module('ui.bootstrap.dropdown', []) return; } + dropdownCtrl.toggleElement = element; + var toggleDropdown = function(event) { event.preventDefault(); event.stopPropagation(); diff --git a/src/dropdown/test/dropdown.spec.js b/src/dropdown/test/dropdown.spec.js index eceb6bfcb0..77c7f509b9 100644 --- a/src/dropdown/test/dropdown.spec.js +++ b/src/dropdown/test/dropdown.spec.js @@ -20,9 +20,13 @@ describe('dropdownToggle', function() { element.trigger(e); }; + var isFocused = function(elm) { + return elm[0] === document.activeElement; + }; + describe('basic', function() { function dropdown() { - return $compile('')($rootScope); + return $compile('')($rootScope); } beforeEach(function() { @@ -44,10 +48,13 @@ describe('dropdownToggle', function() { expect(element.hasClass('open')).toBe(false); }); - it('should close on escape key', function() { + it('should close on escape key & focus toggle element', function() { + $document.find('body').append(element); clickDropdownToggle(); triggerKeyDown($document, 27); expect(element.hasClass('open')).toBe(false); + expect(isFocused(element.find('a'))).toBe(true); + element.remove(); }); it('should not close on backspace key', function() { @@ -170,7 +177,7 @@ describe('dropdownToggle', function() { describe('`is-open`', function() { beforeEach(function() { $rootScope.isopen = true; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); }); @@ -188,6 +195,18 @@ describe('dropdownToggle', function() { $rootScope.$digest(); expect(element.hasClass('open')).toBe(false); }); + + it('focus toggle element when opening', function() { + $document.find('body').append(element); + clickDropdownToggle(); + $rootScope.isopen = false; + $rootScope.$digest(); + expect(isFocused(element.find('a'))).toBe(false); + $rootScope.isopen = true; + $rootScope.$digest(); + expect(isFocused(element.find('a'))).toBe(true); + element.remove(); + }); }); describe('`on-toggle`', function() { From f9b6c49623e863969a5371646eb4d385319c1bda Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sat, 8 Mar 2014 20:33:43 +0100 Subject: [PATCH 0333/1761] fix(timepicker): evaluate correctly the `readonly-input` attribute Closes #1911 --- src/timepicker/test/timepicker.spec.js | 14 ++++++++++++++ src/timepicker/timepicker.js | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/timepicker/test/timepicker.spec.js b/src/timepicker/test/timepicker.spec.js index 9d329ccf64..7958b2d463 100644 --- a/src/timepicker/test/timepicker.spec.js +++ b/src/timepicker/test/timepicker.spec.js @@ -601,6 +601,20 @@ describe('timepicker directive', function () { }); }); + describe('`readonly-input` attribute', function() { + beforeEach(inject(function() { + $rootScope.meridiansArray = ['am', 'pm']; + element = $compile('')($rootScope); + $rootScope.$digest(); + })); + + it('should make inputs readonly', function () { + var inputs = element.find('input'); + expect(inputs.eq(0).attr('readonly')).toBe('readonly'); + expect(inputs.eq(1).attr('readonly')).toBe('readonly'); + }); + }); + describe('setting timepickerConfig steps', function() { var originalConfig = {}; beforeEach(inject(function(_$compile_, _$rootScope_, timepickerConfig) { diff --git a/src/timepicker/timepicker.js b/src/timepicker/timepicker.js index 5cce447415..045e19b989 100644 --- a/src/timepicker/timepicker.js +++ b/src/timepicker/timepicker.js @@ -26,7 +26,7 @@ angular.module('ui.bootstrap.timepicker', []) this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); } - $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; + $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; this.setupInputEvents( hoursInputEl, minutesInputEl ); }; @@ -243,7 +243,7 @@ angular.module('ui.bootstrap.timepicker', []) replace: true, scope: {}, templateUrl: 'template/timepicker/timepicker.html', - link: function(sscope, element, attrs, ctrls) { + link: function(scope, element, attrs, ctrls) { var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if ( ngModelCtrl ) { From f9aa4590e94991fb0b3f414f1c96489a96717719 Mon Sep 17 00:00:00 2001 From: Jesus Rodriguez Date: Tue, 4 Mar 2014 20:06:31 +0100 Subject: [PATCH 0334/1761] fix(karma): exclude demo files Closes #1889 --- karma.conf.js | 1 + 1 file changed, 1 insertion(+) diff --git a/karma.conf.js b/karma.conf.js index 086e2dc916..14a61d5de3 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -16,6 +16,7 @@ files = [ // list of files to exclude exclude = [ + 'src/**/docs/*' ]; // Start these browsers, currently available: From d0024931dee6e35d9e49a1050be3ec60745042f1 Mon Sep 17 00:00:00 2001 From: Shayan Elhami Date: Wed, 12 Feb 2014 22:36:44 +0000 Subject: [PATCH 0335/1761] fix(typeahead): incompatibility with ng-focus Used timeout to avoid $rootScope:inprog error Fixes #1773 Closes #1793 --- src/typeahead/test/typeahead.spec.js | 17 +++++++++++++++++ src/typeahead/typeahead.js | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index 59f3f661aa..1561361ee6 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -575,6 +575,23 @@ describe('typeahead tests', function () { expect(element).toBeOpenWithActive(2, 0); }); + + it('issue #1773 - should not trigger an error when used with ng-focus', function () { + + var element = prepareInputEl('
    '); + var inputEl = findInput(element); + + // Note that this bug can only be found when element is in the document + $document.find('body').append(element); + // Extra teardown for this spec + this.after(function () { element.remove(); }); + + changeInputValueTo(element, 'b'); + var match = $(findMatches(element)[1]).find('a')[0]; + + $(match).click(); + $scope.$digest(); + }); }); describe('input formatting', function () { diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 7148ce3031..123d28bc09 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -233,7 +233,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap resetMatches(); //return focus to the input element if a mach was selected via a mouse click event - element[0].focus(); + // use timeout to avoid $rootScope:inprog error + $timeout(function() { element[0].focus(); }, 0, false); }; //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) From 4b811b7adc84d752599d06f5f82db5f4d5d3b058 Mon Sep 17 00:00:00 2001 From: MacTEC Date: Wed, 12 Mar 2014 16:14:49 +0000 Subject: [PATCH 0336/1761] chore(typeahead): fix typo in comment Closes #1925 --- src/typeahead/typeahead.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 123d28bc09..7151b81bb8 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -232,7 +232,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap resetMatches(); - //return focus to the input element if a mach was selected via a mouse click event + //return focus to the input element if a match was selected via a mouse click event // use timeout to avoid $rootScope:inprog error $timeout(function() { element[0].focus(); }, 0, false); }; @@ -358,4 +358,4 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap return function(matchItem, query) { return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; }; - }); \ No newline at end of file + }); From 3b6ab25b5c227ee21fc863ffe523e719475dbca5 Mon Sep 17 00:00:00 2001 From: tomchentw Date: Wed, 2 Apr 2014 13:49:26 +0800 Subject: [PATCH 0337/1761] fix(carousel): correct glyphicon Fixes #2006 Closes #2014 --- template/carousel/carousel.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/template/carousel/carousel.html b/template/carousel/carousel.html index d622e7e98f..9769b383a6 100644 --- a/template/carousel/carousel.html +++ b/template/carousel/carousel.html @@ -3,6 +3,6 @@
  • - - + +
    From f48e4339e964dd6574f5aa99948f3709f0eba809 Mon Sep 17 00:00:00 2001 From: Philipp Denzler Date: Sat, 5 Apr 2014 22:59:28 +0200 Subject: [PATCH 0338/1761] chore(dropdown): remove unused var Closes #2023 --- src/dropdown/dropdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dropdown/dropdown.js b/src/dropdown/dropdown.js index c674f44ee0..f1c138a670 100644 --- a/src/dropdown/dropdown.js +++ b/src/dropdown/dropdown.js @@ -5,7 +5,7 @@ angular.module('ui.bootstrap.dropdown', []) }) .service('dropdownService', ['$document', function($document) { - var self = this, openScope = null; + var openScope = null; this.open = function( dropdownScope ) { if ( !openScope ) { From 82df4fb1c48520396c4af2400af632fcfeb1f611 Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Thu, 6 Feb 2014 12:13:27 -0600 Subject: [PATCH 0339/1761] feat(button): allow uncheckable radio button Acts as a hybrid checkbox/radio-button, selecting exclusively among the button set, but also allowing the selected item to be unselected, leaving the button set without a selected item. Closes #1760 --- src/buttons/buttons.js | 6 ++-- src/buttons/docs/demo.html | 9 ++++-- src/buttons/docs/readme.md | 3 +- src/buttons/test/buttons.spec.js | 48 ++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/buttons/buttons.js b/src/buttons/buttons.js index 913523ed16..27237be388 100644 --- a/src/buttons/buttons.js +++ b/src/buttons/buttons.js @@ -24,9 +24,11 @@ angular.module('ui.bootstrap.buttons', []) //ui->model element.bind(buttonsCtrl.toggleEvent, function () { - if (!element.hasClass(buttonsCtrl.activeClass)) { + var isActive = element.hasClass(buttonsCtrl.activeClass); + + if (!isActive || angular.isDefined(attrs.uncheckable)) { scope.$apply(function () { - ngModelCtrl.$setViewValue(scope.$eval(attrs.btnRadio)); + ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); ngModelCtrl.$render(); }); } diff --git a/src/buttons/docs/demo.html b/src/buttons/docs/demo.html index 9b2c6bb088..3c7bf2bfc1 100644 --- a/src/buttons/docs/demo.html +++ b/src/buttons/docs/demo.html @@ -11,11 +11,16 @@

    Checkbox

    -

    Radio

    -
    {{radioModel}}
    +

    Radio & Uncheckable Radio

    +
    {{radioModel || 'null'}}
    +
    + + + +
    \ No newline at end of file diff --git a/src/buttons/docs/readme.md b/src/buttons/docs/readme.md index 73ef6003d1..82e736b107 100644 --- a/src/buttons/docs/readme.md +++ b/src/buttons/docs/readme.md @@ -1,2 +1 @@ -There are 2 directives that can make a group of buttons to behave like a set of checkboxes or radio buttons. - +There are two directives that can make a group of buttons behave like a set of checkboxes, radio buttons, or a hybrid where radio buttons can be unchecked. diff --git a/src/buttons/test/buttons.spec.js b/src/buttons/test/buttons.spec.js index 5b4db2c42c..4774b8b5c8 100644 --- a/src/buttons/test/buttons.spec.js +++ b/src/buttons/test/buttons.spec.js @@ -136,5 +136,53 @@ describe('buttons', function () { expect(btns.eq(0)).not.toHaveClass('active'); expect(btns.eq(1)).toHaveClass('active'); }); + + describe('uncheckable', function () { + //model -> UI + it('should set active class based on model', function () { + var btns = compileButtons('', $scope); + expect(btns.eq(0)).not.toHaveClass('active'); + expect(btns.eq(1)).not.toHaveClass('active'); + + $scope.model = 2; + $scope.$digest(); + expect(btns.eq(0)).not.toHaveClass('active'); + expect(btns.eq(1)).toHaveClass('active'); + }); + + //UI->model + it('should unset active class based on model', function () { + var btns = compileButtons('', $scope); + expect($scope.model).toBeUndefined(); + + btns.eq(0).click(); + expect($scope.model).toEqual(1); + expect(btns.eq(0)).toHaveClass('active'); + expect(btns.eq(1)).not.toHaveClass('active'); + + btns.eq(0).click(); + expect($scope.model).toEqual(undefined); + expect(btns.eq(1)).not.toHaveClass('active'); + expect(btns.eq(0)).not.toHaveClass('active'); + }); + + it('should watch btn-radio values and update state', function () { + $scope.values = ['value1', 'value2']; + + var btns = compileButtons('', $scope); + expect(btns.eq(0)).not.toHaveClass('active'); + expect(btns.eq(1)).not.toHaveClass('active'); + + $scope.model = 'value2'; + $scope.$digest(); + expect(btns.eq(0)).not.toHaveClass('active'); + expect(btns.eq(1)).toHaveClass('active'); + + $scope.model = undefined; + $scope.$digest(); + expect(btns.eq(0)).not.toHaveClass('active'); + expect(btns.eq(1)).not.toHaveClass('active'); + }); + }); }); }); \ No newline at end of file From 4c76a858de7bd85b7f81a2e50f5c74723b058df2 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Mon, 10 Mar 2014 23:37:13 +0100 Subject: [PATCH 0340/1761] feat(typeahead): add `aria-owns` & `aria-activedescendant` roles --- src/typeahead/test/typeahead.spec.js | 8 ++++++ src/typeahead/typeahead.js | 34 +++++++++++++++++++------ template/typeahead/typeahead-popup.html | 2 +- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index 1561361ee6..790f7ee535 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -131,15 +131,23 @@ describe('typeahead tests', function () { it('should open and close typeahead based on matches', function () { var element = prepareInputEl('
    '); var inputEl = findInput(element); + var ownsId = inputEl.attr('aria-owns'); + expect(inputEl.attr('aria-expanded')).toBe('false'); + expect(inputEl.attr('aria-activedescendant')).toBeUndefined(); changeInputValueTo(element, 'ba'); expect(element).toBeOpenWithActive(2, 0); + expect(findDropDown(element).attr('id')).toBe(ownsId); expect(inputEl.attr('aria-expanded')).toBe('true'); + var activeOptionId = ownsId + '-option-0'; + expect(inputEl.attr('aria-activedescendant')).toBe(activeOptionId); + expect(findDropDown(element).find('li.active').attr('id')).toBe(activeOptionId); changeInputValueTo(element, ''); expect(element).toBeClosed(); expect(inputEl.attr('aria-expanded')).toBe('false'); + expect(inputEl.attr('aria-activedescendant')).toBeUndefined(); }); it('should not open typeahead if input value smaller than a defined threshold', function () { diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 7151b81bb8..8795ef6fe0 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -69,15 +69,25 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap var hasFocus; + //create a child scope for the typeahead directive so we are not polluting original scope + //with typeahead-specific data (matches, query etc.) + var scope = originalScope.$new(); + originalScope.$on('$destroy', function(){ + scope.$destroy(); + }); + // WAI-ARIA + var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); element.attr({ 'aria-autocomplete': 'list', - 'aria-expanded': false + 'aria-expanded': false, + 'aria-owns': popupId }); //pop-up element used to display matches var popUpEl = angular.element('
    '); popUpEl.attr({ + id: popupId, matches: 'matches', active: 'activeIdx', select: 'select(activeIdx)', @@ -89,19 +99,26 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); } - //create a child scope for the typeahead directive so we are not polluting original scope - //with typeahead-specific data (matches, query etc.) - var scope = originalScope.$new(); - originalScope.$on('$destroy', function(){ - scope.$destroy(); - }); - var resetMatches = function() { scope.matches = []; scope.activeIdx = -1; element.attr('aria-expanded', false); }; + var getMatchId = function(index) { + return popupId + '-option-' + index; + }; + + // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. + // This attribute is added or removed automatically when the `activeIdx` changes. + scope.$watch('activeIdx', function(index) { + if (index < 0) { + element.removeAttr('aria-activedescendant'); + } else { + element.attr('aria-activedescendant', getMatchId(index)); + } + }); + var getMatchesAsync = function(inputValue) { var locals = {$viewValue: inputValue}; @@ -121,6 +138,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap for(var i=0; i -
  • +
  • \ No newline at end of file From 004dd1de25681152008fd47d9a055393a1eb52da Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Fri, 4 Apr 2014 11:51:21 +0200 Subject: [PATCH 0341/1761] fix(dropdown): do not call `on-toggle` initially Closes #2021 --- src/dropdown/dropdown.js | 6 ++++-- src/dropdown/test/dropdown.spec.js | 14 ++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/dropdown/dropdown.js b/src/dropdown/dropdown.js index f1c138a670..62c4582d6f 100644 --- a/src/dropdown/dropdown.js +++ b/src/dropdown/dropdown.js @@ -78,7 +78,7 @@ angular.module('ui.bootstrap.dropdown', []) } }; - scope.$watch('isOpen', function( isOpen ) { + scope.$watch('isOpen', function( isOpen, wasOpen ) { $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); if ( isOpen ) { @@ -89,7 +89,9 @@ angular.module('ui.bootstrap.dropdown', []) } setIsOpen($scope, isOpen); - toggleInvoker($scope, { open: !!isOpen }); + if (angular.isDefined(wasOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } }); $scope.$on('$locationChangeSuccess', function() { diff --git a/src/dropdown/test/dropdown.spec.js b/src/dropdown/test/dropdown.spec.js index 77c7f509b9..d0bf45a2bd 100644 --- a/src/dropdown/test/dropdown.spec.js +++ b/src/dropdown/test/dropdown.spec.js @@ -212,16 +212,18 @@ describe('dropdownToggle', function() { describe('`on-toggle`', function() { beforeEach(function() { $rootScope.toggleHandler = jasmine.createSpy('toggleHandler'); - element = $compile('')($rootScope); + $rootScope.isopen = false; + element = $compile('')($rootScope); $rootScope.$digest(); }); - it('should be called initially', function() { - expect($rootScope.toggleHandler).toHaveBeenCalledWith(false); + it('should not have been called initially', function() { + expect($rootScope.toggleHandler).not.toHaveBeenCalled(); }); it('should call it correctly when toggles', function() { - clickDropdownToggle(); + $rootScope.isopen = true; + $rootScope.$digest(); expect($rootScope.toggleHandler).toHaveBeenCalledWith(true); clickDropdownToggle(); @@ -237,8 +239,8 @@ describe('dropdownToggle', function() { $rootScope.$digest(); }); - it('should be called initially with true', function() { - expect($rootScope.toggleHandler).toHaveBeenCalledWith(true); + it('should not have been called initially', function() { + expect($rootScope.toggleHandler).not.toHaveBeenCalled(); }); it('should call it correctly when toggles', function() { From cb31b875f920abbbdd06bebb0f4e6edd90845cda Mon Sep 17 00:00:00 2001 From: Tanguy Krotoff Date: Thu, 27 Mar 2014 11:13:44 +0100 Subject: [PATCH 0342/1761] fix(modal): give a reason of rejection when escape key pressed Fixes #1956 Closes #1991 --- src/modal/modal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modal/modal.js b/src/modal/modal.js index 3b30469a29..c86048ded3 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -200,7 +200,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) modal = openedWindows.top(); if (modal && modal.value.keyboard) { $rootScope.$apply(function () { - $modalStack.dismiss(modal.key); + $modalStack.dismiss(modal.key, 'escape key press'); }); } } From de5a25e6dea362b5c3804c774b5ddc74e336fd21 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sun, 13 Apr 2014 00:14:34 +0300 Subject: [PATCH 0343/1761] docs(typeahead): add `typeahead-on-select` callback parameters Closes #2047 --- src/typeahead/docs/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typeahead/docs/readme.md b/src/typeahead/docs/readme.md index 08975dfc9c..1fcb47fb43 100644 --- a/src/typeahead/docs/readme.md +++ b/src/typeahead/docs/readme.md @@ -40,7 +40,7 @@ The typeahead directives provide several attributes: _(Defaults: 1)_ : Minimal no of characters that needs to be entered before typeahead kicks-in -* `typeahead-on-select` +* `typeahead-on-select($item, $model, $label)` _(Defaults: null)_ : A callback executed when a match is selected From 2423f6d4c05cb0eb3fd2104dedbeb0e3740f7f68 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Wed, 12 Feb 2014 23:12:36 +0100 Subject: [PATCH 0344/1761] feat(datepicker): make widget accessible * keyboard navigation * WAI-ARIA roles * popup will close on escape on input or calendar * handle focus when closing popup Closes #1922 BREAKING CHANGES: popup calendar does not open on input focus --- src/datepicker/datepicker.js | 283 ++++++++++++++----- src/datepicker/docs/readme.md | 21 +- src/datepicker/test/datepicker.spec.js | 361 +++++++++++++++++++++++-- template/datepicker/datepicker.html | 8 +- template/datepicker/day.html | 16 +- template/datepicker/month.html | 12 +- template/datepicker/popup.html | 4 +- template/datepicker/year.html | 12 +- 8 files changed, 590 insertions(+), 127 deletions(-) diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index 4dae6719e6..89498b85a5 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -17,10 +17,13 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) maxDate: null }) -.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig) { +.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { var self = this, ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; + // Modes chain + this.modes = ['day', 'month', 'year']; + // Configuration attributes angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) { @@ -40,7 +43,16 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) }); $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; - this.currentCalendarDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); + $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); + this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); + + $scope.isActive = function(dateObject) { + if (self.compare(dateObject.date, self.activeDate) === 0) { + $scope.activeDateId = dateObject.uid; + return true; + } + return false; + }; this.init = function( ngModelCtrl_ ) { ngModelCtrl = ngModelCtrl_; @@ -56,7 +68,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) isValid = !isNaN(date); if ( isValid ) { - this.currentCalendarDate = date; + this.activeDate = date; } else { $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); } @@ -66,11 +78,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) }; this.refreshView = function() { - if ( this.mode ) { + if ( this.element ) { this._refreshView(); var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; - ngModelCtrl.$setValidity('date-disabled', !date || (this.mode && !this.isDisabled(date))); + ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); } }; @@ -86,7 +98,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) }; this.isDisabled = function( date ) { - return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($scope.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); + return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); }; // Split array into smaller arrays @@ -105,23 +117,87 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) ngModelCtrl.$setViewValue( dt ); ngModelCtrl.$render(); } else { - self.currentCalendarDate = date; - $scope.datepickerMode = self.mode.previous; + self.activeDate = date; + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; } }; $scope.move = function( direction ) { - var year = self.currentCalendarDate.getFullYear() + direction * (self.mode.step.years || 0), - month = self.currentCalendarDate.getMonth() + direction * (self.mode.step.months || 0); - self.currentCalendarDate.setFullYear(year, month, 1); + var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), + month = self.activeDate.getMonth() + direction * (self.step.months || 0); + self.activeDate.setFullYear(year, month, 1); self.refreshView(); }; - $scope.toggleMode = function() { - $scope.datepickerMode = $scope.datepickerMode === self.maxMode ? self.minMode : self.mode.next; + $scope.toggleMode = function( direction ) { + direction = direction || 1; + + if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { + return; + } + + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; + }; + + // Key event mapper + $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; + + var focusElement = function() { + $timeout(function() { + self.element[0].focus(); + }, 0 , false); + }; + + // Listen for focus requests from popup directive + $scope.$on('datepicker.focus', focusElement); + + $scope.keydown = function( evt ) { + var key = $scope.keys[evt.which]; + + if ( !key || evt.shiftKey || evt.altKey ) { + return; + } + + evt.preventDefault(); + evt.stopPropagation(); + + if (key === 'enter' || key === 'space') { + if ( self.isDisabled(self.activeDate)) { + return; // do nothing + } + $scope.select(self.activeDate); + focusElement(); + } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { + $scope.toggleMode(key === 'up' ? 1 : -1); + focusElement(); + } else { + self.handleKeyDown(key, evt); + self.refreshView(); + } }; }]) +.directive( 'datepicker', function () { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/datepicker.html', + scope: { + datepickerMode: '=?', + dateDisabled: '&' + }, + require: ['datepicker', '?^ngModel'], + controller: 'DatepickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + datepickerCtrl.init( ngModelCtrl ); + } + } + }; +}) + .directive('daypicker', ['dateFilter', function (dateFilter) { return { restrict: 'EA', @@ -131,13 +207,12 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) link: function(scope, element, attrs, ctrl) { scope.showWeeks = ctrl.showWeeks; - ctrl.mode = { - step: { months: 1 }, - next: 'month' - }; + ctrl.step = { months: 1 }; + ctrl.element = element; + var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; function getDaysInMonth( year, month ) { - return new Date(year, month, 0).getDate(); + return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; } function getDates(startDate, n) { @@ -151,8 +226,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) } ctrl._refreshView = function() { - var year = ctrl.currentCalendarDate.getFullYear(), - month = ctrl.currentCalendarDate.getMonth(), + var year = ctrl.activeDate.getFullYear(), + month = ctrl.activeDate.getMonth(), firstDayOfMonth = new Date(year, month, 1), difference = ctrl.startingDay - firstDayOfMonth.getDay(), numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, @@ -162,22 +237,26 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); numDates += numDisplayedFromPreviousMonth; // Previous } - numDates += getDaysInMonth(year, month + 1); // Current + numDates += getDaysInMonth(year, month); // Current numDates += (7 - numDates % 7) % 7; // Next var days = getDates(firstDate, numDates); for (var i = 0; i < numDates; i ++) { days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { - secondary: days[i].getMonth() !== month + secondary: days[i].getMonth() !== month, + uid: scope.uniqueId + '-' + i }); } scope.labels = new Array(7); for (var j = 0; j < 7; j++) { - scope.labels[j] = dateFilter(days[j].date, ctrl.formatDayHeader); + scope.labels[j] = { + abbr: dateFilter(days[j].date, ctrl.formatDayHeader), + full: dateFilter(days[j].date, 'EEEE') + }; } - scope.title = dateFilter(ctrl.currentCalendarDate, ctrl.formatDayTitle); + scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); scope.rows = ctrl.split(days, 7); if ( scope.showWeeks ) { @@ -201,6 +280,29 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; } + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getDate(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 7; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 7; + } else if (key === 'pageup' || key === 'pagedown') { + var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); + ctrl.activeDate.setMonth(month, 1); + date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); + } else if (key === 'home') { + date = 1; + } else if (key === 'end') { + date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); + } + ctrl.activeDate.setDate(date); + }; + ctrl.refreshView(); } }; @@ -213,21 +315,20 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) templateUrl: 'template/datepicker/month.html', require: '^datepicker', link: function(scope, element, attrs, ctrl) { - ctrl.mode = { - step: { years: 1 }, - previous: 'day', - next: 'year' - }; + ctrl.step = { years: 1 }; + ctrl.element = element; ctrl._refreshView = function() { var months = new Array(12), - year = ctrl.currentCalendarDate.getFullYear(); + year = ctrl.activeDate.getFullYear(); for ( var i = 0; i < 12; i++ ) { - months[i] = ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth); + months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { + uid: scope.uniqueId + '-' + i + }); } - scope.title = dateFilter(ctrl.currentCalendarDate, ctrl.formatMonthTitle); + scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); scope.rows = ctrl.split(months, 3); }; @@ -235,6 +336,28 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); }; + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getMonth(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 3; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 3; + } else if (key === 'pageup' || key === 'pagedown') { + var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); + ctrl.activeDate.setFullYear(year); + } else if (key === 'home') { + date = 0; + } else if (key === 'end') { + date = 11; + } + ctrl.activeDate.setMonth(date); + }; + ctrl.refreshView(); } }; @@ -247,18 +370,22 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) templateUrl: 'template/datepicker/year.html', require: '^datepicker', link: function(scope, element, attrs, ctrl) { - ctrl.mode = { - step: { years: ctrl.yearRange }, - previous: 'month' - }; + var range = ctrl.yearRange; + + ctrl.step = { years: range }; + ctrl.element = element; + + function getStartingYear( year ) { + return parseInt((year - 1) / range, 10) * range + 1; + } ctrl._refreshView = function() { - var range = this.mode.step.years, - years = new Array(range), - start = parseInt((ctrl.currentCalendarDate.getFullYear() - 1) / range, 10) * range + 1; + var years = new Array(range); - for ( var i = 0; i < range; i++ ) { - years[i] = ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear); + for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { + years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { + uid: scope.uniqueId + '-' + i + }); } scope.title = [years[0].label, years[range - 1].label].join(' - '); @@ -269,32 +396,32 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) return date1.getFullYear() - date2.getFullYear(); }; + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getFullYear(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 5; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 5; + } else if (key === 'pageup' || key === 'pagedown') { + date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; + } else if (key === 'home') { + date = getStartingYear( ctrl.activeDate.getFullYear() ); + } else if (key === 'end') { + date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; + } + ctrl.activeDate.setFullYear(date); + }; + ctrl.refreshView(); } }; }]) -.directive( 'datepicker', function () { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/datepicker.html', - scope: { - datepickerMode: '=?', - dateDisabled: '&' - }, - require: ['datepicker', '?^ngModel'], - controller: 'DatepickerController', - link: function(scope, element, attrs, ctrls) { - var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - if ( ngModelCtrl ) { - datepickerCtrl.init( ngModelCtrl ); - } - } - }; -}) - .constant('datepickerPopupConfig', { datepickerPopup: 'yyyy-MM-dd', currentText: 'Today', @@ -314,7 +441,8 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon isOpen: '=?', currentText: '@', clearText: '@', - closeText: '@' + closeText: '@', + dateDisabled: '&' }, link: function(scope, element, attrs, ngModel) { var dateFormat, @@ -360,7 +488,7 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon } }); if (attrs.dateDisabled) { - datepickerEl.attr('date-disabled', attrs.dateDisabled); + datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); } // TODO: reverse from dateFilter string to Date object @@ -397,6 +525,7 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon if ( closeOnDateSelection ) { scope.isOpen = false; + element[0].focus(); } }; @@ -421,23 +550,30 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon } }; - var openCalendar = function() { - scope.$apply(function() { + var keydown = function(evt, noApply) { + scope.keydown(evt); + }; + element.bind('keydown', keydown); + + scope.keydown = function(evt) { + if (evt.which === 27) { + evt.preventDefault(); + evt.stopPropagation(); + scope.close(); + } else if (evt.which === 40 && !scope.isOpen) { scope.isOpen = true; - }); + } }; scope.$watch('isOpen', function(value) { if (value) { + scope.$broadcast('datepicker.focus'); scope.position = appendToBody ? $position.offset(element) : $position.position(element); scope.position.top = scope.position.top + element.prop('offsetHeight'); $document.bind('click', documentClickBind); - element.unbind('focus', openCalendar); - element[0].focus(); } else { $document.unbind('click', documentClickBind); - element.bind('focus', openCalendar); } }); @@ -454,6 +590,11 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon scope.dateSelection( date ); }; + scope.close = function() { + scope.isOpen = false; + element[0].focus(); + }; + var $popup = $compile(popupEl)(scope); if ( appendToBody ) { $document.find('body').append($popup); @@ -463,7 +604,7 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon scope.$on('$destroy', function() { $popup.remove(); - element.unbind('focus', openCalendar); + element.unbind('keydown', keydown); $document.unbind('click', documentClickBind); }); } diff --git a/src/datepicker/docs/readme.md b/src/datepicker/docs/readme.md index 0e6886afca..537b08c4f6 100644 --- a/src/datepicker/docs/readme.md +++ b/src/datepicker/docs/readme.md @@ -7,7 +7,7 @@ Everything is formatted using the [date filter](http://docs.angularjs.org/api/ng ### Datepicker Settings ### -All settings can be provided as attributes in the `` or globally configured through the `datepickerConfig`. +All settings can be provided as attributes in the `datepicker` or globally configured through the `datepickerConfig`. * `ng-model` : @@ -65,7 +65,7 @@ All settings can be provided as attributes in the `` or globally con _(Default: 'EEE')_ : Format of day in week header. - * `format-day-title-` + * `format-day-title` _(Default: 'MMMM yyyy')_ : Format of title when selecting day. @@ -110,3 +110,20 @@ Specific settings for the `datepicker-popup`, that can globally configured throu * `datepicker-append-to-body` _(Default: false)_: Append the datepicker popup element to `body`, rather than inserting after `datepicker-popup`. For global configuration, use `datepickerPopupConfig.appendToBody`. + +### Keyboard Support ### + +Depending on datepicker's current mode, the date may reffer either to day, month or year. Accordingly, the term view reffers either to a month, year or year range. + + * `Left`: Move focus to the previous date. Will move to the last date of the previous view, if the current date is the first date of a view. + * `Right`: Move focus to the next date. Will move to the first date of the following view, if the current date is the last date of a view. + * `Up`: Move focus to the same column of the previous row. Will wrap to the appropriate row in the previous view. + * `Down`: Move focus to the same column of the following row. Will wrap to the appropriate row in the following view. + * `PgUp`: Move focus to the same date of the previous view. If that date does not exist, focus is placed on the last date of the month. + * `PgDn`: Move focus to the same date of the following view. If that date does not exist, focus is placed on the last date of the month. + * `Home`: Move to the first date of the view. + * `End`: Move to the last date of the view. + * `Enter`/`Space`: Select date. + * `Ctrl`+`Up`: Move to an upper mode. + * `Ctrl`+`Down`: Move to a lower mode. + * `Esc`: Will close popup, and move focus to the input. \ No newline at end of file diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index d8083c3ce3..cd4c5b8398 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -82,6 +82,28 @@ describe('datepicker directive', function () { }); } + 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); + } + describe('', function () { beforeEach(function() { element = $compile('')($rootScope); @@ -285,7 +307,7 @@ describe('datepicker directive', function () { }); }); - it('loops between different modes', function() { + it('does not loop between after max mode', function() { expect(getTitle()).toBe('September 2010'); clickTitleButton(); @@ -295,7 +317,7 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('2001 - 2020'); clickTitleButton(); - expect(getTitle()).toBe('September 2010'); + expect(getTitle()).toBe('2001 - 2020'); }); describe('month selection mode', function () { @@ -427,6 +449,245 @@ describe('datepicker directive', function () { }); }); + describe('keyboard navigation', function() { + function getActiveLabel() { + return element.find('.active').eq(0).text(); + } + + describe('day mode', function() { + it('will be able to activate previous day', function() { + triggerKeyDown(element, 'left'); + expect(getActiveLabel()).toBe('29'); + }); + + it('will be able to select with enter', function() { + triggerKeyDown(element, 'left'); + triggerKeyDown(element, 'enter'); + expect($rootScope.date).toEqual(new Date('September 29, 2010 15:30:00')); + }); + + it('will be able to select with space', function() { + triggerKeyDown(element, 'left'); + triggerKeyDown(element, 'space'); + expect($rootScope.date).toEqual(new Date('September 29, 2010 15:30:00')); + }); + + it('will be able to activate next day', function() { + triggerKeyDown(element, 'right'); + expect(getActiveLabel()).toBe('01'); + expect(getTitle()).toBe('October 2010'); + }); + + it('will be able to activate same day in previous week', function() { + triggerKeyDown(element, 'up'); + expect(getActiveLabel()).toBe('23'); + }); + + it('will be able to activate same day in next week', function() { + triggerKeyDown(element, 'down'); + expect(getActiveLabel()).toBe('07'); + expect(getTitle()).toBe('October 2010'); + }); + + it('will be able to activate same date in previous month', function() { + triggerKeyDown(element, 'pageup'); + expect(getActiveLabel()).toBe('30'); + expect(getTitle()).toBe('August 2010'); + }); + + it('will be able to activate same date in next month', function() { + triggerKeyDown(element, 'pagedown'); + expect(getActiveLabel()).toBe('30'); + expect(getTitle()).toBe('October 2010'); + }); + + it('will be able to activate first day of the month', function() { + triggerKeyDown(element, 'home'); + expect(getActiveLabel()).toBe('01'); + expect(getTitle()).toBe('September 2010'); + }); + + it('will be able to activate last day of the month', function() { + $rootScope.date = new Date('September 1, 2010 15:30:00'); + $rootScope.$digest(); + + triggerKeyDown(element, 'end'); + expect(getActiveLabel()).toBe('30'); + expect(getTitle()).toBe('September 2010'); + }); + + it('will be able to move to month mode', function() { + triggerKeyDown(element, 'up', true); + expect(getActiveLabel()).toBe('September'); + expect(getTitle()).toBe('2010'); + }); + + it('will not respond when trying to move to lower mode', function() { + triggerKeyDown(element, 'down', true); + expect(getActiveLabel()).toBe('30'); + expect(getTitle()).toBe('September 2010'); + }); + }); + + describe('month mode', function() { + beforeEach(function() { + triggerKeyDown(element, 'up', true); + }); + + it('will be able to activate previous month', function() { + triggerKeyDown(element, 'left'); + expect(getActiveLabel()).toBe('August'); + }); + + it('will be able to activate next month', function() { + triggerKeyDown(element, 'right'); + expect(getActiveLabel()).toBe('October'); + }); + + it('will be able to activate same month in previous row', function() { + triggerKeyDown(element, 'up'); + expect(getActiveLabel()).toBe('June'); + }); + + it('will be able to activate same month in next row', function() { + triggerKeyDown(element, 'down'); + expect(getActiveLabel()).toBe('December'); + }); + + it('will be able to activate same date in previous year', function() { + triggerKeyDown(element, 'pageup'); + expect(getActiveLabel()).toBe('September'); + expect(getTitle()).toBe('2009'); + }); + + it('will be able to activate same date in next year', function() { + triggerKeyDown(element, 'pagedown'); + expect(getActiveLabel()).toBe('September'); + expect(getTitle()).toBe('2011'); + }); + + it('will be able to activate first month of the year', function() { + triggerKeyDown(element, 'home'); + expect(getActiveLabel()).toBe('January'); + expect(getTitle()).toBe('2010'); + }); + + it('will be able to activate last month of the year', function() { + triggerKeyDown(element, 'end'); + expect(getActiveLabel()).toBe('December'); + expect(getTitle()).toBe('2010'); + }); + + it('will be able to move to year mode', function() { + triggerKeyDown(element, 'up', true); + expect(getActiveLabel()).toBe('2010'); + expect(getTitle()).toBe('2001 - 2020'); + }); + + it('will be able to move to day mode', function() { + triggerKeyDown(element, 'down', true); + expect(getActiveLabel()).toBe('30'); + expect(getTitle()).toBe('September 2010'); + }); + + it('will move to day mode when selecting', function() { + triggerKeyDown(element, 'left', true); + triggerKeyDown(element, 'enter', true); + expect(getActiveLabel()).toBe('30'); + expect(getTitle()).toBe('August 2010'); + expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); + }); + }); + + describe('year mode', function() { + beforeEach(function() { + triggerKeyDown(element, 'up', true); + triggerKeyDown(element, 'up', true); + }); + + it('will be able to activate previous year', function() { + triggerKeyDown(element, 'left'); + expect(getActiveLabel()).toBe('2009'); + }); + + it('will be able to activate next year', function() { + triggerKeyDown(element, 'right'); + expect(getActiveLabel()).toBe('2011'); + }); + + it('will be able to activate same year in previous row', function() { + triggerKeyDown(element, 'up'); + expect(getActiveLabel()).toBe('2005'); + }); + + it('will be able to activate same year in next row', function() { + triggerKeyDown(element, 'down'); + expect(getActiveLabel()).toBe('2015'); + }); + + it('will be able to activate same date in previous view', function() { + triggerKeyDown(element, 'pageup'); + expect(getActiveLabel()).toBe('1990'); + }); + + it('will be able to activate same date in next view', function() { + triggerKeyDown(element, 'pagedown'); + expect(getActiveLabel()).toBe('2030'); + }); + + it('will be able to activate first year of the year', function() { + triggerKeyDown(element, 'home'); + expect(getActiveLabel()).toBe('2001'); + }); + + it('will be able to activate last year of the year', function() { + triggerKeyDown(element, 'end'); + expect(getActiveLabel()).toBe('2020'); + }); + + it('will not respond when trying to move to upper mode', function() { + triggerKeyDown(element, 'up', true); + expect(getTitle()).toBe('2001 - 2020'); + }); + + it('will be able to move to month mode', function() { + triggerKeyDown(element, 'down', true); + expect(getActiveLabel()).toBe('September'); + expect(getTitle()).toBe('2010'); + }); + + it('will move to month mode when selecting', function() { + triggerKeyDown(element, 'left', true); + triggerKeyDown(element, 'enter', true); + expect(getActiveLabel()).toBe('September'); + expect(getTitle()).toBe('2009'); + expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); + }); + }); + + describe('`aria-activedescendant`', function() { + function checkActivedescendant() { + var activeId = element.find('table').attr('aria-activedescendant'); + expect(element.find('#' + activeId + ' > button')).toHaveClass('active'); + } + + it('updates correctly', function() { + triggerKeyDown(element, 'left'); + checkActivedescendant(); + + triggerKeyDown(element, 'down'); + checkActivedescendant(); + + triggerKeyDown(element, 'up', true); + checkActivedescendant(); + + triggerKeyDown(element, 'up', true); + checkActivedescendant(); + }); + }); + + }); + }); describe('attribute `starting-day`', function () { @@ -816,7 +1077,7 @@ describe('datepicker directive', function () { }); describe('as popup', function () { - var inputEl, dropdownEl, changeInputValueTo, $document; + var inputEl, dropdownEl, $document, $sniffer; function assignElements(wrapElement) { inputEl = wrapElement.find('input'); @@ -824,31 +1085,43 @@ describe('datepicker directive', function () { element = dropdownEl.find('table'); } - describe('', function () { - beforeEach(inject(function(_$document_, $sniffer) { + 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_; + $rootScope.isopen = true; $rootScope.date = new Date('September 30, 2010 15:30:00'); var wrapElement = $compile('
    ')($rootScope); $rootScope.$digest(); assignElements(wrapElement); - - changeInputValueTo = function (el, value) { - el.val(value); - el.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); - $rootScope.$digest(); - }; })); + it('does not to display datepicker initially', function() { + expect(dropdownEl).toBeHidden(); + }); + it('to display the correct value in input', function() { expect(inputEl.val()).toBe('2010-09-30'); }); + }); - it('does not to display datepicker initially', function() { - expect(dropdownEl).toBeHidden(); - }); + describe('initially opened', 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('displays datepicker on input focus', function() { - inputEl.focus(); + it('datepicker is displayed', function() { expect(dropdownEl).not.toBeHidden(); }); @@ -883,7 +1156,6 @@ describe('datepicker directive', function () { }); it('closes the dropdown when a day is clicked', function() { - inputEl.focus(); expect(dropdownEl.css('display')).not.toBe('none'); clickOption(17); @@ -909,7 +1181,6 @@ describe('datepicker directive', function () { }); it('closes when click outside of calendar', function() { - inputEl.focus(); expect(dropdownEl).not.toBeHidden(); $document.find('body').click(); @@ -935,6 +1206,41 @@ describe('datepicker directive', function () { 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).not.toBeHidden(); + + dropdownEl.find('button').eq(0).focus(); + expect(document.activeElement.tagName).toBe('BUTTON'); + + triggerKeyDown(dropdownEl, 'esc'); + expect(dropdownEl).toBeHidden(); + expect(document.activeElement.tagName).toBe('INPUT'); + }); + + it('returns to the input when ESC key is pressed in the input and closes', function() { + expect(dropdownEl).not.toBeHidden(); + + dropdownEl.find('button').eq(0).focus(); + expect(document.activeElement.tagName).toBe('BUTTON'); + + triggerKeyDown(inputEl, 'esc'); + $rootScope.$digest(); + expect(dropdownEl).toBeHidden(); + expect(document.activeElement.tagName).toBe('INPUT'); + }); + }); }); describe('attribute `datepickerOptions`', function () { @@ -1061,13 +1367,15 @@ describe('datepicker directive', function () { describe('', function () { beforeEach(inject(function() { - var wrapElement = $compile('
    ')($rootScope); + $rootScope.isopen = true; + var wrapElement = $compile('
    ')($rootScope); $rootScope.$digest(); assignElements(wrapElement); assignButtonBar(); })); it('should exist', function() { + expect(dropdownEl).not.toBeHidden(); expect(dropdownEl.find('li').length).toBe(2); }); @@ -1112,11 +1420,8 @@ describe('datepicker directive', function () { }); it('should have a button to close calendar', function() { - inputEl.focus(); - expect(dropdownEl.css('display')).not.toBe('none'); - buttons.eq(2).click(); - expect(dropdownEl.css('display')).toBe('none'); + expect(dropdownEl).toBeHidden(); }); }); @@ -1338,12 +1643,12 @@ describe('datepicker directive', function () { $rootScope.$digest(); })); - it('loops between allowed modes', function() { + it('does not move below it', function() { + expect(getTitle()).toBe('2013'); + clickOption( 5 ); expect(getTitle()).toBe('2013'); clickTitleButton(); expect(getTitle()).toBe('2001 - 2020'); - clickTitleButton(); - expect(getTitle()).toBe('2013'); }); }); @@ -1354,12 +1659,12 @@ describe('datepicker directive', function () { $rootScope.$digest(); })); - it('loops between allowed modes', function() { + it('does not move above it', function() { expect(getTitle()).toBe('August 2013'); clickTitleButton(); expect(getTitle()).toBe('2013'); clickTitleButton(); - expect(getTitle()).toBe('August 2013'); + expect(getTitle()).toBe('2013'); }); }); }); diff --git a/template/datepicker/datepicker.html b/template/datepicker/datepicker.html index 451f2bc28a..1ecb3c50b4 100644 --- a/template/datepicker/datepicker.html +++ b/template/datepicker/datepicker.html @@ -1,5 +1,5 @@ -
    - - - +
    + + +
    \ No newline at end of file diff --git a/template/datepicker/day.html b/template/datepicker/day.html index d4c93fb364..ca212a391a 100644 --- a/template/datepicker/day.html +++ b/template/datepicker/day.html @@ -1,20 +1,20 @@ -
    +
    - - - + + + - + - - + diff --git a/template/datepicker/month.html b/template/datepicker/month.html index eb139f9c07..539219004b 100644 --- a/template/datepicker/month.html +++ b/template/datepicker/month.html @@ -1,15 +1,15 @@ -
    {{label}}{{label.abbr}}
    {{ weekNumbers[$index] }} - + {{ weekNumbers[$index] }} +
    +
    - - - + + + - diff --git a/template/datepicker/popup.html b/template/datepicker/popup.html index f2ea1df7a6..fd48f60663 100644 --- a/template/datepicker/popup.html +++ b/template/datepicker/popup.html @@ -1,10 +1,10 @@ -
    - + +
    +
    - - - + + + - From b0b1434389c889360b174b718b79fc1993c099bc Mon Sep 17 00:00:00 2001 From: Jesus Rodriguez Date: Mon, 10 Mar 2014 00:30:30 +0100 Subject: [PATCH 0345/1761] feat(datepicker): full six-week calendar --- src/datepicker/datepicker.js | 18 +++++++--------- src/datepicker/test/datepicker.spec.js | 30 ++++++++++++++++---------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index 89498b85a5..09c1cff706 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -227,21 +227,19 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) ctrl._refreshView = function() { var year = ctrl.activeDate.getFullYear(), - month = ctrl.activeDate.getMonth(), - firstDayOfMonth = new Date(year, month, 1), - difference = ctrl.startingDay - firstDayOfMonth.getDay(), - numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, - firstDate = new Date(firstDayOfMonth), numDates = 0; + month = ctrl.activeDate.getMonth(), + firstDayOfMonth = new Date(year, month, 1), + difference = ctrl.startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth); if ( numDisplayedFromPreviousMonth > 0 ) { firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); - numDates += numDisplayedFromPreviousMonth; // Previous } - numDates += getDaysInMonth(year, month); // Current - numDates += (7 - numDates % 7) % 7; // Next - var days = getDates(firstDate, numDates); - for (var i = 0; i < numDates; i ++) { + // 42 is the number of days on a six-month calendar + var days = getDates(firstDate, 42); + for (var i = 0; i < 42; i ++) { days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { secondary: days[i].getMonth() !== month, uid: scope.uniqueId + '-' + i diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index cd4c5b8398..a4bc60d29d 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -129,12 +129,13 @@ describe('datepicker directive', function () { ['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'] + ['26', '27', '28', '29', '30', '01', '02'], + ['03', '04', '05', '06', '07', '08', '09'] ]); }); it('renders the week numbers based on ISO 8601', function() { - expect(getWeeks()).toEqual(['34', '35', '36', '37', '38']); + expect(getWeeks()).toEqual(['34', '35', '36', '37', '38', '39']); }); it('value is correct', function() { @@ -183,7 +184,8 @@ describe('datepicker directive', function () { ['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'] + ['29', '30', '31', '01', '02', '03', '04'], + ['05', '06', '07', '08', '09', '10', '11'] ]); expectSelectedElement( null, null ); @@ -257,7 +259,8 @@ describe('datepicker directive', function () { ['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'] + ['27', '28', '29', '30', '01', '02', '03'], + ['04', '05', '06', '07', '08', '09', '10'] ]); expectSelectedElement( 8 ); @@ -386,7 +389,8 @@ describe('datepicker directive', function () { ['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'] + ['27', '28', '29', '30', '01', '02', '03'], + ['04', '05', '06', '07', '08', '09', '10'] ]); clickOption( 17 ); @@ -707,12 +711,13 @@ describe('datepicker directive', function () { ['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'] + ['27', '28', '29', '30', '01', '02', '03'], + ['04', '05', '06', '07', '08', '09', '10'] ]); }); it('renders the week numbers correctly', function() { - expect(getWeeks()).toEqual(['35', '36', '37', '38', '39']); + expect(getWeeks()).toEqual(['35', '36', '37', '38', '39', '40']); }); }); @@ -913,7 +918,7 @@ describe('datepicker directive', function () { }); it('executes the dateDisabled expression for each visible day plus one for validation', function() { - expect($rootScope.dateDisabledHandler.calls.length).toEqual(35 + 1); + expect($rootScope.dateDisabledHandler.calls.length).toEqual(42 + 1); }); it('executes the dateDisabled expression for each visible month plus one for validation', function() { @@ -981,7 +986,8 @@ describe('datepicker directive', function () { ['5', '6', '7', '8', '9', '10', '11'], ['12', '13', '14', '15', '16', '17', '18'], ['19', '20', '21', '22', '23', '24', '25'], - ['26', '27', '28', '29', '30', '1', '2'] + ['26', '27', '28', '29', '30', '1', '2'], + ['3', '4', '5', '6', '7', '8', '9'] ]); }); }); @@ -1042,7 +1048,8 @@ describe('datepicker directive', function () { ['4', '5', '6', '7', '8', '9', '10'], ['11', '12', '13', '14', '15', '16', '17'], ['18', '19', '20', '21', '22', '23', '24'], - ['25', '26', '27', '28', '29', '30', '1'] + ['25', '26', '27', '28', '29', '30', '1'], + ['2', '3', '4', '5', '6', '7', '8'] ]); }); @@ -1133,7 +1140,8 @@ describe('datepicker directive', function () { ['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'] + ['26', '27', '28', '29', '30', '01', '02'], + ['03', '04', '05', '06', '07', '08', '09'] ]); }); From 97b07477777fd513a18b4c43da13e972cfd2bfa5 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Fri, 18 Apr 2014 12:21:48 +0300 Subject: [PATCH 0346/1761] refactor(timepicker): change variable usage --- src/timepicker/timepicker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/timepicker/timepicker.js b/src/timepicker/timepicker.js index 045e19b989..91ac86bcee 100644 --- a/src/timepicker/timepicker.js +++ b/src/timepicker/timepicker.js @@ -144,7 +144,7 @@ angular.module('ui.bootstrap.timepicker', []) }; hoursInputEl.bind('blur', function(e) { - if ( !$scope.validHours && $scope.hours < 10) { + if ( !$scope.invalidHours && $scope.hours < 10) { $scope.$apply( function() { $scope.hours = pad( $scope.hours ); }); From 73c2dea54872714184168f64623aa0dd5f0aeb58 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Wed, 16 Apr 2014 22:34:25 +0300 Subject: [PATCH 0347/1761] refactor(dropdown): do not stop event propagation Fixes #1986 Closes #2076 --- src/dropdown/dropdown.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/dropdown/dropdown.js b/src/dropdown/dropdown.js index 62c4582d6f..7bb37bd18c 100644 --- a/src/dropdown/dropdown.js +++ b/src/dropdown/dropdown.js @@ -28,7 +28,11 @@ angular.module('ui.bootstrap.dropdown', []) } }; - var closeDropdown = function() { + var closeDropdown = function( evt ) { + if (evt && evt.isDefaultPrevented()) { + return; + } + openScope.$apply(function() { openScope.isOpen = false; }); @@ -126,7 +130,6 @@ angular.module('ui.bootstrap.dropdown', []) var toggleDropdown = function(event) { event.preventDefault(); - event.stopPropagation(); if ( !element.hasClass('disabled') && !attrs.disabled ) { scope.$apply(function() { From bd2ae0ee02ae384f0ce12f61fadf16f8cb1ea658 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sun, 2 Mar 2014 01:14:06 +0100 Subject: [PATCH 0348/1761] feat(dateParser): add `dateParser` service Closes #1874 --- src/dateparser/dateparser.js | 126 +++++++++++++++++++++++++ src/dateparser/test/dateparser.spec.js | 96 +++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 src/dateparser/dateparser.js create mode 100644 src/dateparser/test/dateparser.spec.js diff --git a/src/dateparser/dateparser.js b/src/dateparser/dateparser.js new file mode 100644 index 0000000000..85fc203352 --- /dev/null +++ b/src/dateparser/dateparser.js @@ -0,0 +1,126 @@ +angular.module('ui.bootstrap.dateparser', []) + +.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { + + this.parsers = {}; + + var formatCodeToRegex = { + 'yyyy': { + regex: '\\d{4}', + apply: function(value) { this.year = +value; } + }, + 'yy': { + regex: '\\d{2}', + apply: function(value) { this.year = +value + 2000; } + }, + 'y': { + regex: '\\d{1,4}', + apply: function(value) { this.year = +value; } + }, + 'MMMM': { + regex: $locale.DATETIME_FORMATS.MONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } + }, + 'MMM': { + regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } + }, + 'MM': { + regex: '0[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; } + }, + 'M': { + regex: '[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; } + }, + 'dd': { + regex: '[0-2][0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; } + }, + 'd': { + regex: '[1-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; } + }, + 'EEEE': { + regex: $locale.DATETIME_FORMATS.DAY.join('|') + }, + 'EEE': { + regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') + } + }; + + this.createParser = function(format) { + var map = [], regex = format.split(''); + + angular.forEach(formatCodeToRegex, function(data, code) { + var index = format.indexOf(code); + + if (index > -1) { + format = format.split(''); + + regex[index] = '(' + data.regex + ')'; + format[index] = '$'; // Custom symbol to define consumed part of format + for (var i = index + 1, n = index + code.length; i < n; i++) { + regex[i] = ''; + format[i] = '$'; + } + format = format.join(''); + + map.push({ index: index, apply: data.apply }); + } + }); + + return { + regex: new RegExp('^' + regex.join('') + '$'), + map: orderByFilter(map, 'index') + }; + }; + + this.parse = function(input, format) { + if ( !angular.isString(input) ) { + return input; + } + + format = $locale.DATETIME_FORMATS[format] || format; + + if ( !this.parsers[format] ) { + this.parsers[format] = this.createParser(format); + } + + var parser = this.parsers[format], + regex = parser.regex, + map = parser.map, + results = input.match(regex); + + if ( results && results.length ) { + var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt; + + for( var i = 1, n = results.length; i < n; i++ ) { + var mapper = map[i-1]; + if ( mapper.apply ) { + mapper.apply.call(fields, results[i]); + } + } + + if ( isValid(fields.year, fields.month, fields.date) ) { + dt = new Date( fields.year, fields.month, fields.date, fields.hours); + } + + return dt; + } + }; + + // Check if date is valid for specific month (and year for February). + // Month: 0 = Jan, 1 = Feb, etc + function isValid(year, month, date) { + if ( month === 1 && date > 28) { + return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); + } + + if ( month === 3 || month === 5 || month === 8 || month === 10) { + return date < 31; + } + + return true; + } +}]); diff --git a/src/dateparser/test/dateparser.spec.js b/src/dateparser/test/dateparser.spec.js new file mode 100644 index 0000000000..4090b41ecd --- /dev/null +++ b/src/dateparser/test/dateparser.spec.js @@ -0,0 +1,96 @@ +describe('date parser', function () { + var dateParser; + + beforeEach(module('ui.bootstrap.dateparser')); + beforeEach(inject(function (_dateParser_) { + dateParser = _dateParser_; + })); + + function expectParse(input, format, date) { + expect(dateParser.parse(input, format)).toEqual(date); + } + + describe('wih custom formats', function() { + it('should work correctly for `dd`, `MM`, `yyyy`', function() { + expectParse('17.11.2013', 'dd.MM.yyyy', new Date(2013, 10, 17, 0)); + expectParse('31.12.2013', 'dd.MM.yyyy', new Date(2013, 11, 31, 0)); + expectParse('08-03-1991', 'dd-MM-yyyy', new Date(1991, 2, 8, 0)); + expectParse('03/05/1980', 'MM/dd/yyyy', new Date(1980, 2, 5, 0)); + expectParse('10.01/1983', 'dd.MM/yyyy', new Date(1983, 0, 10, 0)); + expectParse('11-09-1980', 'MM-dd-yyyy', new Date(1980, 10, 9, 0)); + expectParse('2011/02/05', 'yyyy/MM/dd', new Date(2011, 1, 5, 0)); + }); + + it('should work correctly for `yy`', function() { + expectParse('17.11.13', 'dd.MM.yy', new Date(2013, 10, 17, 0)); + expectParse('02-05-11', 'dd-MM-yy', new Date(2011, 4, 2, 0)); + expectParse('02/05/80', 'MM/dd/yy', new Date(2080, 1, 5, 0)); + expectParse('55/02/05', 'yy/MM/dd', new Date(2055, 1, 5, 0)); + expectParse('11-08-13', 'dd-MM-yy', new Date(2013, 7, 11, 0)); + }); + + it('should work correctly for `M`', function() { + expectParse('8/11/2013', 'M/dd/yyyy', new Date(2013, 7, 11, 0)); + expectParse('07.11.05', 'dd.M.yy', new Date(2005, 10, 7, 0)); + expectParse('02-5-11', 'dd-M-yy', new Date(2011, 4, 2, 0)); + expectParse('2/05/1980', 'M/dd/yyyy', new Date(1980, 1, 5, 0)); + expectParse('1955/2/05', 'yyyy/M/dd', new Date(1955, 1, 5, 0)); + expectParse('02-5-11', 'dd-M-yy', new Date(2011, 4, 2, 0)); + }); + + it('should work correctly for `MMM`', function() { + expectParse('30.Sep.10', 'dd.MMM.yy', new Date(2010, 8, 30, 0)); + expectParse('02-May-11', 'dd-MMM-yy', new Date(2011, 4, 2, 0)); + expectParse('Feb/05/1980', 'MMM/dd/yyyy', new Date(1980, 1, 5, 0)); + expectParse('1955/Feb/05', 'yyyy/MMM/dd', new Date(1955, 1, 5, 0)); + }); + + it('should work correctly for `MMMM`', function() { + expectParse('17.November.13', 'dd.MMMM.yy', new Date(2013, 10, 17, 0)); + expectParse('05-March-1980', 'dd-MMMM-yyyy', new Date(1980, 2, 5, 0)); + expectParse('February/05/1980', 'MMMM/dd/yyyy', new Date(1980, 1, 5, 0)); + expectParse('1949/December/20', 'yyyy/MMMM/dd', new Date(1949, 11, 20, 0)); + }); + + it('should work correctly for `d`', function() { + expectParse('17.November.13', 'd.MMMM.yy', new Date(2013, 10, 17, 0)); + expectParse('8-March-1991', 'd-MMMM-yyyy', new Date(1991, 2, 8, 0)); + expectParse('February/5/1980', 'MMMM/d/yyyy', new Date(1980, 1, 5, 0)); + expectParse('1955/February/5', 'yyyy/MMMM/d', new Date(1955, 1, 5, 0)); + expectParse('11-08-13', 'd-MM-yy', new Date(2013, 7, 11, 0)); + }); + }); + + describe('wih predefined formats', function() { + it('should work correctly for `shortDate`', function() { + expectParse('9/3/10', 'shortDate', new Date(2010, 8, 3, 0)); + }); + + it('should work correctly for `mediumDate`', function() { + expectParse('Sep 3, 2010', 'mediumDate', new Date(2010, 8, 3, 0)); + }); + + it('should work correctly for `longDate`', function() { + expectParse('September 3, 2010', 'longDate', new Date(2010, 8, 3, 0)); + }); + + it('should work correctly for `fullDate`', function() { + expectParse('Friday, September 3, 2010', 'fullDate', new Date(2010, 8, 3, 0)); + }); + }); + + describe('with edge case', function() { + it('should not work for invalid number of days in February', function() { + expect(dateParser.parse('29.02.2013', 'dd.MM.yyyy')).toBeUndefined(); + }); + + it('should work for 29 days in February for leap years', function() { + expectParse('29.02.2000', 'dd.MM.yyyy', new Date(2000, 1, 29, 0)); + }); + + it('should not work for 31 days for some months', function() { + expect(dateParser.parse('31-04-2013', 'dd-MM-yyyy')).toBeUndefined(); + expect(dateParser.parse('November 31, 2013', 'MMMM d, yyyy')).toBeUndefined(); + }); + }); +}); From e0eb1bce3794c52733140330b8d6b0a8e6eae2a8 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sat, 19 Apr 2014 12:36:57 +0300 Subject: [PATCH 0349/1761] fix(datepicker): parse input using dateParser Fixes #1289 Closes #2085 --- src/datepicker/datepicker.js | 9 ++++----- src/datepicker/docs/demo.js | 2 +- src/datepicker/test/datepicker.spec.js | 13 +++++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index 09c1cff706..691f6e05bd 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -1,4 +1,4 @@ -angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) .constant('datepickerConfig', { formatDay: 'dd', @@ -430,8 +430,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) showButtonBar: true }) -.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'datepickerPopupConfig', -function ($compile, $parse, $document, $position, dateFilter, datepickerPopupConfig) { +.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', +function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { return { restrict: 'EA', require: 'ngModel', @@ -489,7 +489,6 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); } - // TODO: reverse from dateFilter string to Date object function parseDate(viewValue) { if (!viewValue) { ngModel.$setValidity('date', true); @@ -498,7 +497,7 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon ngModel.$setValidity('date', true); return viewValue; } else if (angular.isString(viewValue)) { - var date = new Date(viewValue); + var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); if (isNaN(date)) { ngModel.$setValidity('date', false); return undefined; diff --git a/src/datepicker/docs/demo.js b/src/datepicker/docs/demo.js index c9e7a64442..deff596a56 100644 --- a/src/datepicker/docs/demo.js +++ b/src/datepicker/docs/demo.js @@ -31,6 +31,6 @@ var DatepickerDemoCtrl = function ($scope) { }; $scope.initDate = new Date('2016-15-20'); - $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'shortDate']; + $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate']; $scope.format = $scope.formats[0]; }; diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index a4bc60d29d..d0eedd28d6 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -1351,6 +1351,19 @@ describe('datepicker directive', function () { }); }); + 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 () { beforeEach(inject(function() { $rootScope.close = false; From ded352c5c1287be39d38f07e3443bf2e7d479359 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sat, 19 Apr 2014 18:10:53 +0200 Subject: [PATCH 0350/1761] demo(all): use protocol-less URLs Fixes #2070 Closes #2068 --- misc/demo/assets/plunker.js | 4 ++-- misc/demo/index.html | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/misc/demo/assets/plunker.js b/misc/demo/assets/plunker.js index f740f6beb4..a6ffe18faa 100644 --- a/misc/demo/assets/plunker.js +++ b/misc/demo/assets/plunker.js @@ -15,8 +15,8 @@ angular.module('plunker', []) return '\n' + '\n' + ' \n' + - ' \n' + - ' \n' + + ' \n' + + ' \n' + ' \n' + ' \n' + ' \n' + diff --git a/misc/demo/index.html b/misc/demo/index.html index c471c6b6bd..6e95c3d04f 100644 --- a/misc/demo/index.html +++ b/misc/demo/index.html @@ -8,14 +8,14 @@ - - - + + + - + @@ -80,11 +80,11 @@

    • -
    • -
    • From 00829b60580a5b800772b83cfcc78d53d2551796 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sun, 13 Apr 2014 23:42:05 +0300 Subject: [PATCH 0351/1761] refactor(modal): add class to element instead of template expression --- src/modal/modal.js | 2 +- src/modal/test/modalWindow.spec.js | 3 ++- template/modal/window.html | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modal/modal.js b/src/modal/modal.js index c86048ded3..f60eadb243 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -85,7 +85,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) transclude: true, templateUrl: 'template/modal/window.html', link: function (scope, element, attrs) { - scope.windowClass = attrs.windowClass || ''; + element.addClass(attrs.windowClass || ''); $timeout(function () { // trigger CSS transitions diff --git a/src/modal/test/modalWindow.spec.js b/src/modal/test/modalWindow.spec.js index c117ee9207..6c9ce8d29b 100644 --- a/src/modal/test/modalWindow.spec.js +++ b/src/modal/test/modalWindow.spec.js @@ -10,9 +10,10 @@ describe('modal window', function () { })); it('should support custom CSS classes as string', function () { - var windowEl = $compile('
      content
      ')($rootScope); + var windowEl = $compile('
      content
      ')($rootScope); $rootScope.$digest(); expect(windowEl).toHaveClass('test'); + expect(windowEl).toHaveClass('foo'); }); }); \ No newline at end of file diff --git a/template/modal/window.html b/template/modal/window.html index 5c3b1aa27b..419c4b764c 100644 --- a/template/modal/window.html +++ b/template/modal/window.html @@ -1,3 +1,3 @@ -
    • ')($rootScope); + $rootScope.$digest(); + }); + + it('should not have been called initially', function() { + expect($rootScope.toggleHandler).not.toHaveBeenCalled(); + }); + + it('should call it when clicked', function() { + clickDropdownToggle(); + expect($rootScope.toggleHandler).toHaveBeenCalledWith(true); + + clickDropdownToggle(); + expect($rootScope.toggleHandler).toHaveBeenCalledWith(false); + }); + }); }); From 60cee9dcb5735118f90f55042c25f5f27c45884e Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Thu, 1 May 2014 17:17:09 +0200 Subject: [PATCH 0358/1761] feat(modal): improve accessibility - add role='dialog' --- template/modal/window.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/modal/window.html b/template/modal/window.html index 250d1c20c5..1e9a898646 100644 --- a/template/modal/window.html +++ b/template/modal/window.html @@ -1,3 +1,3 @@ - * @@ -1750,11 +1743,14 @@ angular.module('ngMock', ['ng']).provider({ $rootElement: angular.mock.$RootElementProvider }).config(['$provide', function($provide) { $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); + $provide.decorator('$$rAF', angular.mock.$RAFDecorator); + $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator); }]); /** - * @ngdoc overview + * @ngdoc module * @name ngMockE2E + * @module ngMockE2E * @description * * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. @@ -1766,8 +1762,9 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { }]); /** - * @ngdoc object - * @name ngMockE2E.$httpBackend + * @ngdoc service + * @name $httpBackend + * @module ngMockE2E * @description * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of * applications that use the {@link ng.$http $http service}. @@ -1793,7 +1790,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * To setup the application to run with this http backend, you have to create a module that depends * on the `ngMockE2E` and your application modules and defines the fake backend: * - *
      + * ```js
        *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
        *   myAppDev.run(function($httpBackend) {
        *     phones = [{name: 'phone1'}, {name: 'phone2'}];
      @@ -1808,15 +1805,15 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
        *     $httpBackend.whenGET(/^\/templates\//).passThrough();
        *     //...
        *   });
      - * 
      + * ``` * * Afterwards, bootstrap your app with this new module. */ /** * @ngdoc method - * @name ngMockE2E.$httpBackend#when - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#when + * @module ngMockE2E * @description * Creates a new backend definition. * @@ -1829,19 +1826,20 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * control how a matched request is handled. * * - respond – - * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` + * `{function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers)}` * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). - * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` - * handler, will be pass through to the real backend (an XHR request will be made to the - * server. + * an array containing response status (number), response data (string), response headers + * (Object), and the text for the status (string). + * - passThrough – `{function()}` – Any request matching a backend definition with + * `passThrough` handler will be passed through to the real backend (an XHR request will be made + * to the server.) */ /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenGET - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenGET + * @module ngMockE2E * @description * Creates a new backend definition for GET requests. For more info see `when()`. * @@ -1853,8 +1851,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenHEAD - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenHEAD + * @module ngMockE2E * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * @@ -1866,8 +1864,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenDELETE - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenDELETE + * @module ngMockE2E * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * @@ -1879,8 +1877,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenPOST - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenPOST + * @module ngMockE2E * @description * Creates a new backend definition for POST requests. For more info see `when()`. * @@ -1893,8 +1891,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenPUT - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenPUT + * @module ngMockE2E * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * @@ -1907,8 +1905,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenPATCH - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenPATCH + * @module ngMockE2E * @description * Creates a new backend definition for PATCH requests. For more info see `when()`. * @@ -1921,8 +1919,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenJSONP - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenJSONP + * @module ngMockE2E * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * @@ -1954,7 +1952,7 @@ if(window.jasmine || window.mocha) { var currentSpec = null, isSpecRunning = function() { - return currentSpec && (window.mocha || currentSpec.queue.running); + return !!currentSpec; }; @@ -2039,7 +2037,7 @@ if(window.jasmine || window.mocha) { * *NOTE*: This function is also published on window for easy access.
      * * The inject function wraps a function into an injectable function. The inject() creates new - * instance of {@link AUTO.$injector $injector} per test, which is then used for + * instance of {@link auto.$injector $injector} per test, which is then used for * resolving references. * * @@ -2077,7 +2075,7 @@ if(window.jasmine || window.mocha) { * * ## Example * Example of what a typical jasmine tests looks like with the inject method. - *
      +   * ```js
          *
          *   angular.module('myApplicationModule', [])
          *       .value('mode', 'app')
      @@ -2111,7 +2109,7 @@ if(window.jasmine || window.mocha) {
          *     });
          *   });
          *
      -   * 
      + * ``` * * @param {...Function} fns any number of functions which will be injected using the injector. */ @@ -2132,7 +2130,7 @@ if(window.jasmine || window.mocha) { window.inject = angular.mock.inject = function() { var blockFns = Array.prototype.slice.call(arguments, 0); var errorForStack = new Error('Declaration Location'); - return isSpecRunning() ? workFn() : workFn; + return isSpecRunning() ? workFn.call(currentSpec) : workFn; ///////////////////// function workFn() { var modules = currentSpec.$modules || []; diff --git a/misc/test-lib/angular.js b/misc/test-lib/angular.js index b8621a9a2d..2f26beed69 100644 --- a/misc/test-lib/angular.js +++ b/misc/test-lib/angular.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.10 + * @license AngularJS v1.2.16 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -30,7 +30,7 @@ * should all be static strings, not variables or general expressions. * * @param {string} module The namespace to use for the new minErr instance. - * @returns {function(string, string, ...): Error} instance + * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance */ function minErr(module) { @@ -68,7 +68,7 @@ function minErr(module) { return match; }); - message = message + '\nhttp://errors.angularjs.org/1.2.10/' + + message = message + '\nhttp://errors.angularjs.org/1.2.16/' + (module ? module + '/' : '') + code; for (i = 2; i < arguments.length; i++) { message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + @@ -124,6 +124,7 @@ function minErr(module) { -isWindow, -isScope, -isFile, + -isBlob, -isBoolean, -trim, -isElement, @@ -160,14 +161,31 @@ function minErr(module) { -assertNotHasOwnProperty, -getter, -getBlockElements, + -hasOwnProperty, */ //////////////////////////////////// +/** + * @ngdoc module + * @name ng + * @module ng + * @description + * + * # ng (core module) + * The ng module is loaded by default when an AngularJS application is started. The module itself + * contains the essential components for an AngularJS application to function. The table below + * lists a high level breakdown of each of the services/factories, filters, directives and testing + * components available within this core module. + * + *
      + */ + /** * @ngdoc function * @name angular.lowercase + * @module ng * @function * * @description Converts the specified string to lowercase. @@ -175,11 +193,12 @@ function minErr(module) { * @returns {string} Lowercased string. */ var lowercase = function(string){return isString(string) ? string.toLowerCase() : string;}; - +var hasOwnProperty = Object.prototype.hasOwnProperty; /** * @ngdoc function * @name angular.uppercase + * @module ng * @function * * @description Converts the specified string to uppercase. @@ -263,6 +282,7 @@ function isArrayLike(obj) { /** * @ngdoc function * @name angular.forEach + * @module ng * @function * * @description @@ -271,17 +291,17 @@ function isArrayLike(obj) { * is the value of an object property or an array element and `key` is the object property key or * array element index. Specifying a `context` for the function is optional. * - * It is worth nothing that `.forEach` does not iterate over inherited properties because it filters + * It is worth noting that `.forEach` does not iterate over inherited properties because it filters * using the `hasOwnProperty` method. * -
      +   ```js
            var values = {name: 'misko', gender: 'male'};
            var log = [];
            angular.forEach(values, function(value, key){
              this.push(key + ': ' + value);
            }, log);
      -     expect(log).toEqual(['name: misko', 'gender:male']);
      -   
      + expect(log).toEqual(['name: misko', 'gender: male']); + ``` * * @param {Object|Array} obj Object to iterate over. * @param {Function} iterator Iterator function. @@ -349,7 +369,7 @@ function reverseParams(iteratorFn) { * the number string gets longer over time, and it can also overflow, where as the nextId * will grow much slower, it is a string, and it will never overflow. * - * @returns an unique alpha-numeric string + * @returns {string} an unique alpha-numeric string */ function nextUid() { var index = uid.length; @@ -391,6 +411,7 @@ function setHashKey(obj, h) { /** * @ngdoc function * @name angular.extend + * @module ng * @function * * @description @@ -427,17 +448,18 @@ function inherit(parent, extra) { /** * @ngdoc function * @name angular.noop + * @module ng * @function * * @description * A function that performs no operations. This function can be useful when writing code in the * functional style. -
      +   ```js
            function foo(callback) {
              var result = calculateResult();
              (callback || angular.noop)(result);
            }
      -   
      + ``` */ function noop() {} noop.$inject = []; @@ -446,17 +468,18 @@ noop.$inject = []; /** * @ngdoc function * @name angular.identity + * @module ng * @function * * @description * A function that returns its first argument. This function is useful when writing code in the * functional style. * -
      +   ```js
            function transformer(transformationFn, value) {
              return (transformationFn || angular.identity)(value);
            };
      -   
      + ``` */ function identity($) {return $;} identity.$inject = []; @@ -467,6 +490,7 @@ function valueFn(value) {return function() {return value;};} /** * @ngdoc function * @name angular.isUndefined + * @module ng * @function * * @description @@ -481,6 +505,7 @@ function isUndefined(value){return typeof value === 'undefined';} /** * @ngdoc function * @name angular.isDefined + * @module ng * @function * * @description @@ -495,11 +520,12 @@ function isDefined(value){return typeof value !== 'undefined';} /** * @ngdoc function * @name angular.isObject + * @module ng * @function * * @description * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not - * considered to be objects. + * considered to be objects. Note that JavaScript arrays are objects. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. @@ -510,6 +536,7 @@ function isObject(value){return value != null && typeof value === 'object';} /** * @ngdoc function * @name angular.isString + * @module ng * @function * * @description @@ -524,6 +551,7 @@ function isString(value){return typeof value === 'string';} /** * @ngdoc function * @name angular.isNumber + * @module ng * @function * * @description @@ -538,6 +566,7 @@ function isNumber(value){return typeof value === 'number';} /** * @ngdoc function * @name angular.isDate + * @module ng * @function * * @description @@ -554,6 +583,7 @@ function isDate(value){ /** * @ngdoc function * @name angular.isArray + * @module ng * @function * * @description @@ -570,6 +600,7 @@ function isArray(value) { /** * @ngdoc function * @name angular.isFunction + * @module ng * @function * * @description @@ -615,6 +646,11 @@ function isFile(obj) { } +function isBlob(obj) { + return toString.call(obj) === '[object Blob]'; +} + + function isBoolean(value) { return typeof value === 'boolean'; } @@ -638,6 +674,7 @@ var trim = (function() { /** * @ngdoc function * @name angular.isElement + * @module ng * @function * * @description @@ -649,7 +686,7 @@ var trim = (function() { function isElement(node) { return !!(node && (node.nodeName // we are a direct element - || (node.on && node.find))); // we have an on and find method part of jQuery API + || (node.prop && node.attr && node.find))); // we have an on and find method part of jQuery API } /** @@ -748,6 +785,7 @@ function isLeafNode (node) { /** * @ngdoc function * @name angular.copy + * @module ng * @function * * @description @@ -766,8 +804,8 @@ function isLeafNode (node) { * @returns {*} The copy or updated `destination`, if `destination` was specified. * * @example - - + +
      Name:
      @@ -798,8 +836,8 @@ function isLeafNode (node) { $scope.reset(); } - - + + */ function copy(source, destination){ if (isWindow(source) || isScope(source)) { @@ -851,7 +889,7 @@ function shallowCopy(src, dst) { for(var key in src) { // shallowCopy is only ever called by $compile nodeLinkFn, which has control over src // so we don't need to worry about using our custom hasOwnProperty here - if (src.hasOwnProperty(key) && key.charAt(0) !== '$' && key.charAt(1) !== '$') { + if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { dst[key] = src[key]; } } @@ -863,6 +901,7 @@ function shallowCopy(src, dst) { /** * @ngdoc function * @name angular.equals + * @module ng * @function * * @description @@ -949,6 +988,7 @@ function sliceArgs(args, startIndex) { /** * @ngdoc function * @name angular.bind + * @module ng * @function * * @description @@ -1004,6 +1044,7 @@ function toJsonReplacer(key, value) { /** * @ngdoc function * @name angular.toJson + * @module ng * @function * * @description @@ -1023,13 +1064,14 @@ function toJson(obj, pretty) { /** * @ngdoc function * @name angular.fromJson + * @module ng * @function * * @description * Deserializes a JSON string. * * @param {string} json JSON string to deserialize. - * @returns {Object|Array|Date|string|number} Deserialized thingy. + * @returns {Object|Array|string|number} Deserialized thingy. */ function fromJson(json) { return isString(json) @@ -1096,7 +1138,7 @@ function tryDecodeURIComponent(value) { /** * Parses an escaped url query string into key-value pairs. - * @returns Object.<(string|boolean)> + * @returns {Object.} */ function parseKeyValue(/**string*/keyValue) { var obj = {}, key_value, key; @@ -1178,7 +1220,8 @@ function encodeUriQuery(val, pctEncodeSpaces) { /** * @ngdoc directive - * @name ng.directive:ngApp + * @name ngApp + * @module ng * * @element ANY * @param {angular.Module} ngApp an optional application @@ -1196,7 +1239,7 @@ function encodeUriQuery(val, pctEncodeSpaces) { * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. * * You can specify an **AngularJS module** to be used as the root module for the application. This - * module will be loaded into the {@link AUTO.$injector} when the application is bootstrapped and + * module will be loaded into the {@link auto.$injector} when the application is bootstrapped and * should contain the application code needed or have dependencies on other modules that will * contain the code. See {@link angular.module} for more information. * @@ -1210,6 +1253,7 @@ function encodeUriQuery(val, pctEncodeSpaces) {
      I can add: {{a}} + {{b}} = {{ a+b }} +
      angular.module('ngAppDemo', []).controller('ngAppDemoController', function($scope) { @@ -1267,20 +1311,56 @@ function angularInit(element, bootstrap) { /** * @ngdoc function * @name angular.bootstrap + * @module ng * @description * Use this function to manually start up angular application. * * See: {@link guide/bootstrap Bootstrap} * * Note that ngScenario-based end-to-end tests cannot use this function to bootstrap manually. - * They must use {@link api/ng.directive:ngApp ngApp}. + * They must use {@link ng.directive:ngApp ngApp}. + * + * Angular will detect if it has been loaded into the browser more than once and only allow the + * first loaded script to be bootstrapped and will report a warning to the browser console for + * each of the subsequent scripts. This prevents strange results in applications, where otherwise + * multiple instances of Angular try to work on the DOM. + * + * + * + * + *
      + *

    - + +
    + * + * + * + * + * + * + *
    {{heading}}
    {{fill}}
    + *
    + * + * + * var app = angular.module('multi-bootstrap', []) + * + * .controller('BrokenTable', function($scope) { + * $scope.headings = ['One', 'Two', 'Three']; + * $scope.fillings = [[1, 2, 3], ['A', 'B', 'C'], [7, 8, 9]]; + * }); + * + * + * it('should only insert one table cell for each item in $scope.fillings', function() { + * expect(element.all(by.css('td')).count()) + * .toBe(9); + * }); + * + *
    * - * @param {Element} element DOM element which is the root of angular application. + * @param {DOMElement} element DOM element which is the root of angular application. * @param {Array=} modules an array of modules to load into the application. * Each item in the array should be the name of a predefined module or a (DI annotated) * function that will be invoked by the injector as a run block. * See: {@link angular.module modules} - * @returns {AUTO.$injector} Returns the newly created injector for this app. + * @returns {auto.$injector} Returns the newly created injector for this app. */ function bootstrap(element, modules) { var doBootstrap = function() { @@ -1389,9 +1469,9 @@ function assertNotHasOwnProperty(name, context) { /** * Return the value accessible from the object by path. Any undefined traversals are ignored * @param {Object} obj starting object - * @param {string} path path to traverse - * @param {boolean=true} bindFnToScope - * @returns value as accessible by path + * @param {String} path path to traverse + * @param {boolean} [bindFnToScope=true] + * @returns {Object} value as accessible by path */ //TODO(misko): this function needs to be removed function getter(obj, path, bindFnToScope) { @@ -1416,7 +1496,7 @@ function getter(obj, path, bindFnToScope) { /** * Return the DOM siblings between the first and last node in the given array. * @param {Array} array like object - * @returns jQlite object containing the elements + * @returns {DOMElement} object containing the elements */ function getBlockElements(nodes) { var startNode = nodes[0], @@ -1438,8 +1518,9 @@ function getBlockElements(nodes) { } /** - * @ngdoc interface + * @ngdoc type * @name angular.Module + * @module ng * @description * * Interface for configuring angular {@link angular.module modules}. @@ -1466,6 +1547,7 @@ function setupModuleLoader(window) { /** * @ngdoc function * @name angular.module + * @module ng * @description * * The `angular.module` is a global place for creating, registering and retrieving Angular @@ -1480,9 +1562,9 @@ function setupModuleLoader(window) { * # Module * * A module is a collection of services, directives, filters, and configuration information. - * `angular.module` is used to configure the {@link AUTO.$injector $injector}. + * `angular.module` is used to configure the {@link auto.$injector $injector}. * - *
    +     * ```js
          * // Create a new module
          * var myModule = angular.module('myModule', []);
          *
    @@ -1490,27 +1572,27 @@ function setupModuleLoader(window) {
          * myModule.value('appName', 'MyCoolApp');
          *
          * // configure existing services inside initialization blocks.
    -     * myModule.config(function($locationProvider) {
    +     * myModule.config(['$locationProvider', function($locationProvider) {
          *   // Configure existing providers
          *   $locationProvider.hashPrefix('!');
    -     * });
    -     * 
    + * }]); + * ``` * * Then you can create an injector and load your modules like this: * - *
    -     * var injector = angular.injector(['ng', 'MyModule'])
    -     * 
    + * ```js + * var injector = angular.injector(['ng', 'myModule']) + * ``` * * However it's more likely that you'll just use * {@link ng.directive:ngApp ngApp} or * {@link angular.bootstrap} to simplify this process for you. * * @param {!string} name The name of the module to create or retrieve. - * @param {Array.=} requires If specified then new module is being created. If - * unspecified then the the module is being retrieved for further configuration. +<<<<<* @param {!Array.=} requires If specified then new module is being created. If +>>>>>* unspecified then the module is being retrieved for further configuration. * @param {Function} configFn Optional configuration function for the module. Same as - * {@link angular.Module#methods_config Module#config()}. + * {@link angular.Module#config Module#config()}. * @returns {module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { @@ -1548,7 +1630,7 @@ function setupModuleLoader(window) { /** * @ngdoc property * @name angular.Module#requires - * @propertyOf angular.Module + * @module ng * @returns {Array.} List of module names which must be loaded before this module. * @description * Holds the list of modules which the injector will load before the current module is @@ -1559,7 +1641,7 @@ function setupModuleLoader(window) { /** * @ngdoc property * @name angular.Module#name - * @propertyOf angular.Module + * @module ng * @returns {string} Name of the module. * @description */ @@ -1569,64 +1651,64 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#provider - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {Function} providerType Construction function for creating new instance of the * service. * @description - * See {@link AUTO.$provide#provider $provide.provider()}. + * See {@link auto.$provide#provider $provide.provider()}. */ provider: invokeLater('$provide', 'provider'), /** * @ngdoc method * @name angular.Module#factory - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {Function} providerFunction Function for creating new instance of the service. * @description - * See {@link AUTO.$provide#factory $provide.factory()}. + * See {@link auto.$provide#factory $provide.factory()}. */ factory: invokeLater('$provide', 'factory'), /** * @ngdoc method * @name angular.Module#service - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {Function} constructor A constructor function that will be instantiated. * @description - * See {@link AUTO.$provide#service $provide.service()}. + * See {@link auto.$provide#service $provide.service()}. */ service: invokeLater('$provide', 'service'), /** * @ngdoc method * @name angular.Module#value - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {*} object Service instance object. * @description - * See {@link AUTO.$provide#value $provide.value()}. + * See {@link auto.$provide#value $provide.value()}. */ value: invokeLater('$provide', 'value'), /** * @ngdoc method * @name angular.Module#constant - * @methodOf angular.Module + * @module ng * @param {string} name constant name * @param {*} object Constant value. * @description * Because the constant are fixed, they get applied before other provide methods. - * See {@link AUTO.$provide#constant $provide.constant()}. + * See {@link auto.$provide#constant $provide.constant()}. */ constant: invokeLater('$provide', 'constant', 'unshift'), /** * @ngdoc method * @name angular.Module#animation - * @methodOf angular.Module + * @module ng * @param {string} name animation name * @param {Function} animationFactory Factory function for creating new instance of an * animation. @@ -1638,7 +1720,7 @@ function setupModuleLoader(window) { * Defines an animation hook that can be later used with * {@link ngAnimate.$animate $animate} service and directives that use this service. * - *
    +           * ```js
                * module.animation('.animation-name', function($inject1, $inject2) {
                *   return {
                *     eventName : function(element, done) {
    @@ -1650,7 +1732,7 @@ function setupModuleLoader(window) {
                *     }
                *   }
                * })
    -           * 
    + * ``` * * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and * {@link ngAnimate ngAnimate module} for more information. @@ -1660,7 +1742,7 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#filter - * @methodOf angular.Module + * @module ng * @param {string} name Filter name. * @param {Function} filterFactory Factory function for creating new instance of filter. * @description @@ -1671,7 +1753,7 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#controller - * @methodOf angular.Module + * @module ng * @param {string|Object} name Controller name, or an object map of controllers where the * keys are the names and the values are the constructors. * @param {Function} constructor Controller constructor function. @@ -1683,20 +1765,20 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#directive - * @methodOf angular.Module + * @module ng * @param {string|Object} name Directive name, or an object map of directives where the * keys are the names and the values are the factories. * @param {Function} directiveFactory Factory function for creating new instance of * directives. * @description - * See {@link ng.$compileProvider#methods_directive $compileProvider.directive()}. + * See {@link ng.$compileProvider#directive $compileProvider.directive()}. */ directive: invokeLater('$compileProvider', 'directive'), /** * @ngdoc method * @name angular.Module#config - * @methodOf angular.Module + * @module ng * @param {Function} configFn Execute this function on module load. Useful for service * configuration. * @description @@ -1707,7 +1789,7 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#run - * @methodOf angular.Module + * @module ng * @param {Function} initializationFn Execute this function after injector creation. * Useful for application initialization. * @description @@ -1747,10 +1829,10 @@ function setupModuleLoader(window) { /* global angularModule: true, version: true, - + $LocaleProvider, $CompileProvider, - + htmlAnchorDirective, inputDirective, inputDirective, @@ -1816,13 +1898,16 @@ function setupModuleLoader(window) { $SnifferProvider, $TemplateCacheProvider, $TimeoutProvider, + $$RAFProvider, + $$AsyncCallbackProvider, $WindowProvider */ /** - * @ngdoc property + * @ngdoc object * @name angular.version + * @module ng * @description * An object that contains information about the current AngularJS version. This object has the * following properties: @@ -1834,11 +1919,11 @@ function setupModuleLoader(window) { * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.2.10', // all of these placeholder strings will be replaced by grunt's + full: '1.2.16', // all of these placeholder strings will be replaced by grunt's major: 1, // package task minor: 2, - dot: 10, - codeName: 'augmented-serendipity' + dot: 16, + codeName: 'badger-enumeration' }; @@ -1954,7 +2039,9 @@ function publishExternalAPI(angular){ $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, $timeout: $TimeoutProvider, - $window: $WindowProvider + $window: $WindowProvider, + $$rAF: $$RAFProvider, + $$asyncCallback : $$AsyncCallbackProvider }); } ]); @@ -1975,6 +2062,7 @@ function publishExternalAPI(angular){ /** * @ngdoc function * @name angular.element + * @module ng * @function * * @description @@ -2000,7 +2088,7 @@ function publishExternalAPI(angular){ * - [`after()`](http://api.jquery.com/after/) * - [`append()`](http://api.jquery.com/append/) * - [`attr()`](http://api.jquery.com/attr/) - * - [`bind()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData + * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData * - [`children()`](http://api.jquery.com/children/) - Does not support selectors * - [`clone()`](http://api.jquery.com/clone/) * - [`contents()`](http://api.jquery.com/contents/) @@ -2027,7 +2115,7 @@ function publishExternalAPI(angular){ * - [`text()`](http://api.jquery.com/text/) * - [`toggleClass()`](http://api.jquery.com/toggleClass/) * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. - * - [`unbind()`](http://api.jquery.com/off/) - Does not support namespaces + * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces * - [`val()`](http://api.jquery.com/val/) * - [`wrap()`](http://api.jquery.com/wrap/) * @@ -2045,9 +2133,9 @@ function publishExternalAPI(angular){ * camelCase directive name, then the controller for this directive will be retrieved (e.g. * `'ngModel'`). * - `injector()` - retrieves the injector of the current element or its parent. - * - `scope()` - retrieves the {@link api/ng.$rootScope.Scope scope} of the current + * - `scope()` - retrieves the {@link ng.$rootScope.Scope scope} of the current * element or its parent. - * - `isolateScope()` - retrieves an isolate {@link api/ng.$rootScope.Scope scope} if one is attached directly to the + * - `isolateScope()` - retrieves an isolate {@link ng.$rootScope.Scope scope} if one is attached directly to the * current element. This getter should be used only on elements that contain a directive which starts a new isolate * scope. Calling `scope()` on this element always returns the original non-isolate scope. * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top @@ -2067,6 +2155,14 @@ var jqCache = JQLite.cache = {}, ? function(element, type, fn) {element.removeEventListener(type, fn, false); } : function(element, type, fn) {element.detachEvent('on' + type, fn); }); +/* + * !!! This is an undocumented "private" function !!! + */ +var jqData = JQLite._data = function(node) { + //jQuery always returns an object on cache miss + return this.cache[node[this.expando]] || {}; +}; + function jqNextId() { return ++jqId; } @@ -2130,11 +2226,83 @@ function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArgu } } +var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; +var HTML_REGEXP = /<|&#?\w+;/; +var TAG_NAME_REGEXP = /<([\w:]+)/; +var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi; + +var wrapMap = { + 'option': [1, ''], + + 'thead': [1, '', '
    '], + 'col': [2, '', '
    '], + 'tr': [2, '', '
    '], + 'td': [3, '', '
    '], + '_default': [0, "", ""] +}; + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +function jqLiteIsTextNode(html) { + return !HTML_REGEXP.test(html); +} + +function jqLiteBuildFragment(html, context) { + var elem, tmp, tag, wrap, + fragment = context.createDocumentFragment(), + nodes = [], i, j, jj; + + if (jqLiteIsTextNode(html)) { + // Convert non-html into a text node + nodes.push(context.createTextNode(html)); + } else { + tmp = fragment.appendChild(context.createElement('div')); + // Convert html into DOM nodes + tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase(); + wrap = wrapMap[tag] || wrapMap._default; + tmp.innerHTML = '
     
    ' + + wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1>") + wrap[2]; + tmp.removeChild(tmp.firstChild); + + // Descend through wrappers to the right content + i = wrap[0]; + while (i--) { + tmp = tmp.lastChild; + } + + for (j=0, jj=tmp.childNodes.length; j} modules A list of module functions or their aliases. See * {@link angular.module}. The `ng` module must be explicitly added. - * @returns {function()} Injector function. See {@link AUTO.$injector $injector}. + * @returns {function()} Injector function. See {@link auto.$injector $injector}. * * @example * Typical usage - *
    + * ```js
      *   // create an injector
      *   var $injector = angular.injector(['ng']);
      *
    @@ -2952,7 +3125,7 @@ HashMap.prototype = {
      *     $compile($document)($rootScope);
      *     $rootScope.$digest();
      *   });
    - * 
    + * ``` * * Sometimes you want to get access to the injector of a currently running Angular app * from outside Angular. Perhaps, you want to inject and compile some markup after the @@ -2966,7 +3139,7 @@ HashMap.prototype = { * directive is added to the end of the document body by JQuery. We then compile and link * it into the current AngularJS scope. * - *
    + * ```js
      * var $div = $('
    {{content.label}}
    '); * $(document.body).append($div); * @@ -2974,16 +3147,16 @@ HashMap.prototype = { * var scope = angular.element($div).scope(); * $compile($div)(scope); * }); - *
    + * ``` */ /** - * @ngdoc overview - * @name AUTO + * @ngdoc module + * @name auto * @description * - * Implicit module which gets automatically added to each {@link AUTO.$injector $injector}. + * Implicit module which gets automatically added to each {@link auto.$injector $injector}. */ var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; @@ -3024,32 +3197,32 @@ function annotate(fn) { /////////////////////////////////////// /** - * @ngdoc object - * @name AUTO.$injector + * @ngdoc service + * @name $injector * @function * * @description * * `$injector` is used to retrieve object instances as defined by - * {@link AUTO.$provide provider}, instantiate types, invoke methods, + * {@link auto.$provide provider}, instantiate types, invoke methods, * and load modules. * * The following always holds true: * - *
    + * ```js
      *   var $injector = angular.injector();
      *   expect($injector.get('$injector')).toBe($injector);
      *   expect($injector.invoke(function($injector){
      *     return $injector;
      *   }).toBe($injector);
    - * 
    + * ``` * * # Injection Function Annotation * * JavaScript does not have annotations, and annotations are needed for dependency injection. The * following are all valid ways of annotating function with injection arguments and are equivalent. * - *
    + * ```js
      *   // inferred (only works if code not minified/obfuscated)
      *   $injector.invoke(function(serviceA){});
      *
    @@ -3060,7 +3233,7 @@ function annotate(fn) {
      *
      *   // inline
      *   $injector.invoke(['serviceA', function(serviceA){}]);
    - * 
    + * ``` * * ## Inference * @@ -3077,8 +3250,7 @@ function annotate(fn) { /** * @ngdoc method - * @name AUTO.$injector#get - * @methodOf AUTO.$injector + * @name $injector#get * * @description * Return an instance of the service. @@ -3089,13 +3261,12 @@ function annotate(fn) { /** * @ngdoc method - * @name AUTO.$injector#invoke - * @methodOf AUTO.$injector + * @name $injector#invoke * * @description * Invoke the method and supply the method arguments from the `$injector`. * - * @param {!function} fn The function to invoke. Function parameters are injected according to the + * @param {!Function} fn The function to invoke. Function parameters are injected according to the * {@link guide/di $inject Annotation} rules. * @param {Object=} self The `this` for the invoked method. * @param {Object=} locals Optional object. If preset then any argument names are read from this @@ -3105,8 +3276,7 @@ function annotate(fn) { /** * @ngdoc method - * @name AUTO.$injector#has - * @methodOf AUTO.$injector + * @name $injector#has * * @description * Allows the user to query if the particular service exist. @@ -3117,14 +3287,13 @@ function annotate(fn) { /** * @ngdoc method - * @name AUTO.$injector#instantiate - * @methodOf AUTO.$injector + * @name $injector#instantiate * @description * Create a new instance of JS type. The method takes a constructor function invokes the new * operator and supplies all of the arguments to the constructor function as specified by the * constructor annotation. * - * @param {function} Type Annotated constructor function. + * @param {Function} Type Annotated constructor function. * @param {Object=} locals Optional object. If preset then any argument names are read from this * object first, before the `$injector` is consulted. * @returns {Object} new instance of `Type`. @@ -3132,8 +3301,7 @@ function annotate(fn) { /** * @ngdoc method - * @name AUTO.$injector#annotate - * @methodOf AUTO.$injector + * @name $injector#annotate * * @description * Returns an array of service names which the function is requesting for injection. This API is @@ -3146,7 +3314,7 @@ function annotate(fn) { * The simplest form is to extract the dependencies from the arguments of the function. This is done * by converting the function into a string using `toString()` method and extracting the argument * names. - *
    + * ```js
      *   // Given
      *   function MyController($scope, $route) {
      *     // ...
    @@ -3154,7 +3322,7 @@ function annotate(fn) {
      *
      *   // Then
      *   expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
    - * 
    + * ``` * * This method does not work with code minification / obfuscation. For this reason the following * annotation strategies are supported. @@ -3163,7 +3331,7 @@ function annotate(fn) { * * If a function has an `$inject` property and its value is an array of strings, then the strings * represent names of services to be injected into the function. - *
    + * ```js
      *   // Given
      *   var MyController = function(obfuscatedScope, obfuscatedRoute) {
      *     // ...
    @@ -3173,7 +3341,7 @@ function annotate(fn) {
      *
      *   // Then
      *   expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
    - * 
    + * ``` * * # The array notation * @@ -3181,7 +3349,7 @@ function annotate(fn) { * is very inconvenient. In these situations using the array notation to specify the dependencies in * a way that survives minification is a better choice: * - *
    + * ```js
      *   // We wish to write this (not minification / obfuscation safe)
      *   injector.invoke(function($compile, $rootScope) {
      *     // ...
    @@ -3203,9 +3371,9 @@ function annotate(fn) {
      *   expect(injector.annotate(
      *      ['$compile', '$rootScope', function(obfus_$compile, obfus_$rootScope) {}])
      *    ).toEqual(['$compile', '$rootScope']);
    - * 
    + * ``` * - * @param {function|Array.} fn Function for which dependent service names need to + * @param {Function|Array.} fn Function for which dependent service names need to * be retrieved as described above. * * @returns {Array.} The names of the services which the function requires. @@ -3216,12 +3384,12 @@ function annotate(fn) { /** * @ngdoc object - * @name AUTO.$provide + * @name $provide * * @description * - * The {@link AUTO.$provide $provide} service has a number of methods for registering components - * with the {@link AUTO.$injector $injector}. Many of these functions are also exposed on + * The {@link auto.$provide $provide} service has a number of methods for registering components + * with the {@link auto.$injector $injector}. Many of these functions are also exposed on * {@link angular.Module}. * * An Angular **service** is a singleton object created by a **service factory**. These **service @@ -3229,25 +3397,25 @@ function annotate(fn) { * The **service providers** are constructor functions. When instantiated they must contain a * property called `$get`, which holds the **service factory** function. * - * When you request a service, the {@link AUTO.$injector $injector} is responsible for finding the + * When you request a service, the {@link auto.$injector $injector} is responsible for finding the * correct **service provider**, instantiating it and then calling its `$get` **service factory** * function to get the instance of the **service**. * * Often services have no configuration options and there is no need to add methods to the service * provider. The provider will be no more than a constructor function with a `$get` property. For - * these cases the {@link AUTO.$provide $provide} service has additional helper methods to register + * these cases the {@link auto.$provide $provide} service has additional helper methods to register * services without specifying a provider. * - * * {@link AUTO.$provide#methods_provider provider(provider)} - registers a **service provider** with the - * {@link AUTO.$injector $injector} - * * {@link AUTO.$provide#methods_constant constant(obj)} - registers a value/object that can be accessed by + * * {@link auto.$provide#provider provider(provider)} - registers a **service provider** with the + * {@link auto.$injector $injector} + * * {@link auto.$provide#constant constant(obj)} - registers a value/object that can be accessed by * providers and services. - * * {@link AUTO.$provide#methods_value value(obj)} - registers a value/object that can only be accessed by + * * {@link auto.$provide#value value(obj)} - registers a value/object that can only be accessed by * services, not providers. - * * {@link AUTO.$provide#methods_factory factory(fn)} - registers a service **factory function**, `fn`, + * * {@link auto.$provide#factory factory(fn)} - registers a service **factory function**, `fn`, * that will be wrapped in a **service provider** object, whose `$get` property will contain the * given factory function. - * * {@link AUTO.$provide#methods_service service(class)} - registers a **constructor function**, `class` that + * * {@link auto.$provide#service service(class)} - registers a **constructor function**, `class` * that will be wrapped in a **service provider** object, whose `$get` property will instantiate * a new object using the given constructor function. * @@ -3256,11 +3424,10 @@ function annotate(fn) { /** * @ngdoc method - * @name AUTO.$provide#provider - * @methodOf AUTO.$provide + * @name $provide#provider * @description * - * Register a **provider function** with the {@link AUTO.$injector $injector}. Provider functions + * Register a **provider function** with the {@link auto.$injector $injector}. Provider functions * are constructor functions, whose instances are responsible for "providing" a factory for a * service. * @@ -3280,18 +3447,18 @@ function annotate(fn) { * @param {(Object|function())} provider If the provider is: * * - `Object`: then it should have a `$get` method. The `$get` method will be invoked using - * {@link AUTO.$injector#invoke $injector.invoke()} when an instance needs to be created. - * - `Constructor`: a new instance of the provider will be created using - * {@link AUTO.$injector#instantiate $injector.instantiate()}, then treated as `object`. + * {@link auto.$injector#invoke $injector.invoke()} when an instance needs to be created. + * - `Constructor`: a new instance of the provider will be created using + * {@link auto.$injector#instantiate $injector.instantiate()}, then treated as `object`. * * @returns {Object} registered provider instance * @example * * The following example shows how to create a simple event tracking service and register it using - * {@link AUTO.$provide#methods_provider $provide.provider()}. + * {@link auto.$provide#provider $provide.provider()}. * - *
    + * ```js
      *  // Define the eventTracker provider
      *  function EventTrackerProvider() {
      *    var trackingUrl = '/track';
    @@ -3348,19 +3515,18 @@ function annotate(fn) {
      *      expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 });
      *    }));
      *  });
    - * 
    + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#factory - * @methodOf AUTO.$provide + * @name $provide#factory * @description * * Register a **service factory**, which will be called to return the service instance. * This is short for registering a service where its provider consists of only a `$get` property, * which is the given service factory function. - * You should use {@link AUTO.$provide#factory $provide.factory(getFn)} if you do not need to + * You should use {@link auto.$provide#factory $provide.factory(getFn)} if you do not need to * configure your service in a provider. * * @param {string} name The name of the instance. @@ -3370,26 +3536,25 @@ function annotate(fn) { * * @example * Here is an example of registering a service - *
    + * ```js
      *   $provide.factory('ping', ['$http', function($http) {
      *     return function ping() {
      *       return $http.send('/ping');
      *     };
      *   }]);
    - * 
    + * ``` * You would then inject and use this service like this: - *
    + * ```js
      *   someModule.controller('Ctrl', ['ping', function(ping) {
      *     ping();
      *   }]);
    - * 
    + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#service - * @methodOf AUTO.$provide + * @name $provide#service * @description * * Register a **service constructor**, which will be invoked with `new` to create the service @@ -3397,7 +3562,7 @@ function annotate(fn) { * This is short for registering a service where its provider's `$get` property is the service * constructor function that will be used to instantiate the service instance. * - * You should use {@link AUTO.$provide#methods_service $provide.service(class)} if you define your service + * You should use {@link auto.$provide#service $provide.service(class)} if you define your service * as a type/class. * * @param {string} name The name of the instance. @@ -3406,36 +3571,34 @@ function annotate(fn) { * * @example * Here is an example of registering a service using - * {@link AUTO.$provide#methods_service $provide.service(class)}. - *
    - *   $provide.service('ping', ['$http', function($http) {
    - *     var Ping = function() {
    - *       this.$http = $http;
    - *     };
    - *   
    - *     Ping.prototype.send = function() {
    - *       return this.$http.get('/ping');
    - *     }; 
    - *   
    - *     return Ping;
    - *   }]);
    - * 
    + * {@link auto.$provide#service $provide.service(class)}. + * ```js + * var Ping = function($http) { + * this.$http = $http; + * }; + * + * Ping.$inject = ['$http']; + * + * Ping.prototype.send = function() { + * return this.$http.get('/ping'); + * }; + * $provide.service('ping', Ping); + * ``` * You would then inject and use this service like this: - *
    + * ```js
      *   someModule.controller('Ctrl', ['ping', function(ping) {
      *     ping.send();
      *   }]);
    - * 
    + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#value - * @methodOf AUTO.$provide + * @name $provide#value * @description * - * Register a **value service** with the {@link AUTO.$injector $injector}, such as a string, a + * Register a **value service** with the {@link auto.$injector $injector}, such as a string, a * number, an array, an object or a function. This is short for registering a service where its * provider's `$get` property is a factory function that takes no arguments and returns the **value * service**. @@ -3443,7 +3606,7 @@ function annotate(fn) { * Value services are similar to constant services, except that they cannot be injected into a * module configuration function (see {@link angular.Module#config}) but they can be overridden by * an Angular - * {@link AUTO.$provide#decorator decorator}. + * {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the instance. * @param {*} value The value. @@ -3451,7 +3614,7 @@ function annotate(fn) { * * @example * Here are some examples of creating value services. - *
    + * ```js
      *   $provide.value('ADMIN_USER', 'admin');
      *
      *   $provide.value('RoleLookup', { admin: 0, writer: 1, reader: 2 });
    @@ -3459,20 +3622,19 @@ function annotate(fn) {
      *   $provide.value('halfOf', function(value) {
      *     return value / 2;
      *   });
    - * 
    + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#constant - * @methodOf AUTO.$provide + * @name $provide#constant * @description * * Register a **constant service**, such as a string, a number, an array, an object or a function, - * with the {@link AUTO.$injector $injector}. Unlike {@link AUTO.$provide#value value} it can be + * with the {@link auto.$injector $injector}. Unlike {@link auto.$provide#value value} it can be * injected into a module configuration function (see {@link angular.Module#config}) and it cannot - * be overridden by an Angular {@link AUTO.$provide#decorator decorator}. + * be overridden by an Angular {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the constant. * @param {*} value The constant value. @@ -3480,7 +3642,7 @@ function annotate(fn) { * * @example * Here a some examples of creating constants: - *
    + * ```js
      *   $provide.constant('SHARD_HEIGHT', 306);
      *
      *   $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']);
    @@ -3488,17 +3650,16 @@ function annotate(fn) {
      *   $provide.constant('double', function(value) {
      *     return value * 2;
      *   });
    - * 
    + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#decorator - * @methodOf AUTO.$provide + * @name $provide#decorator * @description * - * Register a **service decorator** with the {@link AUTO.$injector $injector}. A service decorator + * Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator * intercepts the creation of a service, allowing it to override or modify the behaviour of the * service. The object returned by the decorator may be the original service, or a new service * object which replaces or wraps and delegates to the original service. @@ -3506,7 +3667,7 @@ function annotate(fn) { * @param {string} name The name of the service to decorate. * @param {function()} decorator This function will be invoked when the service needs to be * instantiated and should return the decorated service instance. The function is called using - * the {@link AUTO.$injector#invoke injector.invoke} method and is therefore fully injectable. + * the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable. * Local injection arguments: * * * `$delegate` - The original service instance, which can be monkey patched, configured, @@ -3515,12 +3676,12 @@ function annotate(fn) { * @example * Here we decorate the {@link ng.$log $log} service to convert warnings to errors by intercepting * calls to {@link ng.$log#error $log.warn()}. - *
    - *   $provider.decorator('$log', ['$delegate', function($delegate) {
    + * ```js
    + *   $provide.decorator('$log', ['$delegate', function($delegate) {
      *     $delegate.warn = $delegate.error;
      *     return $delegate;
      *   }]);
    - * 
    + * ``` */ @@ -3734,8 +3895,9 @@ function createInjector(modulesToLoad) { } /** - * @ngdoc function - * @name ng.$anchorScroll + * @ngdoc service + * @name $anchorScroll + * @kind function * @requires $window * @requires $location * @requires $rootScope @@ -3743,11 +3905,11 @@ function createInjector(modulesToLoad) { * @description * When called, it checks current value of `$location.hash()` and scroll to related element, * according to rules specified in - * {@link http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document Html5 spec}. + * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document). * * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. - * + * * @example @@ -3762,10 +3924,10 @@ function createInjector(modulesToLoad) { // set the location.hash to the id of // the element you wish to scroll to. $location.hash('bottom'); - + // call $anchorScroll() $anchorScroll(); - } + }; } @@ -3836,8 +3998,8 @@ function $AnchorScrollProvider() { var $animateMinErr = minErr('$animate'); /** - * @ngdoc object - * @name ng.$animateProvider + * @ngdoc provider + * @name $animateProvider * * @description * Default implementation of $animate that doesn't perform any animations, instead just @@ -3850,14 +4012,13 @@ var $animateMinErr = minErr('$animate'); */ var $AnimateProvider = ['$provide', function($provide) { - + this.$$selectors = {}; /** - * @ngdoc function - * @name ng.$animateProvider#register - * @methodOf ng.$animateProvider + * @ngdoc method + * @name $animateProvider#register * * @description * Registers a new injectable animation factory function. The factory function produces the @@ -3870,7 +4031,7 @@ var $AnimateProvider = ['$provide', function($provide) { * triggered. * * - *
    +   * ```js
        *   return {
          *     eventFn : function(element, done) {
          *       //code to run the animation
    @@ -3880,10 +4041,10 @@ var $AnimateProvider = ['$provide', function($provide) {
          *       }
          *     }
          *   }
    -   *
    + * ``` * * @param {string} name The name of the animation. - * @param {function} factory The factory function that will be executed to return the animation + * @param {Function} factory The factory function that will be executed to return the animation * object. */ this.register = function(name, factory) { @@ -3895,9 +4056,8 @@ var $AnimateProvider = ['$provide', function($provide) { }; /** - * @ngdoc function - * @name ng.$animateProvider#classNameFilter - * @methodOf ng.$animateProvider + * @ngdoc method + * @name $animateProvider#classNameFilter * * @description * Sets and/or returns the CSS class regular expression that is checked when performing @@ -3916,12 +4076,16 @@ var $AnimateProvider = ['$provide', function($provide) { return this.$$classNameFilter; }; - this.$get = ['$timeout', function($timeout) { + this.$get = ['$timeout', '$$asyncCallback', function($timeout, $$asyncCallback) { + + function async(fn) { + fn && $$asyncCallback(fn); + } /** * - * @ngdoc object - * @name ng.$animate + * @ngdoc service + * @name $animate * @description The $animate service provides rudimentary DOM manipulation functions to * insert, remove and move elements within the DOM, as well as adding and removing classes. * This service is the core service used by the ngAnimate $animator service which provides @@ -3939,18 +4103,17 @@ var $AnimateProvider = ['$provide', function($provide) { /** * - * @ngdoc function - * @name ng.$animate#enter - * @methodOf ng.$animate + * @ngdoc method + * @name $animate#enter * @function * @description Inserts the element into the DOM either after the `after` element or within * the `parent` element. Once complete, the done() callback will be fired (if provided). - * @param {jQuery/jqLite element} element the element which will be inserted into the DOM - * @param {jQuery/jqLite element} parent the parent element which will append the element as + * @param {DOMElement} element the element which will be inserted into the DOM + * @param {DOMElement} parent the parent element which will append the element as * a child (if the after element is not present) - * @param {jQuery/jqLite element} after the sibling element which will append the element + * @param {DOMElement} after the sibling element which will append the element * after itself - * @param {function=} done callback function that will be called after the element has been + * @param {Function=} done callback function that will be called after the element has been * inserted into the DOM */ enter : function(element, parent, after, done) { @@ -3962,43 +4125,41 @@ var $AnimateProvider = ['$provide', function($provide) { } parent.append(element); } - done && $timeout(done, 0, false); + async(done); }, /** * - * @ngdoc function - * @name ng.$animate#leave - * @methodOf ng.$animate + * @ngdoc method + * @name $animate#leave * @function * @description Removes the element from the DOM. Once complete, the done() callback will be * fired (if provided). - * @param {jQuery/jqLite element} element the element which will be removed from the DOM - * @param {function=} done callback function that will be called after the element has been + * @param {DOMElement} element the element which will be removed from the DOM + * @param {Function=} done callback function that will be called after the element has been * removed from the DOM */ leave : function(element, done) { element.remove(); - done && $timeout(done, 0, false); + async(done); }, /** * - * @ngdoc function - * @name ng.$animate#move - * @methodOf ng.$animate + * @ngdoc method + * @name $animate#move * @function * @description Moves the position of the provided element within the DOM to be placed * either after the `after` element or inside of the `parent` element. Once complete, the * done() callback will be fired (if provided). - * - * @param {jQuery/jqLite element} element the element which will be moved around within the + * + * @param {DOMElement} element the element which will be moved around within the * DOM - * @param {jQuery/jqLite element} parent the parent element where the element will be + * @param {DOMElement} parent the parent element where the element will be * inserted into (if the after element is not present) - * @param {jQuery/jqLite element} after the sibling element where the element will be + * @param {DOMElement} after the sibling element where the element will be * positioned next to - * @param {function=} done the callback function (if provided) that will be fired after the + * @param {Function=} done the callback function (if provided) that will be fired after the * element has been moved to its new position */ move : function(element, parent, after, done) { @@ -4009,16 +4170,15 @@ var $AnimateProvider = ['$provide', function($provide) { /** * - * @ngdoc function - * @name ng.$animate#addClass - * @methodOf ng.$animate + * @ngdoc method + * @name $animate#addClass * @function * @description Adds the provided className CSS class value to the provided element. Once * complete, the done() callback will be fired (if provided). - * @param {jQuery/jqLite element} element the element which will have the className value + * @param {DOMElement} element the element which will have the className value * added to it * @param {string} className the CSS class which will be added to the element - * @param {function=} done the callback function (if provided) that will be fired after the + * @param {Function=} done the callback function (if provided) that will be fired after the * className value has been added to the element */ addClass : function(element, className, done) { @@ -4028,21 +4188,20 @@ var $AnimateProvider = ['$provide', function($provide) { forEach(element, function (element) { jqLiteAddClass(element, className); }); - done && $timeout(done, 0, false); + async(done); }, /** * - * @ngdoc function - * @name ng.$animate#removeClass - * @methodOf ng.$animate + * @ngdoc method + * @name $animate#removeClass * @function * @description Removes the provided className CSS class value from the provided element. * Once complete, the done() callback will be fired (if provided). - * @param {jQuery/jqLite element} element the element which will have the className value + * @param {DOMElement} element the element which will have the className value * removed from it * @param {string} className the CSS class which will be removed from the element - * @param {function=} done the callback function (if provided) that will be fired after the + * @param {Function=} done the callback function (if provided) that will be fired after the * className value has been removed from the element */ removeClass : function(element, className, done) { @@ -4052,7 +4211,29 @@ var $AnimateProvider = ['$provide', function($provide) { forEach(element, function (element) { jqLiteRemoveClass(element, className); }); - done && $timeout(done, 0, false); + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#setClass + * @function + * @description Adds and/or removes the given CSS classes to and from the element. + * Once complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will it's CSS classes changed + * removed from it + * @param {string} add the CSS classes which will be added to the element + * @param {string} remove the CSS class which will be removed from the element + * @param {Function=} done the callback function (if provided) that will be fired after the + * CSS classes have been set on the element + */ + setClass : function(element, add, remove, done) { + forEach(element, function (element) { + jqLiteAddClass(element, add); + jqLiteRemoveClass(element, remove); + }); + async(done); }, enabled : noop @@ -4060,10 +4241,20 @@ var $AnimateProvider = ['$provide', function($provide) { }]; }]; +function $$AsyncCallbackProvider(){ + this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) { + return $$rAF.supported + ? function(fn) { return $$rAF(fn); } + : function(fn) { + return $timeout(fn, 0, false); + }; + }]; +} + /** * ! This is a private undocumented service ! * - * @name ng.$browser + * @name $browser * @requires $log * @description * This object has two goals: @@ -4147,8 +4338,7 @@ function Browser(window, document, $log, $sniffer) { pollTimeout; /** - * @name ng.$browser#addPollFn - * @methodOf ng.$browser + * @name $browser#addPollFn * * @param {function()} fn Poll function to add * @@ -4188,8 +4378,7 @@ function Browser(window, document, $log, $sniffer) { newLocation = null; /** - * @name ng.$browser#url - * @methodOf ng.$browser + * @name $browser#url * * @description * GETTER: @@ -4255,9 +4444,7 @@ function Browser(window, document, $log, $sniffer) { } /** - * @name ng.$browser#onUrlChange - * @methodOf ng.$browser - * @TODO(vojta): refactor to use node's syntax for events + * @name $browser#onUrlChange * * @description * Register callback function that will be called, when url changes. @@ -4278,6 +4465,7 @@ function Browser(window, document, $log, $sniffer) { * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. */ self.onUrlChange = function(callback) { + // TODO(vojta): refactor to use node's syntax for events if (!urlChangeInit) { // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) // don't fire popstate when user change the address bar and don't fire hashchange when url @@ -4302,14 +4490,13 @@ function Browser(window, document, $log, $sniffer) { ////////////////////////////////////////////////////////////// /** - * @name ng.$browser#baseHref - * @methodOf ng.$browser + * @name $browser#baseHref * * @description * Returns current * (always relative - without domain) * - * @returns {string=} current + * @returns {string} The current base href */ self.baseHref = function() { var href = baseElement.attr('href'); @@ -4324,8 +4511,7 @@ function Browser(window, document, $log, $sniffer) { var cookiePath = self.baseHref(); /** - * @name ng.$browser#cookies - * @methodOf ng.$browser + * @name $browser#cookies * * @param {string=} name Cookie name * @param {string=} value Cookie value @@ -4394,8 +4580,7 @@ function Browser(window, document, $log, $sniffer) { /** - * @name ng.$browser#defer - * @methodOf ng.$browser + * @name $browser#defer * @param {function()} fn A function, who's execution should be deferred. * @param {number=} [delay=0] of milliseconds to defer the function execution. * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. @@ -4421,8 +4606,7 @@ function Browser(window, document, $log, $sniffer) { /** - * @name ng.$browser#defer.cancel - * @methodOf ng.$browser.defer + * @name $browser#defer.cancel * * @description * Cancels a deferred task identified with `deferId`. @@ -4451,14 +4635,15 @@ function $BrowserProvider(){ } /** - * @ngdoc object - * @name ng.$cacheFactory + * @ngdoc service + * @name $cacheFactory * * @description - * Factory that constructs cache objects and gives access to them. - * - *
    - * 
    + * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to
    + * them.
    + *
    + * ```js
    + *
      *  var cache = $cacheFactory('cacheId');
      *  expect($cacheFactory.get('cacheId')).toBe(cache);
      *  expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined();
    @@ -4467,9 +4652,9 @@ function $BrowserProvider(){
      *  cache.put("another key", "another value");
      *
      *  // We've specified no options on creation
    - *  expect(cache.info()).toEqual({id: 'cacheId', size: 2}); 
    - * 
    - * 
    + * expect(cache.info()).toEqual({id: 'cacheId', size: 2}); + * + * ``` * * * @param {string} cacheId Name or id of the newly created cache. @@ -4487,6 +4672,46 @@ function $BrowserProvider(){ * - `{void}` `removeAll()` — Removes all cached values. * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. * + * @example + + +
    + + + + +

    Cached Values

    +
    + + : + +
    + +

    Cache Info

    +
    + + : + +
    +
    +
    + + angular.module('cacheExampleApp', []). + controller('CacheController', ['$scope', '$cacheFactory', function($scope, $cacheFactory) { + $scope.keys = []; + $scope.cache = $cacheFactory('cacheId'); + $scope.put = function(key, value) { + $scope.cache.put(key, value); + $scope.keys.push(key); + }; + }]); + + + p { + margin: 10px 0 3px; + } + +
    */ function $CacheFactoryProvider() { @@ -4506,12 +4731,71 @@ function $CacheFactoryProvider() { freshEnd = null, staleEnd = null; + /** + * @ngdoc type + * @name $cacheFactory.Cache + * + * @description + * A cache object used to store and retrieve data, primarily used by + * {@link $http $http} and the {@link ng.directive:script script} directive to cache + * templates and other data. + * + * ```js + * angular.module('superCache') + * .factory('superCache', ['$cacheFactory', function($cacheFactory) { + * return $cacheFactory('super-cache'); + * }]); + * ``` + * + * Example test: + * + * ```js + * it('should behave like a cache', inject(function(superCache) { + * superCache.put('key', 'value'); + * superCache.put('another key', 'another value'); + * + * expect(superCache.info()).toEqual({ + * id: 'super-cache', + * size: 2 + * }); + * + * superCache.remove('another key'); + * expect(superCache.get('another key')).toBeUndefined(); + * + * superCache.removeAll(); + * expect(superCache.info()).toEqual({ + * id: 'super-cache', + * size: 0 + * }); + * })); + * ``` + */ return caches[cacheId] = { + /** + * @ngdoc method + * @name $cacheFactory.Cache#put + * @function + * + * @description + * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be + * retrieved later, and incrementing the size of the cache if the key was not already + * present in the cache. If behaving like an LRU cache, it will also remove stale + * entries from the set. + * + * It will not insert undefined values into the cache. + * + * @param {string} key the key under which the cached data is stored. + * @param {*} value the value to store alongside the key. If it is undefined, the key + * will not be stored. + * @returns {*} the value stored. + */ put: function(key, value) { - var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); - refresh(lruEntry); + refresh(lruEntry); + } if (isUndefined(value)) return; if (!(key in data)) size++; @@ -4524,33 +4808,66 @@ function $CacheFactoryProvider() { return value; }, - + /** + * @ngdoc method + * @name $cacheFactory.Cache#get + * @function + * + * @description + * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object. + * + * @param {string} key the key of the data to be retrieved + * @returns {*} the value stored. + */ get: function(key) { - var lruEntry = lruHash[key]; + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key]; - if (!lruEntry) return; + if (!lruEntry) return; - refresh(lruEntry); + refresh(lruEntry); + } return data[key]; }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#remove + * @function + * + * @description + * Removes an entry from the {@link $cacheFactory.Cache Cache} object. + * + * @param {string} key the key of the entry to be removed + */ remove: function(key) { - var lruEntry = lruHash[key]; + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key]; - if (!lruEntry) return; + if (!lruEntry) return; - if (lruEntry == freshEnd) freshEnd = lruEntry.p; - if (lruEntry == staleEnd) staleEnd = lruEntry.n; - link(lruEntry.n,lruEntry.p); + if (lruEntry == freshEnd) freshEnd = lruEntry.p; + if (lruEntry == staleEnd) staleEnd = lruEntry.n; + link(lruEntry.n,lruEntry.p); + + delete lruHash[key]; + } - delete lruHash[key]; delete data[key]; size--; }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#removeAll + * @function + * + * @description + * Clears the cache object of any entries. + */ removeAll: function() { data = {}; size = 0; @@ -4559,6 +4876,15 @@ function $CacheFactoryProvider() { }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#destroy + * @function + * + * @description + * Destroys the {@link $cacheFactory.Cache Cache} object entirely, + * removing it from the {@link $cacheFactory $cacheFactory} set. + */ destroy: function() { data = null; stats = null; @@ -4567,6 +4893,22 @@ function $CacheFactoryProvider() { }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#info + * @function + * + * @description + * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}. + * + * @returns {object} an object with the following properties: + *
      + *
    • **id**: the id of the cache instance
    • + *
    • **size**: the number of entries kept in the cache instance
    • + *
    • **...**: any additional properties from the options object when creating the + * cache.
    • + *
    + */ info: function() { return extend({}, stats, {size: size}); } @@ -4606,8 +4948,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method - * @name ng.$cacheFactory#info - * @methodOf ng.$cacheFactory + * @name $cacheFactory#info * * @description * Get information about all the of the caches that have been created @@ -4625,8 +4966,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method - * @name ng.$cacheFactory#get - * @methodOf ng.$cacheFactory + * @name $cacheFactory#get * * @description * Get access to a cache object by the `cacheId` used when it was created. @@ -4644,48 +4984,44 @@ function $CacheFactoryProvider() { } /** - * @ngdoc object - * @name ng.$templateCache + * @ngdoc service + * @name $templateCache * * @description * The first time a template is used, it is loaded in the template cache for quick retrieval. You * can load templates directly into the cache in a `script` tag, or by consuming the * `$templateCache` service directly. - * + * * Adding via the `script` tag: - *
    - * 
    - * 
    - * 
    - * 
    - *   ...
    - * 
    - * 
    - * + * + * ```html + * + * ``` + * * **Note:** the `script` tag containing the template does not need to be included in the `head` of * the document, but it must be below the `ng-app` definition. - * + * * Adding via the $templateCache service: - * - *
    + *
    + * ```js
      * var myApp = angular.module('myApp', []);
      * myApp.run(function($templateCache) {
      *   $templateCache.put('templateId.html', 'This is the content of the template');
      * });
    - * 
    - * + * ``` + * * To retrieve the template later, simply use it in your HTML: - *
    + * ```html
      * 
    - *
    - * + * ``` + * * or get it via Javascript: - *
    + * ```js
      * $templateCache.get('templateId.html')
    - * 
    - * + * ``` + * * See {@link ng.$cacheFactory $cacheFactory}. * */ @@ -4714,8 +5050,8 @@ function $TemplateCacheProvider() { /** - * @ngdoc function - * @name ng.$compile + * @ngdoc service + * @name $compile * @function * * @description @@ -4723,7 +5059,7 @@ function $TemplateCacheProvider() { * can then be used to link {@link ng.$rootScope.Scope `scope`} and the template together. * * The compilation is a process of walking the DOM tree and matching DOM elements to - * {@link ng.$compileProvider#methods_directive directives}. + * {@link ng.$compileProvider#directive directives}. * *
    * **Note:** This document is an in-depth reference of all directive options. @@ -4745,7 +5081,7 @@ function $TemplateCacheProvider() { * * Here's an example directive declared with a Directive Definition Object: * - *
    + * ```js
      *   var myModule = angular.module(...);
      *
      *   myModule.directive('directiveName', function factory(injectables) {
    @@ -4759,6 +5095,7 @@ function $TemplateCacheProvider() {
      *       restrict: 'A',
      *       scope: false,
      *       controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
    + *       controllerAs: 'stringAlias',
      *       require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'],
      *       compile: function compile(tElement, tAttrs, transclude) {
      *         return {
    @@ -4778,7 +5115,7 @@ function $TemplateCacheProvider() {
      *     };
      *     return directiveDefinitionObject;
      *   });
    - * 
    + * ``` * *
    * **Note:** Any unspecified options will use the default value. You can see the default values below. @@ -4786,7 +5123,7 @@ function $TemplateCacheProvider() { * * Therefore the above can be simplified as: * - *
    + * ```js
      *   var myModule = angular.module(...);
      *
      *   myModule.directive('directiveName', function factory(injectables) {
    @@ -4797,13 +5134,13 @@ function $TemplateCacheProvider() {
      *     // or
      *     // return function postLink(scope, iElement, iAttrs) { ... }
      *   });
    - * 
    + * ``` * * * * ### Directive Definition Object * - * The directive definition object provides instructions to the {@link api/ng.$compile + * The directive definition object provides instructions to the {@link ng.$compile * compiler}. The attributes are: * * #### `priority` @@ -4926,7 +5263,7 @@ function $TemplateCacheProvider() { * You can specify `templateUrl` as a string representing the URL or as a function which takes two * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns * a string value representing the url. In either case, the template URL is passed through {@link - * api/ng.$sce#methods_getTrustedResourceUrl $sce.getTrustedResourceUrl}. + * api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. * * * #### `replace` @@ -4938,7 +5275,7 @@ function $TemplateCacheProvider() { * * #### `transclude` * compile the content of the element and make it available to the directive. - * Typically used with {@link api/ng.directive:ngTransclude + * Typically used with {@link ng.directive:ngTransclude * ngTransclude}. The advantage of transclusion is that the linking function receives a * transclusion function which is pre-bound to the correct scope. In a typical setup the widget * creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate` @@ -4951,15 +5288,15 @@ function $TemplateCacheProvider() { * * #### `compile` * - *
    + * ```js
      *   function compile(tElement, tAttrs, transclude) { ... }
    - * 
    + * ``` * * The compile function deals with transforming the template DOM. Since most directives do not do * template transformation, it is not used often. Examples that require compile functions are * directives that transform template DOM, such as {@link * api/ng.directive:ngRepeat ngRepeat}, or load the contents - * asynchronously, such as {@link api/ngRoute.directive:ngView ngView}. The + * asynchronously, such as {@link ngRoute.directive:ngView ngView}. The * compile function takes the following arguments. * * * `tElement` - template element - The element where the directive has been declared. It is @@ -4976,6 +5313,16 @@ function $TemplateCacheProvider() { * apply to all cloned DOM nodes within the compile function. Specifically, DOM listener registration * should be done in a linking function rather than in a compile function. *
    + + *
    + * **Note:** The compile function cannot handle directives that recursively use themselves in their + * own templates or compile functions. Compiling these directives results in an infinite loop and a + * stack overflow errors. + * + * This can be avoided by manually using $compile in the postLink function to imperatively compile + * a directive's template instead of relying on automatic template compilation via `template` or + * `templateUrl` declaration or manual compilation inside the compile function. + *
    * *
    * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it @@ -4996,16 +5343,16 @@ function $TemplateCacheProvider() { * #### `link` * This property is used only if the `compile` property is not defined. * - *
    + * ```js
      *   function link(scope, iElement, iAttrs, controller, transcludeFn) { ... }
    - * 
    + * ``` * * The link function is responsible for registering DOM listeners as well as updating the DOM. It is * executed after the template has been cloned. This is where most of the directive logic will be * put. * - * * `scope` - {@link api/ng.$rootScope.Scope Scope} - The scope to be used by the - * directive for registering {@link api/ng.$rootScope.Scope#methods_$watch watches}. + * * `scope` - {@link ng.$rootScope.Scope Scope} - The scope to be used by the + * directive for registering {@link ng.$rootScope.Scope#$watch watches}. * * * `iElement` - instance element - The element where the directive is to be used. It is safe to * manipulate the children of the element only in `postLink` function since the children have @@ -5036,7 +5383,7 @@ function $TemplateCacheProvider() { * * ### Attributes * - * The {@link api/ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the + * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the * `link()` or `compile()` functions. It has a variety of uses. * * accessing *Normalized attribute names:* @@ -5056,7 +5403,7 @@ function $TemplateCacheProvider() { * the only way to easily get the actual value because during the linking phase the interpolation * hasn't been evaluated yet and so the value is at this time set to `undefined`. * - *
    + * ```js
      * function linkingFn(scope, elm, attrs, ctrl) {
      *   // get the attribute value
      *   console.log(attrs.ngModel);
    @@ -5069,7 +5416,7 @@ function $TemplateCacheProvider() {
      *     console.log('ngModel has changed value to ' + value);
      *   });
      * }
    - * 
    + * ``` * * Below is an example using `$compileProvider`. * @@ -5078,8 +5425,8 @@ function $TemplateCacheProvider() { * to illustrate how `$compile` works. *
    * - - + + - -
    -
    - Date format:
    - Current time is: -
    - Blood 1 : {{blood_1}} - Blood 2 : {{blood_2}} - - - -
    -
    - -
    -
    + * + * + * + * + *
    + *
    + * Date format:
    + * Current time is: + *
    + * Blood 1 : {{blood_1}} + * Blood 2 : {{blood_2}} + * + * + * + *
    + *
    + * + *
    + *
    */ function interval(fn, delay, count, invokeApply) { var setInterval = $window.setInterval, @@ -8441,14 +8842,13 @@ function $IntervalProvider() { /** - * @ngdoc function - * @name ng.$interval#cancel - * @methodOf ng.$interval + * @ngdoc method + * @name $interval#cancel * * @description * Cancels a task associated with the `promise`. * - * @param {number} promise Promise returned by the `$interval` function. + * @param {promise} promise returned by the `$interval` function. * @returns {boolean} Returns `true` if the task was successfully canceled. */ interval.cancel = function(promise) { @@ -8466,8 +8866,8 @@ function $IntervalProvider() { } /** - * @ngdoc object - * @name ng.$locale + * @ngdoc service + * @name $locale * * @description * $locale service provides localization rules for various Angular components. As of right now the @@ -8826,14 +9226,13 @@ LocationHashbangInHtml5Url.prototype = /** * @ngdoc method - * @name ng.$location#absUrl - * @methodOf ng.$location + * @name $location#absUrl * * @description * This method is getter only. * * Return full url representation with all segments encoded according to rules specified in - * {@link http://www.ietf.org/rfc/rfc3986.txt RFC 3986}. + * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). * * @return {string} full url */ @@ -8841,8 +9240,7 @@ LocationHashbangInHtml5Url.prototype = /** * @ngdoc method - * @name ng.$location#url - * @methodOf ng.$location + * @name $location#url * * @description * This method is getter / setter. @@ -8869,8 +9267,7 @@ LocationHashbangInHtml5Url.prototype = /** * @ngdoc method - * @name ng.$location#protocol - * @methodOf ng.$location + * @name $location#protocol * * @description * This method is getter only. @@ -8883,8 +9280,7 @@ LocationHashbangInHtml5Url.prototype = /** * @ngdoc method - * @name ng.$location#host - * @methodOf ng.$location + * @name $location#host * * @description * This method is getter only. @@ -8897,8 +9293,7 @@ LocationHashbangInHtml5Url.prototype = /** * @ngdoc method - * @name ng.$location#port - * @methodOf ng.$location + * @name $location#port * * @description * This method is getter only. @@ -8911,8 +9306,7 @@ LocationHashbangInHtml5Url.prototype = /** * @ngdoc method - * @name ng.$location#path - * @methodOf ng.$location + * @name $location#path * * @description * This method is getter / setter. @@ -8933,8 +9327,7 @@ LocationHashbangInHtml5Url.prototype = /** * @ngdoc method - * @name ng.$location#search - * @methodOf ng.$location + * @name $location#search * * @description * This method is getter / setter. @@ -8981,8 +9374,7 @@ LocationHashbangInHtml5Url.prototype = /** * @ngdoc method - * @name ng.$location#hash - * @methodOf ng.$location + * @name $location#hash * * @description * This method is getter / setter. @@ -8998,8 +9390,7 @@ LocationHashbangInHtml5Url.prototype = /** * @ngdoc method - * @name ng.$location#replace - * @methodOf ng.$location + * @name $location#replace * * @description * If called, all changes to $location during current `$digest` will be replacing current history @@ -9032,16 +9423,14 @@ function locationGetterSetter(property, preprocess) { /** - * @ngdoc object - * @name ng.$location + * @ngdoc service + * @name $location * - * @requires $browser - * @requires $sniffer * @requires $rootElement * * @description * The $location service parses the URL in the browser address bar (based on the - * {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL + * [window.location](https://developer.mozilla.org/en/window.location)) and makes the URL * available to your application. Changes to the URL in the address bar are reflected into * $location service and changes to $location are reflected into the browser address bar. * @@ -9056,13 +9445,12 @@ function locationGetterSetter(property, preprocess) { * - Clicks on a link. * - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). * - * For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular - * Services: Using $location} + * For more information see {@link guide/$location Developer Guide: Using $location} */ /** - * @ngdoc object - * @name ng.$locationProvider + * @ngdoc provider + * @name $locationProvider * @description * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ @@ -9072,8 +9460,7 @@ function $LocationProvider(){ /** * @ngdoc property - * @name ng.$locationProvider#hashPrefix - * @methodOf ng.$locationProvider + * @name $locationProvider#hashPrefix * @description * @param {string=} prefix Prefix for hash part (containing path and search) * @returns {*} current value if used as getter or itself (chaining) if used as setter @@ -9089,8 +9476,7 @@ function $LocationProvider(){ /** * @ngdoc property - * @name ng.$locationProvider#html5Mode - * @methodOf ng.$locationProvider + * @name $locationProvider#html5Mode * @description * @param {boolean=} mode Use HTML5 strategy if available. * @returns {*} current value if used as getter or itself (chaining) if used as setter @@ -9106,8 +9492,7 @@ function $LocationProvider(){ /** * @ngdoc event - * @name ng.$location#$locationChangeStart - * @eventOf ng.$location + * @name $location#$locationChangeStart * @eventType broadcast on root scope * @description * Broadcasted before a URL will change. This change can be prevented by calling @@ -9122,8 +9507,7 @@ function $LocationProvider(){ /** * @ngdoc event - * @name ng.$location#$locationChangeSuccess - * @eventOf ng.$location + * @name $location#$locationChangeSuccess * @eventType broadcast on root scope * @description * Broadcasted after a URL was changed. @@ -9244,14 +9628,14 @@ function $LocationProvider(){ } /** - * @ngdoc object - * @name ng.$log + * @ngdoc service + * @name $log * @requires $window * * @description * Simple service for logging. Default implementation safely writes the message * into the browser's console (if present). - * + * * The main purpose of this service is to simplify debugging and troubleshooting. * * The default is to log `debug` messages. You can use @@ -9280,21 +9664,20 @@ function $LocationProvider(){ */ /** - * @ngdoc object - * @name ng.$logProvider + * @ngdoc provider + * @name $logProvider * @description * Use the `$logProvider` to configure how the application logs messages */ function $LogProvider(){ var debug = true, self = this; - + /** * @ngdoc property - * @name ng.$logProvider#debugEnabled - * @methodOf ng.$logProvider + * @name $logProvider#debugEnabled * @description - * @param {string=} flag enable or disable debug level messages + * @param {boolean=} flag enable or disable debug level messages * @returns {*} current value if used as getter or itself (chaining) if used as setter */ this.debugEnabled = function(flag) { @@ -9305,13 +9688,12 @@ function $LogProvider(){ return debug; } }; - + this.$get = ['$window', function($window){ return { /** * @ngdoc method - * @name ng.$log#log - * @methodOf ng.$log + * @name $log#log * * @description * Write a log message @@ -9320,8 +9702,7 @@ function $LogProvider(){ /** * @ngdoc method - * @name ng.$log#info - * @methodOf ng.$log + * @name $log#info * * @description * Write an information message @@ -9330,8 +9711,7 @@ function $LogProvider(){ /** * @ngdoc method - * @name ng.$log#warn - * @methodOf ng.$log + * @name $log#warn * * @description * Write a warning message @@ -9340,19 +9720,17 @@ function $LogProvider(){ /** * @ngdoc method - * @name ng.$log#error - * @methodOf ng.$log + * @name $log#error * * @description * Write an error message */ error: consoleLog('error'), - + /** * @ngdoc method - * @name ng.$log#debug - * @methodOf ng.$log - * + * @name $log#debug + * * @description * Write a debug message */ @@ -9388,7 +9766,7 @@ function $LogProvider(){ // Note: reading logFn.apply throws an error in IE11 in IE8 document mode. // The reason behind this is that console.log has type "object" in IE8... try { - hasApply = !! logFn.apply; + hasApply = !!logFn.apply; } catch (e) {} if (hasApply) { @@ -9465,7 +9843,7 @@ function ensureSafeObject(obj, fullExpression) { 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', fullExpression); } else if (// isElement(obj) - obj.children && (obj.nodeName || (obj.on && obj.find))) { + obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { throw $parseMinErr('isecdom', 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', fullExpression); @@ -9803,7 +10181,11 @@ var Parser = function (lexer, $filter, options) { this.options = options; }; -Parser.ZERO = function () { return 0; }; +Parser.ZERO = extend(function () { + return 0; +}, { + constant: true +}); Parser.prototype = { constructor: Parser, @@ -10195,6 +10577,10 @@ Parser.prototype = { var allConstant = true; if (this.peekToken().text !== ']') { do { + if (this.peek(']')) { + // Support trailing commas per ES5.1. + break; + } var elementFn = this.expression(); elementFns.push(elementFn); if (!elementFn.constant) { @@ -10221,6 +10607,10 @@ Parser.prototype = { var allConstant = true; if (this.peekToken().text !== '}') { do { + if (this.peek('}')) { + // Support trailing commas per ES5.1. + break; + } var token = this.expect(), key = token.string || token.text; this.consume(':'); @@ -10493,15 +10883,15 @@ function getterFn(path, options, fullExp) { /////////////////////////////////// /** - * @ngdoc function - * @name ng.$parse - * @function + * @ngdoc service + * @name $parse + * @kind function * * @description * * Converts Angular {@link guide/expression expression} into a function. * - *
    + * ```js
      *   var getter = $parse('user.name');
      *   var setter = getter.assign;
      *   var context = {user:{name:'angular'}};
    @@ -10511,7 +10901,7 @@ function getterFn(path, options, fullExp) {
      *   setter(context, 'newValue');
      *   expect(context.user.name).toEqual('newValue');
      *   expect(getter(context, locals)).toEqual('local');
    - * 
    + * ``` * * * @param {string} expression String expression to compile. @@ -10534,8 +10924,8 @@ function getterFn(path, options, fullExp) { /** - * @ngdoc object - * @name ng.$parseProvider + * @ngdoc provider + * @name $parseProvider * @function * * @description @@ -10556,8 +10946,7 @@ function $ParseProvider() { * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. * * @ngdoc method - * @name ng.$parseProvider#unwrapPromises - * @methodOf ng.$parseProvider + * @name $parseProvider#unwrapPromises * @description * * **This feature is deprecated, see deprecation notes below for more info** @@ -10611,8 +11000,7 @@ function $ParseProvider() { * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. * * @ngdoc method - * @name ng.$parseProvider#logPromiseWarnings - * @methodOf ng.$parseProvider + * @name $parseProvider#logPromiseWarnings * @description * * Controls whether Angular should log a warning on any encounter of a promise in an expression. @@ -10679,7 +11067,7 @@ function $ParseProvider() { /** * @ngdoc service - * @name ng.$q + * @name $q * @requires $rootScope * * @description @@ -10692,10 +11080,10 @@ function $ParseProvider() { * From the perspective of dealing with error handling, deferred and promise APIs are to * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. * - *
    + * ```js
      *   // for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet`
      *   // are available in the current lexical scope (they could have been injected or passed in).
    - * 
    + *
      *   function asyncGreet(name) {
      *     var deferred = $q.defer();
      *
    @@ -10724,7 +11112,7 @@ function $ParseProvider() {
      *   }, function(update) {
      *     alert('Got notification: ' + update);
      *   });
    - * 
    + * ``` * * At first it might not be obvious why this extra complexity is worth the trouble. The payoff * comes in the way of guarantees that promise and deferred APIs make, see @@ -10750,7 +11138,7 @@ function $ParseProvider() { * constructed via `$q.reject`, the promise will be rejected instead. * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to * resolving it with a rejection constructed via `$q.reject`. - * - `notify(value)` - provides updates on the status of the promises execution. This may be called + * - `notify(value)` - provides updates on the status of the promise's execution. This may be called * multiple times before the promise is either resolved or rejected. * * **Properties** @@ -10789,21 +11177,21 @@ function $ParseProvider() { * * Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as * property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to - * make your code IE8 compatible. + * make your code IE8 and Android 2.x compatible. * * # Chaining promises * * Because calling the `then` method of a promise returns a new derived promise, it is easily * possible to create a chain of promises: * - *
    + * ```js
      *   promiseB = promiseA.then(function(result) {
      *     return result + 1;
      *   });
      *
      *   // promiseB will be resolved immediately after promiseA is resolved and its value
      *   // will be the result of promiseA incremented by 1
    - * 
    + * ``` * * It is possible to create chains of any length and since a promise can be resolved with another * promise (which will defer its resolution further), it is possible to pause/defer resolution of @@ -10823,7 +11211,7 @@ function $ParseProvider() { * * # Testing * - *
    + *  ```js
      *    it('should simulate promise', inject(function($q, $rootScope) {
      *      var deferred = $q.defer();
      *      var promise = deferred.promise;
    @@ -10843,7 +11231,7 @@ function $ParseProvider() {
      *      $rootScope.$apply();
      *      expect(resolvedValue).toEqual(123);
      *    }));
    - *  
    + * ``` */ function $QProvider() { @@ -10858,7 +11246,7 @@ function $QProvider() { /** * Constructs a promise manager. * - * @param {function(function)} nextTick Function for executing functions in the next turn. + * @param {function(Function)} nextTick Function for executing functions in the next turn. * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for * debugging purposes. * @returns {object} Promise manager. @@ -10866,9 +11254,10 @@ function $QProvider() { function qFactory(nextTick, exceptionHandler) { /** - * @ngdoc - * @name ng.$q#defer - * @methodOf ng.$q + * @ngdoc method + * @name $q#defer + * @function + * * @description * Creates a `Deferred` object which represents a task which will finish in the future. * @@ -10900,7 +11289,7 @@ function qFactory(nextTick, exceptionHandler) { reject: function(reason) { - deferred.resolve(reject(reason)); + deferred.resolve(createInternalRejectedPromise(reason)); }, @@ -11022,9 +11411,10 @@ function qFactory(nextTick, exceptionHandler) { /** - * @ngdoc - * @name ng.$q#reject - * @methodOf ng.$q + * @ngdoc method + * @name $q#reject + * @function + * * @description * Creates a promise that is resolved as rejected with the specified `reason`. This api should be * used to forward rejection in a chain of promises. If you are dealing with the last promise in @@ -11036,7 +11426,7 @@ function qFactory(nextTick, exceptionHandler) { * current promise, you have to "rethrow" the error by returning a rejection constructed via * `reject`. * - *
    +   * ```js
        *   promiseB = promiseA.then(function(result) {
        *     // success: do something and resolve promiseB
        *     //          with the old or a new result
    @@ -11051,12 +11441,18 @@ function qFactory(nextTick, exceptionHandler) {
        *     }
        *     return $q.reject(reason);
        *   });
    -   * 
    + * ``` * * @param {*} reason Constant, message, exception or an object representing the rejection reason. * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ var reject = function(reason) { + var result = defer(); + result.reject(reason); + return result.promise; + }; + + var createInternalRejectedPromise = function(reason) { return { then: function(callback, errback) { var result = defer(); @@ -11075,9 +11471,10 @@ function qFactory(nextTick, exceptionHandler) { /** - * @ngdoc - * @name ng.$q#when - * @methodOf ng.$q + * @ngdoc method + * @name $q#when + * @function + * * @description * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. * This is useful when you are dealing with an object that might or might not be a promise, or if @@ -11146,9 +11543,10 @@ function qFactory(nextTick, exceptionHandler) { /** - * @ngdoc - * @name ng.$q#all - * @methodOf ng.$q + * @ngdoc method + * @name $q#all + * @function + * * @description * Combines multiple promises into a single promise that is resolved when all of the input * promises are resolved. @@ -11191,6 +11589,38 @@ function qFactory(nextTick, exceptionHandler) { }; } +function $$RAFProvider(){ //rAF + this.$get = ['$window', '$timeout', function($window, $timeout) { + var requestAnimationFrame = $window.requestAnimationFrame || + $window.webkitRequestAnimationFrame || + $window.mozRequestAnimationFrame; + + var cancelAnimationFrame = $window.cancelAnimationFrame || + $window.webkitCancelAnimationFrame || + $window.mozCancelAnimationFrame || + $window.webkitCancelRequestAnimationFrame; + + var rafSupported = !!requestAnimationFrame; + var raf = rafSupported + ? function(fn) { + var id = requestAnimationFrame(fn); + return function() { + cancelAnimationFrame(id); + }; + } + : function(fn) { + var timer = $timeout(fn, 16.66, false); // 1000 / 60 = 16.666 + return function() { + $timeout.cancel(timer); + }; + }; + + raf.supported = rafSupported; + + return raf; + }]; +} + /** * DESIGN NOTES * @@ -11218,17 +11648,16 @@ function qFactory(nextTick, exceptionHandler) { /** - * @ngdoc object - * @name ng.$rootScopeProvider + * @ngdoc provider + * @name $rootScopeProvider * @description * * Provider for the $rootScope service. */ /** - * @ngdoc function - * @name ng.$rootScopeProvider#digestTtl - * @methodOf ng.$rootScopeProvider + * @ngdoc method + * @name $rootScopeProvider#digestTtl * @description * * Sets the number of `$digest` iterations the scope should attempt to execute before giving up and @@ -11249,8 +11678,8 @@ function qFactory(nextTick, exceptionHandler) { /** - * @ngdoc object - * @name ng.$rootScope + * @ngdoc service + * @name $rootScope * @description * * Every application has a single root {@link ng.$rootScope.Scope scope}. @@ -11275,23 +11704,23 @@ function $RootScopeProvider(){ function( $injector, $exceptionHandler, $parse, $browser) { /** - * @ngdoc function - * @name ng.$rootScope.Scope + * @ngdoc type + * @name $rootScope.Scope * * @description * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the - * {@link AUTO.$injector $injector}. Child scopes are created using the - * {@link ng.$rootScope.Scope#methods_$new $new()} method. (Most scopes are created automatically when + * {@link auto.$injector $injector}. Child scopes are created using the + * {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when * compiled HTML template is executed.) * * Here is a simple scope snippet to show how you can interact with the scope. - *
    +     * ```html
          * 
    -     * 
    + * ``` * * # Inheritance * A scope can inherit from a parent scope, as in this example: - *
    +     * ```js
              var parent = $rootScope;
              var child = parent.$new();
     
    @@ -11302,7 +11731,7 @@ function $RootScopeProvider(){
              child.salutation = "Welcome";
              expect(child.salutation).toEqual('Welcome');
              expect(parent.salutation).toEqual('Hello');
    -     * 
    + * ``` * * * @param {Object.=} providers Map of service factory which need to be @@ -11330,8 +11759,7 @@ function $RootScopeProvider(){ /** * @ngdoc property - * @name ng.$rootScope.Scope#$id - * @propertyOf ng.$rootScope.Scope + * @name $rootScope.Scope#$id * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for * debugging. */ @@ -11340,19 +11768,18 @@ function $RootScopeProvider(){ Scope.prototype = { constructor: Scope, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$new - * @methodOf ng.$rootScope.Scope + * @ngdoc method + * @name $rootScope.Scope#$new * @function * * @description * Creates a new child {@link ng.$rootScope.Scope scope}. * - * The parent scope will propagate the {@link ng.$rootScope.Scope#methods_$digest $digest()} and - * {@link ng.$rootScope.Scope#methods_$digest $digest()} events. The scope can be removed from the - * scope hierarchy using {@link ng.$rootScope.Scope#methods_$destroy $destroy()}. + * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} and + * {@link ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the + * scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. * - * {@link ng.$rootScope.Scope#methods_$destroy $destroy()} must be called on a scope when it is + * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is * desired for the scope and its child scopes to be permanently detached from the parent and * thus stop participating in model change detection and listener notification by invoking. * @@ -11398,19 +11825,18 @@ function $RootScopeProvider(){ }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$watch - * @methodOf ng.$rootScope.Scope + * @ngdoc method + * @name $rootScope.Scope#$watch * @function * * @description * Registers a `listener` callback to be executed whenever the `watchExpression` changes. * - * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#methods_$digest + * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest * $digest()} and should return the value that will be watched. (Since - * {@link ng.$rootScope.Scope#methods_$digest $digest()} reruns when it detects changes the + * {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the * `watchExpression` can execute multiple times per - * {@link ng.$rootScope.Scope#methods_$digest $digest()} and should be idempotent.) + * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) * - The `listener` is called only when the value from the current `watchExpression` and the * previous call to `watchExpression` are not equal (with the exception of the initial run, * see below). The inequality is determined according to @@ -11422,13 +11848,13 @@ function $RootScopeProvider(){ * iteration limit is 10 to prevent an infinite loop deadlock. * * - * If you want to be notified whenever {@link ng.$rootScope.Scope#methods_$digest $digest} is called, + * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, * you can register a `watchExpression` function with no `listener`. (Since `watchExpression` - * can execute multiple times per {@link ng.$rootScope.Scope#methods_$digest $digest} cycle when a + * can execute multiple times per {@link ng.$rootScope.Scope#$digest $digest} cycle when a * change is detected, be prepared for multiple calls to your listener.) * * After a watcher is registered with the scope, the `listener` fn is called asynchronously - * (via {@link ng.$rootScope.Scope#methods_$evalAsync $evalAsync}) to initialize the + * (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the * watcher. In rare cases, this is undesirable because the listener is called when the result * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the @@ -11438,7 +11864,7 @@ function $RootScopeProvider(){ * * * # Example - *
    +       * ```js
                // let's assume that scope was dependency injected as the $rootScope
                var scope = $rootScope;
                scope.name = 'misko';
    @@ -11487,12 +11913,12 @@ function $RootScopeProvider(){
                scope.$digest();
                expect(scope.foodCounter).toEqual(1);
     
    -       * 
    + * ``` * * * * @param {(function()|string)} watchExpression Expression that is evaluated on each - * {@link ng.$rootScope.Scope#methods_$digest $digest} cycle. A change in the return value triggers + * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers * a call to the `listener`. * * - `string`: Evaluated as {@link guide/expression expression} @@ -11504,7 +11930,8 @@ function $RootScopeProvider(){ * - `function(newValue, oldValue, scope)`: called with current and previous values as * parameters. * - * @param {boolean=} objectEquality Compare object for equality rather than for reference. + * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of + * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ $watch: function(watchExp, listener, objectEquality) { @@ -11550,9 +11977,8 @@ function $RootScopeProvider(){ /** - * @ngdoc function - * @name ng.$rootScope.Scope#$watchCollection - * @methodOf ng.$rootScope.Scope + * @ngdoc method + * @name $rootScope.Scope#$watchCollection * @function * * @description @@ -11567,7 +11993,7 @@ function $RootScopeProvider(){ * * * # Example - *
    +       * ```js
               $scope.names = ['igor', 'matias', 'misko', 'james'];
               $scope.dataCount = 4;
     
    @@ -11586,38 +12012,48 @@ function $RootScopeProvider(){
     
               //now there's been a change
               expect($scope.dataCount).toEqual(3);
    -       * 
    + * ``` * * - * @param {string|Function(scope)} obj Evaluated as {@link guide/expression expression}. The + * @param {string|function(scope)} obj Evaluated as {@link guide/expression expression}. The * expression value should evaluate to an object or an array which is observed on each - * {@link ng.$rootScope.Scope#methods_$digest $digest} cycle. Any shallow change within the + * {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the * collection will trigger a call to the `listener`. * - * @param {function(newCollection, oldCollection, scope)} listener a callback function that is - * fired with both the `newCollection` and `oldCollection` as parameters. - * The `newCollection` object is the newly modified data obtained from the `obj` expression - * and the `oldCollection` object is a copy of the former collection data. - * The `scope` refers to the current scope. + * @param {function(newCollection, oldCollection, scope)} listener a callback function called + * when a change is detected. + * - The `newCollection` object is the newly modified data obtained from the `obj` expression + * - The `oldCollection` object is a copy of the former collection data. + * Due to performance considerations, the`oldCollection` value is computed only if the + * `listener` function declares two or more arguments. + * - The `scope` argument refers to the current scope. * * @returns {function()} Returns a de-registration function for this listener. When the * de-registration function is executed, the internal watch operation is terminated. */ $watchCollection: function(obj, listener) { var self = this; - var oldValue; + // the current value, updated on each dirty-check run var newValue; + // a shallow copy of the newValue from the last dirty-check run, + // updated to match newValue during dirty-check run + var oldValue; + // a shallow copy of the newValue from when the last change happened + var veryOldValue; + // only track veryOldValue if the listener is asking for it + var trackVeryOldValue = (listener.length > 1); var changeDetected = 0; var objGetter = $parse(obj); var internalArray = []; var internalObject = {}; + var initRun = true; var oldLength = 0; function $watchCollectionWatch() { newValue = objGetter(self); var newLength, key; - if (!isObject(newValue)) { + if (!isObject(newValue)) { // if primitive if (oldValue !== newValue) { oldValue = newValue; changeDetected++; @@ -11639,7 +12075,9 @@ function $RootScopeProvider(){ } // copy the items to oldValue and look for changes. for (var i = 0; i < newLength; i++) { - if (oldValue[i] !== newValue[i]) { + var bothNaN = (oldValue[i] !== oldValue[i]) && + (newValue[i] !== newValue[i]); + if (!bothNaN && (oldValue[i] !== newValue[i])) { changeDetected++; oldValue[i] = newValue[i]; } @@ -11683,40 +12121,64 @@ function $RootScopeProvider(){ } function $watchCollectionAction() { - listener(newValue, oldValue, self); + if (initRun) { + initRun = false; + listener(newValue, newValue, self); + } else { + listener(newValue, veryOldValue, self); + } + + // make a copy for the next time a collection is changed + if (trackVeryOldValue) { + if (!isObject(newValue)) { + //primitive + veryOldValue = newValue; + } else if (isArrayLike(newValue)) { + veryOldValue = new Array(newValue.length); + for (var i = 0; i < newValue.length; i++) { + veryOldValue[i] = newValue[i]; + } + } else { // if object + veryOldValue = {}; + for (var key in newValue) { + if (hasOwnProperty.call(newValue, key)) { + veryOldValue[key] = newValue[key]; + } + } + } + } } return this.$watch($watchCollectionWatch, $watchCollectionAction); }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$digest - * @methodOf ng.$rootScope.Scope + * @ngdoc method + * @name $rootScope.Scope#$digest * @function * * @description - * Processes all of the {@link ng.$rootScope.Scope#methods_$watch watchers} of the current scope and - * its children. Because a {@link ng.$rootScope.Scope#methods_$watch watcher}'s listener can change - * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#methods_$watch watchers} + * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and + * its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change + * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers} * until no more listeners are firing. This means that it is possible to get into an infinite * loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of * iterations exceeds 10. * * Usually, you don't call `$digest()` directly in * {@link ng.directive:ngController controllers} or in - * {@link ng.$compileProvider#methods_directive directives}. - * Instead, you should call {@link ng.$rootScope.Scope#methods_$apply $apply()} (typically from within - * a {@link ng.$compileProvider#methods_directive directives}), which will force a `$digest()`. + * {@link ng.$compileProvider#directive directives}. + * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within + * a {@link ng.$compileProvider#directive directives}), which will force a `$digest()`. * * If you want to be notified whenever `$digest()` is called, * you can register a `watchExpression` function with - * {@link ng.$rootScope.Scope#methods_$watch $watch()} with no `listener`. + * {@link ng.$rootScope.Scope#$watch $watch()} with no `listener`. * * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. * * # Example - *
    +       * ```js
                var scope = ...;
                scope.name = 'misko';
                scope.counter = 0;
    @@ -11734,7 +12196,7 @@ function $RootScopeProvider(){
                scope.name = 'adam';
                scope.$digest();
                expect(scope.counter).toEqual(1);
    -       * 
    + * ``` * */ $digest: function() { @@ -11847,8 +12309,7 @@ function $RootScopeProvider(){ /** * @ngdoc event - * @name ng.$rootScope.Scope#$destroy - * @eventOf ng.$rootScope.Scope + * @name $rootScope.Scope#$destroy * @eventType broadcast on scope being destroyed * * @description @@ -11859,14 +12320,13 @@ function $RootScopeProvider(){ */ /** - * @ngdoc function - * @name ng.$rootScope.Scope#$destroy - * @methodOf ng.$rootScope.Scope + * @ngdoc method + * @name $rootScope.Scope#$destroy * @function * * @description * Removes the current scope (and all of its children) from the parent scope. Removal implies - * that calls to {@link ng.$rootScope.Scope#methods_$digest $digest()} will no longer + * that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer * propagate to the current scope and its children. Removal also implies that the current * scope is eligible for garbage collection. * @@ -11892,21 +12352,37 @@ function $RootScopeProvider(){ forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); + // sever all the references to parent scopes (after this cleanup, the current scope should + // not be retained by any of our references and should be eligible for garbage collection) if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; - // This is bogus code that works around Chrome's GC leak - // see: https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 + + // All of the code below is bogus code that works around V8's memory leak via optimized code + // and inline caches. + // + // see: + // - https://code.google.com/p/v8/issues/detail?id=2073#c26 + // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 + // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 + this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = - this.$$childTail = null; + this.$$childTail = this.$root = null; + + // don't reset these to null in case some async task tries to register a listener/watch/task + this.$$listeners = {}; + this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = []; + + // prevent NPEs since these methods have references to properties we nulled out + this.$destroy = this.$digest = this.$apply = noop; + this.$on = this.$watch = function() { return noop; }; }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$eval - * @methodOf ng.$rootScope.Scope + * @ngdoc method + * @name $rootScope.Scope#$eval * @function * * @description @@ -11915,14 +12391,14 @@ function $RootScopeProvider(){ * expressions. * * # Example - *
    +       * ```js
                var scope = ng.$rootScope.Scope();
                scope.a = 1;
                scope.b = 2;
     
                expect(scope.$eval('a+b')).toEqual(3);
                expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3);
    -       * 
    + * ``` * * @param {(string|function())=} expression An angular expression to be executed. * @@ -11937,9 +12413,8 @@ function $RootScopeProvider(){ }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$evalAsync - * @methodOf ng.$rootScope.Scope + * @ngdoc method + * @name $rootScope.Scope#$evalAsync * @function * * @description @@ -11950,7 +12425,7 @@ function $RootScopeProvider(){ * * - it will execute after the function that scheduled the evaluation (preferably before DOM * rendering). - * - at least one {@link ng.$rootScope.Scope#methods_$digest $digest cycle} will be performed after + * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after * `expression` execution. * * Any exceptions from the execution of the expression are forwarded to the @@ -11985,9 +12460,8 @@ function $RootScopeProvider(){ }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$apply - * @methodOf ng.$rootScope.Scope + * @ngdoc method + * @name $rootScope.Scope#$apply * @function * * @description @@ -11995,12 +12469,12 @@ function $RootScopeProvider(){ * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). * Because we are calling into the angular framework we need to perform proper scope life * cycle of {@link ng.$exceptionHandler exception handling}, - * {@link ng.$rootScope.Scope#methods_$digest executing watches}. + * {@link ng.$rootScope.Scope#$digest executing watches}. * * ## Life cycle * * # Pseudo-Code of `$apply()` - *
    +       * ```js
                function $apply(expr) {
                  try {
                    return $eval(expr);
    @@ -12010,17 +12484,17 @@ function $RootScopeProvider(){
                    $root.$digest();
                  }
                }
    -       * 
    + * ``` * * * Scope's `$apply()` method transitions through the following stages: * * 1. The {@link guide/expression expression} is executed using the - * {@link ng.$rootScope.Scope#methods_$eval $eval()} method. + * {@link ng.$rootScope.Scope#$eval $eval()} method. * 2. Any exceptions from the execution of the expression are forwarded to the * {@link ng.$exceptionHandler $exceptionHandler} service. - * 3. The {@link ng.$rootScope.Scope#methods_$watch watch} listeners are fired immediately after the - * expression was executed using the {@link ng.$rootScope.Scope#methods_$digest $digest()} method. + * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the + * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. * * * @param {(string|function())=} exp An angular expression to be executed. @@ -12048,13 +12522,12 @@ function $RootScopeProvider(){ }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$on - * @methodOf ng.$rootScope.Scope + * @ngdoc method + * @name $rootScope.Scope#$on * @function * * @description - * Listens on events of a given type. See {@link ng.$rootScope.Scope#methods_$emit $emit} for + * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for * discussion of event life cycle. * * The event listener function format is: `function(event, args...)`. The `event` object @@ -12071,7 +12544,7 @@ function $RootScopeProvider(){ * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. * * @param {string} name Event name to listen on. - * @param {function(event, args...)} listener Function to call when the event is emitted. + * @param {function(event, ...args)} listener Function to call when the event is emitted. * @returns {function()} Returns a deregistration function for this listener. */ $on: function(name, listener) { @@ -12098,27 +12571,26 @@ function $RootScopeProvider(){ /** - * @ngdoc function - * @name ng.$rootScope.Scope#$emit - * @methodOf ng.$rootScope.Scope + * @ngdoc method + * @name $rootScope.Scope#$emit * @function * * @description * Dispatches an event `name` upwards through the scope hierarchy notifying the - * registered {@link ng.$rootScope.Scope#methods_$on} listeners. + * registered {@link ng.$rootScope.Scope#$on} listeners. * * The event life cycle starts at the scope on which `$emit` was called. All - * {@link ng.$rootScope.Scope#methods_$on listeners} listening for `name` event on this scope get + * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get * notified. Afterwards, the event traverses upwards toward the root scope and calls all * registered listeners along the way. The event will stop propagating if one of the listeners * cancels it. * - * Any exception emitted from the {@link ng.$rootScope.Scope#methods_$on listeners} will be passed + * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed * onto the {@link ng.$exceptionHandler $exceptionHandler} service. * * @param {string} name Event name to emit. - * @param {...*} args Optional set of arguments which will be passed onto the event listeners. - * @return {Object} Event object (see {@link ng.$rootScope.Scope#methods_$on}). + * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. + * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). */ $emit: function(name, args) { var empty = [], @@ -12167,26 +12639,25 @@ function $RootScopeProvider(){ /** - * @ngdoc function - * @name ng.$rootScope.Scope#$broadcast - * @methodOf ng.$rootScope.Scope + * @ngdoc method + * @name $rootScope.Scope#$broadcast * @function * * @description * Dispatches an event `name` downwards to all child scopes (and their children) notifying the - * registered {@link ng.$rootScope.Scope#methods_$on} listeners. + * registered {@link ng.$rootScope.Scope#$on} listeners. * * The event life cycle starts at the scope on which `$broadcast` was called. All - * {@link ng.$rootScope.Scope#methods_$on listeners} listening for `name` event on this scope get + * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get * notified. Afterwards, the event propagates to all direct and indirect scopes of the current * scope and calls all registered listeners along the way. The event cannot be canceled. * - * Any exception emitted from the {@link ng.$rootScope.Scope#methods_$on listeners} will be passed + * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed * onto the {@link ng.$exceptionHandler $exceptionHandler} service. * * @param {string} name Event name to broadcast. - * @param {...*} args Optional set of arguments which will be passed onto the event listeners. - * @return {Object} Event object, see {@link ng.$rootScope.Scope#methods_$on} + * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. + * @return {Object} Event object, see {@link ng.$rootScope.Scope#$on} */ $broadcast: function(name, args) { var target = this, @@ -12417,7 +12888,7 @@ function adjustMatchers(matchers) { /** * @ngdoc service - * @name ng.$sceDelegate + * @name $sceDelegate * @function * * @description @@ -12437,21 +12908,21 @@ function adjustMatchers(matchers) { * can override it completely to change the behavior of `$sce`, the common case would * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as - * templates. Refer {@link ng.$sceDelegateProvider#methods_resourceUrlWhitelist + * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist * $sceDelegateProvider.resourceUrlWhitelist} and {@link - * ng.$sceDelegateProvider#methods_resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} */ /** - * @ngdoc object - * @name ng.$sceDelegateProvider + * @ngdoc provider + * @name $sceDelegateProvider * @description * * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure * that the URLs used for sourcing Angular templates are safe. Refer {@link - * ng.$sceDelegateProvider#methods_resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and - * {@link ng.$sceDelegateProvider#methods_resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and + * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} * * For the general details about this service in Angular, read the main page for {@link ng.$sce * Strict Contextual Escaping (SCE)}. @@ -12488,9 +12959,8 @@ function $SceDelegateProvider() { resourceUrlBlacklist = []; /** - * @ngdoc function - * @name ng.sceDelegateProvider#resourceUrlWhitelist - * @methodOf ng.$sceDelegateProvider + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlWhitelist * @function * * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value @@ -12518,9 +12988,8 @@ function $SceDelegateProvider() { }; /** - * @ngdoc function - * @name ng.sceDelegateProvider#resourceUrlBlacklist - * @methodOf ng.$sceDelegateProvider + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlBlacklist * @function * * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value @@ -12623,8 +13092,7 @@ function $SceDelegateProvider() { /** * @ngdoc method - * @name ng.$sceDelegate#trustAs - * @methodOf ng.$sceDelegate + * @name $sceDelegate#trustAs * * @description * Returns an object that is trusted by angular for use in specified strict @@ -12661,20 +13129,19 @@ function $SceDelegateProvider() { /** * @ngdoc method - * @name ng.$sceDelegate#valueOf - * @methodOf ng.$sceDelegate + * @name $sceDelegate#valueOf * * @description - * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#methods_trustAs + * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link - * ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`}. + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. * * If the passed parameter is not a value that had been returned by {@link - * ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`}, returns it as-is. + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is. * - * @param {*} value The result of a prior {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`} + * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} * call or anything else. - * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#methods_trustAs + * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns * `value` unchanged. */ @@ -12688,18 +13155,17 @@ function $SceDelegateProvider() { /** * @ngdoc method - * @name ng.$sceDelegate#getTrusted - * @methodOf ng.$sceDelegate + * @name $sceDelegate#getTrusted * * @description - * Takes the result of a {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`} call and + * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and * returns the originally supplied value if the queried context type is a supertype of the * created type. If this condition isn't satisfied, throws an exception. * * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#methods_trustAs + * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`} call. - * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#methods_trustAs + * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. */ function getTrusted(type, maybeTrusted) { @@ -12735,8 +13201,8 @@ function $SceDelegateProvider() { /** - * @ngdoc object - * @name ng.$sceProvider + * @ngdoc provider + * @name $sceProvider * @description * * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. @@ -12750,7 +13216,7 @@ function $SceDelegateProvider() { /** * @ngdoc service - * @name ng.$sce + * @name $sce * @function * * @description @@ -12804,20 +13270,20 @@ function $SceDelegateProvider() { * allowing only the files in a specific directory to do this. Ensuring that the internal API * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. * - * In the case of AngularJS' SCE service, one uses {@link ng.$sce#methods_trustAs $sce.trustAs} - * (and shorthand methods such as {@link ng.$sce#methods_trustAsHtml $sce.trustAsHtml}, etc.) to + * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} + * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to * obtain values that will be accepted by SCE / privileged contexts. * * * ## How does it work? * - * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#methods_getTrusted + * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link - * ng.$sce#methods_parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the - * {@link ng.$sce#methods_getTrusted $sce.getTrusted} behind the scenes on non-constant literals. + * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the + * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. * * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link - * ng.$sce#methods_parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly + * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly * simplified): * *
    @@ -12836,15 +13302,15 @@ function $SceDelegateProvider() {
      * `templateUrl`'s specified by {@link guide/directive directives}.
      *
      * By default, Angular only loads templates from the same domain and protocol as the application
    - * document.  This is done by calling {@link ng.$sce#methods_getTrustedResourceUrl
    + * document.  This is done by calling {@link ng.$sce#getTrustedResourceUrl
      * $sce.getTrustedResourceUrl} on the template URL.  To load templates from other domains and/or
    - * protocols, you may either either {@link ng.$sceDelegateProvider#methods_resourceUrlWhitelist whitelist
    - * them} or {@link ng.$sce#methods_trustAsResourceUrl wrap it} into a trusted value.
    + * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist
    + * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value.
      *
      * *Please note*:
      * The browser's
    - * {@link https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest
    - * Same Origin Policy} and {@link http://www.w3.org/TR/cors/ Cross-Origin Resource Sharing (CORS)}
    + * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest)
    + * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/)
      * policy apply in addition to this and may further restrict whether the template is successfully
      * loaded.  This means that without the right CORS policy, loading templates from a different domain
      * won't work on all browsers.  Also, loading templates from `file://` URL does not work on some
    @@ -12859,14 +13325,14 @@ function $SceDelegateProvider() {
      * `
    `) just works. * * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them - * through {@link ng.$sce#methods_getTrusted $sce.getTrusted}. SCE doesn't play a role here. + * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. * * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load * templates in `ng-include` from your application's domain without having to even know about SCE. * It blocks loading templates from other domains or loading templates over http from an https * served document. You can change these by setting your own custom {@link - * ng.$sceDelegateProvider#methods_resourceUrlWhitelist whitelists} and {@link - * ng.$sceDelegateProvider#methods_resourceUrlBlacklist blacklists} for matching such URLs. + * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. * * This significantly reduces the overhead. It is far easier to pay the small overhead and have an * application that's secure and can be audited to verify that with much more ease than bolting @@ -12879,11 +13345,11 @@ function $SceDelegateProvider() { * |---------------------|----------------| * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. | * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | - * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
    Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | + * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`

    Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | * - * ## Format of items in {@link ng.$sceDelegateProvider#methods_resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#methods_resourceUrlBlacklist Blacklist}
    + * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist} * * Each element in these arrays must be one of the following: * @@ -12895,10 +13361,10 @@ function $SceDelegateProvider() { * being tested (substring matches are not good enough.) * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters * match themselves. - * - `*`: matches zero or more occurances of any character other than one of the following 6 + * - `*`: matches zero or more occurrences of any character other than one of the following 6 * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use * in a whitelist. - * - `**`: matches zero or more occurances of *any* character. As such, it's not + * - `**`: matches zero or more occurrences of *any* character. As such, it's not * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might * not have been the intention.) It's usage at the very end of the path is ok. (e.g. @@ -12977,13 +13443,15 @@ function $SceDelegateProvider() { ] - + describe('SCE doc demo', function() { it('should sanitize untrusted values', function() { - expect(element('.htmlComment').html()).toBe('Is anyone reading this?'); + expect(element(by.css('.htmlComment')).getInnerHtml()) + .toBe('Is anyone reading this?'); }); + it('should NOT sanitize explicitly trusted values', function() { - expect(element('#explicitlyTrustedHtml').html()).toBe( + expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( 'Hover over this text.'); }); @@ -13018,9 +13486,8 @@ function $SceProvider() { var enabled = true; /** - * @ngdoc function - * @name ng.sceProvider#enabled - * @methodOf ng.$sceProvider + * @ngdoc method + * @name $sceProvider#enabled * @function * * @param {boolean=} value If provided, then enables/disables SCE. @@ -13097,9 +13564,8 @@ function $SceProvider() { var sce = copy(SCE_CONTEXTS); /** - * @ngdoc function - * @name ng.sce#isEnabled - * @methodOf ng.$sce + * @ngdoc method + * @name $sce#isEnabled * @function * * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you @@ -13122,13 +13588,12 @@ function $SceProvider() { /** * @ngdoc method - * @name ng.$sce#parse - * @methodOf ng.$sce + * @name $sce#parse * * @description * Converts Angular {@link guide/expression expression} into a function. This is like {@link * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it - * wraps the expression in a call to {@link ng.$sce#methods_getTrusted $sce.getTrusted(*type*, + * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, * *result*)} * * @param {string} type The kind of SCE context in which this result will be used. @@ -13153,11 +13618,10 @@ function $SceProvider() { /** * @ngdoc method - * @name ng.$sce#trustAs - * @methodOf ng.$sce + * @name $sce#trustAs * * @description - * Delegates to {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`}. As such, + * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, * returns an object that is trusted by angular for use in specified strict contextual * escaping contexts (such as ng-bind-html, ng-include, any src attribute * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) @@ -13173,95 +13637,89 @@ function $SceProvider() { /** * @ngdoc method - * @name ng.$sce#trustAsHtml - * @methodOf ng.$sce + * @name $sce#trustAsHtml * * @description * Shorthand method. `$sce.trustAsHtml(value)` → - * {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs($sce.HTML, value)`} + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} * * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#methods_getTrustedHtml + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#methods_trustAs $sce.trustAs}.) + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) */ /** * @ngdoc method - * @name ng.$sce#trustAsUrl - * @methodOf ng.$sce + * @name $sce#trustAsUrl * * @description * Shorthand method. `$sce.trustAsUrl(value)` → - * {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs($sce.URL, value)`} + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} * * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#methods_getTrustedUrl + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#methods_trustAs $sce.trustAs}.) + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) */ /** * @ngdoc method - * @name ng.$sce#trustAsResourceUrl - * @methodOf ng.$sce + * @name $sce#trustAsResourceUrl * * @description * Shorthand method. `$sce.trustAsResourceUrl(value)` → - * {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} * * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#methods_getTrustedResourceUrl + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives * only accept expressions that are either literal constants or are the return - * value of {@link ng.$sce#methods_trustAs $sce.trustAs}.) + * value of {@link ng.$sce#trustAs $sce.trustAs}.) */ /** * @ngdoc method - * @name ng.$sce#trustAsJs - * @methodOf ng.$sce + * @name $sce#trustAsJs * * @description * Shorthand method. `$sce.trustAsJs(value)` → - * {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs($sce.JS, value)`} + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} * * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#methods_getTrustedJs + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#methods_trustAs $sce.trustAs}.) + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) */ /** * @ngdoc method - * @name ng.$sce#getTrusted - * @methodOf ng.$sce + * @name $sce#getTrusted * * @description - * Delegates to {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted`}. As such, - * takes the result of a {@link ng.$sce#methods_trustAs `$sce.trustAs`}() call and returns the + * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, + * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the * originally supplied value if the queried context type is a supertype of the created type. * If this condition isn't satisfied, throws an exception. * * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sce#methods_trustAs `$sce.trustAs`} + * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} * call. * @returns {*} The value the was originally provided to - * {@link ng.$sce#methods_trustAs `$sce.trustAs`} if valid in this context. + * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context. * Otherwise, throws an exception. */ /** * @ngdoc method - * @name ng.$sce#getTrustedHtml - * @methodOf ng.$sce + * @name $sce#getTrustedHtml * * @description * Shorthand method. `$sce.getTrustedHtml(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` @@ -13269,12 +13727,11 @@ function $SceProvider() { /** * @ngdoc method - * @name ng.$sce#getTrustedCss - * @methodOf ng.$sce + * @name $sce#getTrustedCss * * @description * Shorthand method. `$sce.getTrustedCss(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` @@ -13282,12 +13739,11 @@ function $SceProvider() { /** * @ngdoc method - * @name ng.$sce#getTrustedUrl - * @methodOf ng.$sce + * @name $sce#getTrustedUrl * * @description * Shorthand method. `$sce.getTrustedUrl(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` @@ -13295,12 +13751,11 @@ function $SceProvider() { /** * @ngdoc method - * @name ng.$sce#getTrustedResourceUrl - * @methodOf ng.$sce + * @name $sce#getTrustedResourceUrl * * @description * Shorthand method. `$sce.getTrustedResourceUrl(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} * * @param {*} value The value to pass to `$sceDelegate.getTrusted`. * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` @@ -13308,12 +13763,11 @@ function $SceProvider() { /** * @ngdoc method - * @name ng.$sce#getTrustedJs - * @methodOf ng.$sce + * @name $sce#getTrustedJs * * @description * Shorthand method. `$sce.getTrustedJs(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` @@ -13321,12 +13775,11 @@ function $SceProvider() { /** * @ngdoc method - * @name ng.$sce#parseAsHtml - * @methodOf ng.$sce + * @name $sce#parseAsHtml * * @description * Shorthand method. `$sce.parseAsHtml(expression string)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.HTML, value)`} + * {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13339,12 +13792,11 @@ function $SceProvider() { /** * @ngdoc method - * @name ng.$sce#parseAsCss - * @methodOf ng.$sce + * @name $sce#parseAsCss * * @description * Shorthand method. `$sce.parseAsCss(value)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.CSS, value)`} + * {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13357,12 +13809,11 @@ function $SceProvider() { /** * @ngdoc method - * @name ng.$sce#parseAsUrl - * @methodOf ng.$sce + * @name $sce#parseAsUrl * * @description * Shorthand method. `$sce.parseAsUrl(value)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.URL, value)`} + * {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13375,12 +13826,11 @@ function $SceProvider() { /** * @ngdoc method - * @name ng.$sce#parseAsResourceUrl - * @methodOf ng.$sce + * @name $sce#parseAsResourceUrl * * @description * Shorthand method. `$sce.parseAsResourceUrl(value)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.RESOURCE_URL, value)`} + * {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13393,12 +13843,11 @@ function $SceProvider() { /** * @ngdoc method - * @name ng.$sce#parseAsJs - * @methodOf ng.$sce + * @name $sce#parseAsJs * * @description * Shorthand method. `$sce.parseAsJs(value)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.JS, value)`} + * {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13434,7 +13883,7 @@ function $SceProvider() { /** * !!! This is an undocumented "private" service !!! * - * @name ng.$sniffer + * @name $sniffer * @requires $window * @requires $document * @@ -13530,9 +13979,8 @@ function $TimeoutProvider() { /** - * @ngdoc function - * @name ng.$timeout - * @requires $browser + * @ngdoc service + * @name $timeout * * @description * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch @@ -13550,10 +13998,10 @@ function $TimeoutProvider() { * @param {function()} fn A function, whose execution should be delayed. * @param {number=} [delay=0] Delay in milliseconds. * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#methods_$apply $apply} block. + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this * promise will be resolved with is the return value of the `fn` function. - * + * */ function timeout(fn, delay, invokeApply) { var deferred = $q.defer(), @@ -13583,9 +14031,8 @@ function $TimeoutProvider() { /** - * @ngdoc function - * @name ng.$timeout#cancel - * @methodOf ng.$timeout + * @ngdoc method + * @name $timeout#cancel * * @description * Cancels a task associated with the `promise`. As a result of this, the promise will be @@ -13712,8 +14159,8 @@ function urlIsSameOrigin(requestUrl) { } /** - * @ngdoc object - * @name ng.$window + * @ngdoc service + * @name $window * * @description * A reference to the browser's `window` object. While `window` @@ -13727,8 +14174,8 @@ function urlIsSameOrigin(requestUrl) { * expression. * * @example - - + +

    - default currency symbol ($): {{amount | currency}}
    - custom currency identifier (USD$): {{amount | currency:"USD$"}} + default currency symbol ($): {{amount | currency}}
    + custom currency identifier (USD$): {{amount | currency:"USD$"}}
    -
    - +
    + it('should init with 1234.56', function() { - expect(binding('amount | currency')).toBe('$1,234.56'); - expect(binding('amount | currency:"USD$"')).toBe('USD$1,234.56'); + expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); + expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('USD$1,234.56'); }); it('should update', function() { - input('amount').enter('-1234'); - expect(binding('amount | currency')).toBe('($1,234.00)'); - expect(binding('amount | currency:"USD$"')).toBe('(USD$1,234.00)'); + if (browser.params.browser == 'safari') { + // Safari does not understand the minus key. See + // https://github.com/angular/protractor/issues/481 + return; + } + element(by.model('amount')).clear(); + element(by.model('amount')).sendKeys('-1234'); + expect(element(by.id('currency-default')).getText()).toBe('($1,234.00)'); + expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('(USD$1,234.00)'); }); - - + + */ currencyFilter.$inject = ['$locale']; function currencyFilter($locale) { @@ -14138,7 +14610,7 @@ function currencyFilter($locale) { /** * @ngdoc filter - * @name ng.filter:number + * @name number * @function * * @description @@ -14153,8 +14625,8 @@ function currencyFilter($locale) { * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. * * @example - - + +
    Enter number:
    - Default formatting: {{val | number}}
    - No fractions: {{val | number:0}}
    - Negative number: {{-val | number:4}} + Default formatting: {{val | number}}
    + No fractions: {{val | number:0}}
    + Negative number: {{-val | number:4}}
    -
    - +
    + it('should format numbers', function() { - expect(binding('val | number')).toBe('1,234.568'); - expect(binding('val | number:0')).toBe('1,235'); - expect(binding('-val | number:4')).toBe('-1,234.5679'); + expect(element(by.id('number-default')).getText()).toBe('1,234.568'); + expect(element(by.binding('val | number:0')).getText()).toBe('1,235'); + expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679'); }); it('should update', function() { - input('val').enter('3374.333'); - expect(binding('val | number')).toBe('3,374.333'); - expect(binding('val | number:0')).toBe('3,374'); - expect(binding('-val | number:4')).toBe('-3,374.3330'); - }); - - + element(by.model('val')).clear(); + element(by.model('val')).sendKeys('3374.333'); + expect(element(by.id('number-default')).getText()).toBe('3,374.333'); + expect(element(by.binding('val | number:0')).getText()).toBe('3,374'); + expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330'); + }); + + */ @@ -14196,7 +14669,7 @@ function numberFilter($locale) { var DECIMAL_SEP = '.'; function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (isNaN(number) || !isFinite(number)) return ''; + if (number == null || !isFinite(number) || isObject(number)) return ''; var isNegative = number < 0; number = Math.abs(number); @@ -14349,7 +14822,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ /** * @ngdoc filter - * @name ng.filter:date + * @name date * @function * * @description @@ -14407,26 +14880,26 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * @returns {string} Formatted string or the input if input is not recognized as date/millis. * * @example - - + + {{1288323623006 | date:'medium'}}: - {{1288323623006 | date:'medium'}}
    + {{1288323623006 | date:'medium'}}
    {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: - {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
    + {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
    {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: - {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
    -
    - + {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
    + + it('should format date', function() { - expect(binding("1288323623006 | date:'medium'")). + expect(element(by.binding("1288323623006 | date:'medium'")).getText()). toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); - expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")). + expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); - expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")). + expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); }); -
    -
    + + */ dateFilter.$inject = ['$locale']; function dateFilter($locale) { @@ -14506,7 +14979,7 @@ function dateFilter($locale) { /** * @ngdoc filter - * @name ng.filter:json + * @name json * @function * * @description @@ -14519,17 +14992,17 @@ function dateFilter($locale) { * @returns {string} JSON string. * * - * @example: - - + * @example + +
    {{ {'name':'value'} | json }}
    -
    - + + it('should jsonify filtered objects', function() { - expect(binding("{'name':'value'}")).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/); }); - -
    + + * */ function jsonFilter() { @@ -14541,7 +15014,7 @@ function jsonFilter() { /** * @ngdoc filter - * @name ng.filter:lowercase + * @name lowercase * @function * @description * Converts string to lowercase. @@ -14552,7 +15025,7 @@ var lowercaseFilter = valueFn(lowercase); /** * @ngdoc filter - * @name ng.filter:uppercase + * @name uppercase * @function * @description * Converts string to uppercase. @@ -14561,8 +15034,8 @@ var lowercaseFilter = valueFn(lowercase); var uppercaseFilter = valueFn(uppercase); /** - * @ngdoc function - * @name ng.filter:limitTo + * @ngdoc filter + * @name limitTo * @function * * @description @@ -14571,16 +15044,16 @@ var uppercaseFilter = valueFn(uppercase); * the value and sign (positive or negative) of `limit`. * * @param {Array|string} input Source array or string to be limited. - * @param {string|number} limit The length of the returned array or string. If the `limit` number + * @param {string|number} limit The length of the returned array or string. If the `limit` number * is positive, `limit` number of items from the beginning of the source array/string are copied. - * If the number is negative, `limit` number of items from the end of the source array/string + * If the number is negative, `limit` number of items from the end of the source array/string * are copied. The `limit` will be trimmed if it exceeds `array.length` * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array * had less than `limit` elements. * * @example - - + + - + + userType: Required!
    userType = {{userType}}
    @@ -15527,20 +16054,30 @@ function FormController(element, attrs) { myForm.$valid = {{myForm.$valid}}
    myForm.$error.required = {{!!myForm.$error.required}}
    -
    - + + it('should initialize to model', function() { - expect(binding('userType')).toEqual('guest'); - expect(binding('myForm.input.$valid')).toEqual('true'); + var userType = element(by.binding('userType')); + var valid = element(by.binding('myForm.input.$valid')); + + expect(userType.getText()).toContain('guest'); + expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { - input('userType').enter(''); - expect(binding('userType')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + var userType = element(by.binding('userType')); + var valid = element(by.binding('myForm.input.$valid')); + var userInput = element(by.model('userType')); + + userInput.clear(); + userInput.sendKeys(''); + + expect(userType.getText()).toEqual('userType ='); + expect(valid.getText()).toContain('false'); }); - -
    + + + * */ var formDirectiveFactory = function(isNgForm) { return ['$timeout', function($timeout) { @@ -15617,8 +16154,8 @@ var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; var inputType = { /** - * @ngdoc inputType - * @name ng.directive:input.text + * @ngdoc input + * @name input[text] * * @description * Standard HTML text input with angular data binding. @@ -15641,8 +16178,8 @@ var inputType = { * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. * * @example - - + + + + Update input to see transitions when valid/invalid. + Integer is a valid value. +
    + +
    +
    + *
    */ var ngModelDirective = function() { return { @@ -16794,7 +17472,7 @@ var ngModelDirective = function() { /** * @ngdoc directive - * @name ng.directive:ngChange + * @name ngChange * * @description * Evaluate the given expression when the user changes the input. @@ -16810,8 +17488,8 @@ var ngModelDirective = function() { * in input value. * * @example - * - * + * + * * Load inlined template
    -
    - + + it('should load template defined inside script tag', function() { - element('#tpl-link').click(); - expect(element('#tpl-content').text()).toMatch(/Content of the template/); + element(by.css('#tpl-link')).click(); + expect(element(by.css('#tpl-content')).getText()).toMatch(/Content of the template/); }); - -
    + + */ var scriptDirective = ['$templateCache', function($templateCache) { return { @@ -19949,7 +20805,7 @@ var scriptDirective = ['$templateCache', function($templateCache) { var ngOptionsMinErr = minErr('ngOptions'); /** * @ngdoc directive - * @name ng.directive:select + * @name select * @restrict E * * @description @@ -19967,7 +20823,7 @@ var ngOptionsMinErr = minErr('ngOptions'); * *
    * **Note:** `ngModel` compares by reference, not value. This is important when binding to an - * array of objects. See an example {@link http://jsfiddle.net/qWzTb/ in this jsfiddle}. + * array of objects. See an example [in this jsfiddle](http://jsfiddle.net/qWzTb/). *
    * * Optionally, a single hard-coded `
    + * ``` + * angular.module('myApp', []).config(function($sceDelegateProvider) { + * $sceDelegateProvider.resourceUrlWhitelist([ + * // Allow same origin resource loads. + * 'self', + * // Allow loading from our assets domain. Notice the difference between * and **. + * 'http://srv*.assets.example.com/**' + * ]); + * + * // The blacklist overrides the whitelist so the open redirect here is blocked. + * $sceDelegateProvider.resourceUrlBlacklist([ + * 'http://myapp.example.com/clickThru**' + * ]); + * }); + * ``` */ function $SceDelegateProvider() { @@ -12961,7 +15000,7 @@ function $SceDelegateProvider() { /** * @ngdoc method * @name $sceDelegateProvider#resourceUrlWhitelist - * @function + * @kind function * * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value * provided. This must be an array or null. A snapshot of this array is used so further @@ -12980,7 +15019,7 @@ function $SceDelegateProvider() { * @description * Sets/Gets the whitelist of trusted resource URLs. */ - this.resourceUrlWhitelist = function (value) { + this.resourceUrlWhitelist = function(value) { if (arguments.length) { resourceUrlWhitelist = adjustMatchers(value); } @@ -12990,7 +15029,7 @@ function $SceDelegateProvider() { /** * @ngdoc method * @name $sceDelegateProvider#resourceUrlBlacklist - * @function + * @kind function * * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value * provided. This must be an array or null. A snapshot of this array is used so further @@ -13014,7 +15053,7 @@ function $SceDelegateProvider() { * Sets/Gets the blacklist of trusted resource URLs. */ - this.resourceUrlBlacklist = function (value) { + this.resourceUrlBlacklist = function(value) { if (arguments.length) { resourceUrlBlacklist = adjustMatchers(value); } @@ -13217,7 +15256,7 @@ function $SceDelegateProvider() { /** * @ngdoc service * @name $sce - * @function + * @kind function * * @description * @@ -13232,7 +15271,7 @@ function $SceDelegateProvider() { * * As of version 1.2, Angular ships with SCE enabled by default. * - * Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows + * Note: When enabled (the default), IE<11 in quirks mode is not supported. In this mode, IE<11 allow * one to execute arbitrary javascript by the use of the expression() syntax. Refer * to learn more about them. * You can ensure your document is in standards mode and not quirks mode by adding `` @@ -13243,10 +15282,10 @@ function $SceDelegateProvider() { * * Here's an example of a binding in a privileged context: * - *
    - *     
    - *     
    - *
    + * ``` + * + *
    + * ``` * * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE * disabled, this application allows the user to render arbitrary HTML into the DIV. @@ -13279,22 +15318,22 @@ function $SceDelegateProvider() { * * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link - * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the + * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. * * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly * simplified): * - *
    - *   var ngBindHtmlDirective = ['$sce', function($sce) {
    - *     return function(scope, element, attr) {
    - *       scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) {
    - *         element.html(value || '');
    - *       });
    - *     };
    - *   }];
    - * 
    + * ``` + * var ngBindHtmlDirective = ['$sce', function($sce) { + * return function(scope, element, attr) { + * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { + * element.html(value || ''); + * }); + * }; + * }]; + * ``` * * ## Impact on loading templates * @@ -13316,7 +15355,7 @@ function $SceDelegateProvider() { * won't work on all browsers. Also, loading templates from `file://` URL does not work on some * browsers. * - * ## This feels like too much overhead for the developer? + * ## This feels like too much overhead * * It's important to remember that SCE only applies to interpolation expressions. * @@ -13343,7 +15382,7 @@ function $SceDelegateProvider() { * * | Context | Notes | * |---------------------|----------------| - * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. | + * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
    Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | @@ -13367,7 +15406,7 @@ function $SceDelegateProvider() { * - `**`: matches zero or more occurrences of *any* character. As such, it's not * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might - * not have been the intention.) It's usage at the very end of the path is ok. (e.g. + * not have been the intention.) Its usage at the very end of the path is ok. (e.g. * http://foo.example.com/templates/**). * - **RegExp** (*see caveat below*) * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax @@ -13398,86 +15437,85 @@ function $SceDelegateProvider() { * * ## Show me an example using SCE. * - * @example - - -
    -

    - User comments
    - By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when - $sanitize is available. If $sanitize isn't available, this results in an error instead of an - exploit. -
    -
    - {{userComment.name}}: - -
    -
    -
    -
    -
    - - - var mySceApp = angular.module('mySceApp', ['ngSanitize']); - - mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) { - var self = this; - $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - self.userComments = userComments; - }); - self.explicitlyTrustedHtml = $sce.trustAsHtml( - 'Hover over this text.'); - }); - - - -[ - { "name": "Alice", - "htmlComment": - "Is anyone reading this?" - }, - { "name": "Bob", - "htmlComment": "Yes! Am I the only other one?" - } -] - - - - describe('SCE doc demo', function() { - it('should sanitize untrusted values', function() { - expect(element(by.css('.htmlComment')).getInnerHtml()) - .toBe('Is anyone reading this?'); - }); - - it('should NOT sanitize explicitly trusted values', function() { - expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( - 'Hover over this text.'); - }); - }); - -
    + * + * + *
    + *

    + * User comments
    + * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when + * $sanitize is available. If $sanitize isn't available, this results in an error instead of an + * exploit. + *
    + *
    + * {{userComment.name}}: + * + *
    + *
    + *
    + *
    + *
    * + * + * angular.module('mySceApp', ['ngSanitize']) + * .controller('AppController', ['$http', '$templateCache', '$sce', + * function($http, $templateCache, $sce) { + * var self = this; + * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { + * self.userComments = userComments; + * }); + * self.explicitlyTrustedHtml = $sce.trustAsHtml( + * 'Hover over this text.'); + * }]); + * * + * + * [ + * { "name": "Alice", + * "htmlComment": + * "Is anyone reading this?" + * }, + * { "name": "Bob", + * "htmlComment": "Yes! Am I the only other one?" + * } + * ] + * * - * ## Can I disable SCE completely? + * + * describe('SCE doc demo', function() { + * it('should sanitize untrusted values', function() { + * expect(element.all(by.css('.htmlComment')).first().getInnerHtml()) + * .toBe('Is anyone reading this?'); + * }); * - * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits - * for little coding overhead. It will be much harder to take an SCE disabled application and - * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE - * for cases where you have a lot of existing code that was written before SCE was introduced and - * you're migrating them a module at a time. + * it('should NOT sanitize explicitly trusted values', function() { + * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( + * 'Hover over this text.'); + * }); + * }); + * + *
    + * + * + * + * ## Can I disable SCE completely? + * + * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits + * for little coding overhead. It will be much harder to take an SCE disabled application and + * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE + * for cases where you have a lot of existing code that was written before SCE was introduced and + * you're migrating them a module at a time. * * That said, here's how you can completely disable SCE: * - *
    - *   angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
    - *     // Completely disable SCE.  For demonstration purposes only!
    - *     // Do not use in new projects.
    - *     $sceProvider.enabled(false);
    - *   });
    - * 
    + * ``` + * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { + * // Completely disable SCE. For demonstration purposes only! + * // Do not use in new projects. + * $sceProvider.enabled(false); + * }); + * ``` * */ /* jshint maxlen: 100 */ @@ -13488,7 +15526,7 @@ function $SceProvider() { /** * @ngdoc method * @name $sceProvider#enabled - * @function + * @kind function * * @param {boolean=} value If provided, then enables/disables SCE. * @return {boolean} true if SCE is enabled, false otherwise. @@ -13496,7 +15534,7 @@ function $SceProvider() { * @description * Enables/disables SCE and returns the current value. */ - this.enabled = function (value) { + this.enabled = function(value) { if (arguments.length) { enabled = !!value; } @@ -13550,23 +15588,23 @@ function $SceProvider() { * sce.js and sceSpecs.js would need to be aware of this detail. */ - this.$get = ['$parse', '$sniffer', '$sceDelegate', function( - $parse, $sniffer, $sceDelegate) { - // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows + this.$get = ['$parse', '$sceDelegate', function( + $parse, $sceDelegate) { + // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow // the "expression(javascript expression)" syntax which is insecure. - if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) { + if (enabled && msie < 8) { throw $sceMinErr('iequirks', - 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' + + 'Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks ' + 'mode. You can fix this by adding the text to the top of your HTML ' + 'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); } - var sce = copy(SCE_CONTEXTS); + var sce = shallowCopy(SCE_CONTEXTS); /** * @ngdoc method * @name $sce#isEnabled - * @function + * @kind function * * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. @@ -13574,7 +15612,7 @@ function $SceProvider() { * @description * Returns a boolean indicating if SCE is enabled. */ - sce.isEnabled = function () { + sce.isEnabled = function() { return enabled; }; sce.trustAs = $sceDelegate.trustAs; @@ -13588,7 +15626,7 @@ function $SceProvider() { /** * @ngdoc method - * @name $sce#parse + * @name $sce#parseAs * * @description * Converts Angular {@link guide/expression expression} into a function. This is like {@link @@ -13610,9 +15648,9 @@ function $SceProvider() { if (parsed.literal && parsed.constant) { return parsed; } else { - return function sceParseAsTrusted(self, locals) { - return sce.getTrusted(type, parsed(self, locals)); - }; + return $parse(expr, function(value) { + return sce.getTrusted(type, value); + }); } }; @@ -13779,7 +15817,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsHtml(expression string)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13796,7 +15834,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsCss(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13813,7 +15851,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsUrl(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13830,7 +15868,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsResourceUrl(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13847,7 +15885,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsJs(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13863,15 +15901,15 @@ function $SceProvider() { getTrusted = sce.getTrusted, trustAs = sce.trustAs; - forEach(SCE_CONTEXTS, function (enumValue, name) { + forEach(SCE_CONTEXTS, function(enumValue, name) { var lName = lowercase(name); - sce[camelCase("parse_as_" + lName)] = function (expr) { + sce[camelCase("parse_as_" + lName)] = function(expr) { return parse(enumValue, expr); }; - sce[camelCase("get_trusted_" + lName)] = function (value) { + sce[camelCase("get_trusted_" + lName)] = function(value) { return getTrusted(enumValue, value); }; - sce[camelCase("trust_as_" + lName)] = function (value) { + sce[camelCase("trust_as_" + lName)] = function(value) { return trustAs(enumValue, value); }; }); @@ -13888,7 +15926,6 @@ function $SceProvider() { * @requires $document * * @property {boolean} history Does the browser support html5 history api ? - * @property {boolean} hashchange Does the browser support hashchange event ? * @property {boolean} transitions Does the browser support CSS transition events ? * @property {boolean} animations Does the browser support CSS animation events ? * @@ -13902,31 +15939,30 @@ function $SnifferProvider() { int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), document = $document[0] || {}, - documentMode = document.documentMode, vendorPrefix, - vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, + vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/, bodyStyle = document.body && document.body.style, transitions = false, animations = false, match; if (bodyStyle) { - for(var prop in bodyStyle) { - if(match = vendorRegex.exec(prop)) { + for (var prop in bodyStyle) { + if (match = vendorRegex.exec(prop)) { vendorPrefix = match[0]; vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); break; } } - if(!vendorPrefix) { + if (!vendorPrefix) { vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; } transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); - if (android && (!transitions||!animations)) { + if (android && (!transitions || !animations)) { transitions = isString(document.body.style.webkitTransition); animations = isString(document.body.style.webkitAnimation); } @@ -13945,14 +15981,13 @@ function $SnifferProvider() { // jshint -W018 history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), // jshint +W018 - hashchange: 'onhashchange' in $window && - // IE8 compatible mode lies - (!documentMode || documentMode > 7), hasEvent: function(event) { // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have // it. In particular the event is not fired when backspace or delete key are pressed or // when cut operation is performed. - if (event == 'input' && msie == 9) return false; + // IE10+ implements 'input' event but it erroneously fires under various situations, + // e.g. when placeholder changes, or a form is focused. + if (event === 'input' && msie <= 11) return false; if (isUndefined(eventSupport[event])) { var divElm = document.createElement('div'); @@ -13963,18 +15998,192 @@ function $SnifferProvider() { }, csp: csp(), vendorPrefix: vendorPrefix, - transitions : transitions, - animations : animations, - android: android, - msie : msie, - msieDocumentMode: documentMode + transitions: transitions, + animations: animations, + android: android + }; + }]; +} + +var $compileMinErr = minErr('$compile'); + +/** + * @ngdoc service + * @name $templateRequest + * + * @description + * The `$templateRequest` service downloads the provided template using `$http` and, upon success, + * stores the contents inside of `$templateCache`. If the HTTP request fails or the response data + * of the HTTP request is empty, a `$compile` error will be thrown (the exception can be thwarted + * by setting the 2nd parameter of the function to true). + * + * @param {string} tpl The HTTP request template URL + * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty + * + * @return {Promise} the HTTP Promise for the given. + * + * @property {number} totalPendingRequests total amount of pending template requests being downloaded. + */ +function $TemplateRequestProvider() { + this.$get = ['$templateCache', '$http', '$q', function($templateCache, $http, $q) { + function handleRequestFn(tpl, ignoreRequestError) { + handleRequestFn.totalPendingRequests++; + + var transformResponse = $http.defaults && $http.defaults.transformResponse; + + if (isArray(transformResponse)) { + transformResponse = transformResponse.filter(function(transformer) { + return transformer !== defaultHttpResponseTransform; + }); + } else if (transformResponse === defaultHttpResponseTransform) { + transformResponse = null; + } + + var httpOptions = { + cache: $templateCache, + transformResponse: transformResponse + }; + + return $http.get(tpl, httpOptions) + .finally(function() { + handleRequestFn.totalPendingRequests--; + }) + .then(function(response) { + return response.data; + }, handleError); + + function handleError(resp) { + if (!ignoreRequestError) { + throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl); + } + return $q.reject(resp); + } + } + + handleRequestFn.totalPendingRequests = 0; + + return handleRequestFn; + }]; +} + +function $$TestabilityProvider() { + this.$get = ['$rootScope', '$browser', '$location', + function($rootScope, $browser, $location) { + + /** + * @name $testability + * + * @description + * The private $$testability service provides a collection of methods for use when debugging + * or by automated test and debugging tools. + */ + var testability = {}; + + /** + * @name $$testability#findBindings + * + * @description + * Returns an array of elements that are bound (via ng-bind or {{}}) + * to expressions matching the input. + * + * @param {Element} element The element root to search from. + * @param {string} expression The binding expression to match. + * @param {boolean} opt_exactMatch If true, only returns exact matches + * for the expression. Filters and whitespace are ignored. + */ + testability.findBindings = function(element, expression, opt_exactMatch) { + var bindings = element.getElementsByClassName('ng-binding'); + var matches = []; + forEach(bindings, function(binding) { + var dataBinding = angular.element(binding).data('$binding'); + if (dataBinding) { + forEach(dataBinding, function(bindingName) { + if (opt_exactMatch) { + var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)'); + if (matcher.test(bindingName)) { + matches.push(binding); + } + } else { + if (bindingName.indexOf(expression) != -1) { + matches.push(binding); + } + } + }); + } + }); + return matches; + }; + + /** + * @name $$testability#findModels + * + * @description + * Returns an array of elements that are two-way found via ng-model to + * expressions matching the input. + * + * @param {Element} element The element root to search from. + * @param {string} expression The model expression to match. + * @param {boolean} opt_exactMatch If true, only returns exact matches + * for the expression. + */ + testability.findModels = function(element, expression, opt_exactMatch) { + var prefixes = ['ng-', 'data-ng-', 'ng\\:']; + for (var p = 0; p < prefixes.length; ++p) { + var attributeEquals = opt_exactMatch ? '=' : '*='; + var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]'; + var elements = element.querySelectorAll(selector); + if (elements.length) { + return elements; + } + } + }; + + /** + * @name $$testability#getLocation + * + * @description + * Shortcut for getting the location in a browser agnostic way. Returns + * the path, search, and hash. (e.g. /path?a=b#hash) + */ + testability.getLocation = function() { + return $location.url(); + }; + + /** + * @name $$testability#setLocation + * + * @description + * Shortcut for navigating to a location without doing a full page reload. + * + * @param {string} url The location url (path, search and hash, + * e.g. /path?a=b#hash) to go to. + */ + testability.setLocation = function(url) { + if (url !== $location.url()) { + $location./service/https://github.com/url(url); + $rootScope.$digest(); + } + }; + + /** + * @name $$testability#whenStable + * + * @description + * Calls the callback when $timeout and $http requests are completed. + * + * @param {function} callback + */ + testability.whenStable = function(callback) { + $browser.notifyWhenNoOutstandingRequests(callback); }; + + return testability; }]; } function $TimeoutProvider() { - this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler', - function($rootScope, $browser, $q, $exceptionHandler) { + this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', + function($rootScope, $browser, $q, $$q, $exceptionHandler) { var deferreds = {}; @@ -14004,15 +16213,15 @@ function $TimeoutProvider() { * */ function timeout(fn, delay, invokeApply) { - var deferred = $q.defer(), + var skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), promise = deferred.promise, - skipApply = (isDefined(invokeApply) && !invokeApply), timeoutId; timeoutId = $browser.defer(function() { try { deferred.resolve(fn()); - } catch(e) { + } catch (e) { deferred.reject(e); $exceptionHandler(e); } @@ -14063,7 +16272,7 @@ function $TimeoutProvider() { // exactly the behavior needed here. There is little value is mocking these out for this // service. var urlParsingNode = document.createElement("a"); -var originUrl = urlResolve(window.location.href, true); +var originUrl = urlResolve(window.location.href); /** @@ -14101,7 +16310,7 @@ var originUrl = urlResolve(window.location.href, true); * https://github.com/angular/angular.js/pull/2902 * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ * - * @function + * @kind function * @param {string} url The URL to be parsed. * @description Normalizes and parses a URL. * @returns {object} Returns the normalized URL as a dictionary. @@ -14118,7 +16327,7 @@ var originUrl = urlResolve(window.location.href, true); * | pathname | The pathname, beginning with "/" * */ -function urlResolve(url, base) { +function urlResolve(url) { var href = url; if (msie) { @@ -14174,17 +16383,18 @@ function urlIsSameOrigin(requestUrl) { * expression. * * @example - + -
    +
    @@ -14198,10 +16408,21 @@ function urlIsSameOrigin(requestUrl) { */ -function $WindowProvider(){ +function $WindowProvider() { this.$get = valueFn(window); } +/* global currencyFilter: true, + dateFilter: true, + filterFilter: true, + jsonFilter: true, + limitToFilter: true, + lowercaseFilter: true, + numberFilter: true, + orderByFilter: true, + uppercaseFilter: true, + */ + /** * @ngdoc provider * @name $filterProvider @@ -14251,21 +16472,11 @@ function $WindowProvider(){ * For more information about how angular filters work, and how to create your own filters, see * {@link guide/filter Filters} in the Angular Developer Guide. */ -/** - * @ngdoc method - * @name $filterProvider#register - * @description - * Register filter factory function. - * - * @param {String} name Name of the filter. - * @param {Function} fn The filter factory function which is injectable. - */ - /** * @ngdoc service * @name $filter - * @function + * @kind function * @description * Filters are used for formatting data displayed to the user. * @@ -14275,21 +16486,38 @@ function $WindowProvider(){ * * @param {String} name Name of the filter function to retrieve * @return {Function} the filter function - */ + * @example + + +
    +

    {{ originalText }}

    +

    {{ filteredText }}

    +
    +
    + + + angular.module('filterExample', []) + .controller('MainCtrl', function($scope, $filter) { + $scope.originalText = 'hello'; + $scope.filteredText = $filter('uppercase')($scope.originalText); + }); + +
    + */ $FilterProvider.$inject = ['$provide']; function $FilterProvider($provide) { var suffix = 'Filter'; /** * @ngdoc method - * @name $controllerProvider#register + * @name $filterProvider#register * @param {string|Object} name Name of the filter function, or an object map of filters where * the keys are the filter names and the values are the filter factories. * @returns {Object} Registered filter instance, or if a map of filters was provided then a map * of the registered filter instances. */ function register(name, factory) { - if(isObject(name)) { + if (isObject(name)) { var filters = {}; forEach(name, function(filter, key) { filters[key] = register(key, filter); @@ -14335,7 +16563,7 @@ function $FilterProvider($provide) { /** * @ngdoc filter * @name filter - * @function + * @kind function * * @description * Selects a subset of items from `array` and returns it as a new array. @@ -14346,20 +16574,29 @@ function $FilterProvider($provide) { * * Can be one of: * - * - `string`: The string is evaluated as an expression and the resulting value is used for substring match against - * the contents of the `array`. All strings or objects with string properties in `array` that contain this string - * will be returned. The predicate can be negated by prefixing the string with `!`. + * - `string`: The string is used for matching against the contents of the `array`. All strings or + * objects with string properties in `array` that match this string will be returned. This also + * applies to nested object properties. + * The predicate can be negated by prefixing the string with `!`. * * - `Object`: A pattern object can be used to filter specific properties on objects contained * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items * which have property `name` containing "M" and property `phone` containing "1". A special * property name `$` can be used (as in `{$:"text"}`) to accept a match against any - * property of the object. That's equivalent to the simple substring match with a `string` - * as described above. + * property of the object or its nested object properties. That's equivalent to the simple + * substring match with a `string` as described above. The predicate can be negated by prefixing + * the string with `!`. + * For example `{name: "!M"}` predicate will return an array of items which have property `name` + * not containing "M". + * + * Note that a named property will match properties on the same level only, while the special + * `$` property will match properties on the same level or deeper. E.g. an array item like + * `{name: {first: 'John', last: 'Doe'}}` will **not** be matched by `{name: 'John'}`, but + * **will** be matched by `{$: 'John'}`. * - * - `function(value)`: A predicate function can be used to write arbitrary filters. The function is - * called for each element of `array`. The final result is an array of those elements that - * the predicate returned true for. + * - `function(value, index)`: A predicate function can be used to write arbitrary filters. The + * function is called for each element of `array`. The final result is an array of those + * elements that the predicate returned true for. * * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in * determining if the expected value (from the filter expression) and actual value (from @@ -14367,15 +16604,15 @@ function $FilterProvider($provide) { * * Can be one of: * - * - `function(actual, expected)`: - * The function will be given the object value and the predicate value to compare and - * should return true if the item should be included in filtered result. + * - `function(actual, expected)`: + * The function will be given the object value and the predicate value to compare and + * should return true if both values should be considered equal. * - * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`. - * this is essentially strict comparison of expected and actual. + * - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`. + * This is essentially strict comparison of expected and actual. * - * - `false|undefined`: A short hand for a function which will look for a substring match in case - * insensitive way. + * - `false|undefined`: A short hand for a function which will look for a substring match in case + * insensitive way. * * @example @@ -14449,112 +16686,113 @@ function filterFilter() { return function(array, expression, comparator) { if (!isArray(array)) return array; - var comparatorType = typeof(comparator), - predicates = []; - - predicates.check = function(value) { - for (var j = 0; j < predicates.length; j++) { - if(!predicates[j](value)) { - return false; - } - } - return true; - }; - - if (comparatorType !== 'function') { - if (comparatorType === 'boolean' && comparator) { - comparator = function(obj, text) { - return angular.equals(obj, text); - }; - } else { - comparator = function(obj, text) { - if (obj && text && typeof obj === 'object' && typeof text === 'object') { - for (var objKey in obj) { - if (objKey.charAt(0) !== '$' && hasOwnProperty.call(obj, objKey) && - comparator(obj[objKey], text[objKey])) { - return true; - } - } - return false; - } - text = (''+text).toLowerCase(); - return (''+obj).toLowerCase().indexOf(text) > -1; - }; - } - } + var predicateFn; + var matchAgainstAnyProp; - var search = function(obj, text){ - if (typeof text == 'string' && text.charAt(0) === '!') { - return !search(obj, text.substr(1)); - } - switch (typeof obj) { - case "boolean": - case "number": - case "string": - return comparator(obj, text); - case "object": - switch (typeof text) { - case "object": - return comparator(obj, text); - default: - for ( var objKey in obj) { - if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { - return true; - } - } - break; - } - return false; - case "array": - for ( var i = 0; i < obj.length; i++) { - if (search(obj[i], text)) { - return true; - } - } - return false; - default: - return false; - } - }; switch (typeof expression) { - case "boolean": - case "number": - case "string": - // Set up expression object and fall through - expression = {$:expression}; - // jshint -W086 - case "object": - // jshint +W086 - for (var key in expression) { - (function(path) { - if (typeof expression[path] == 'undefined') return; - predicates.push(function(value) { - return search(path == '$' ? value : (value && value[path]), expression[path]); - }); - })(key); - } - break; case 'function': - predicates.push(expression); + predicateFn = expression; + break; + case 'boolean': + case 'number': + case 'string': + matchAgainstAnyProp = true; + //jshint -W086 + case 'object': + //jshint +W086 + predicateFn = createPredicateFn(expression, comparator, matchAgainstAnyProp); break; default: return array; } - var filtered = []; - for ( var j = 0; j < array.length; j++) { - var value = array[j]; - if (predicates.check(value)) { - filtered.push(value); + + return array.filter(predicateFn); + }; +} + +// Helper functions for `filterFilter` +function createPredicateFn(expression, comparator, matchAgainstAnyProp) { + var shouldMatchPrimitives = isObject(expression) && ('$' in expression); + var predicateFn; + + if (comparator === true) { + comparator = equals; + } else if (!isFunction(comparator)) { + comparator = function(actual, expected) { + if (isObject(actual) || isObject(expected)) { + // Prevent an object to be considered equal to a string like `'[object'` + return false; } + + actual = lowercase('' + actual); + expected = lowercase('' + expected); + return actual.indexOf(expected) !== -1; + }; + } + + predicateFn = function(item) { + if (shouldMatchPrimitives && !isObject(item)) { + return deepCompare(item, expression.$, comparator, false); } - return filtered; + return deepCompare(item, expression, comparator, matchAgainstAnyProp); }; + + return predicateFn; +} + +function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatchWholeObject) { + var actualType = typeof actual; + var expectedType = typeof expected; + + if ((expectedType === 'string') && (expected.charAt(0) === '!')) { + return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp); + } else if (isArray(actual)) { + // In case `actual` is an array, consider it a match + // if ANY of it's items matches `expected` + return actual.some(function(item) { + return deepCompare(item, expected, comparator, matchAgainstAnyProp); + }); + } + + switch (actualType) { + case 'object': + var key; + if (matchAgainstAnyProp) { + for (key in actual) { + if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, true)) { + return true; + } + } + return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, false); + } else if (expectedType === 'object') { + for (key in expected) { + var expectedVal = expected[key]; + if (isFunction(expectedVal)) { + continue; + } + + var matchAnyProperty = key === '$'; + var actualVal = matchAnyProperty ? actual : actual[key]; + if (!deepCompare(actualVal, expectedVal, comparator, matchAnyProperty, matchAnyProperty)) { + return false; + } + } + return true; + } else { + return comparator(actual, expected); + } + break; + case 'function': + return false; + default: + return comparator(actual, expected); + } } /** * @ngdoc filter * @name currency - * @function + * @kind function * * @description * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default @@ -14562,27 +16800,31 @@ function filterFilter() { * * @param {number} amount Input to filter. * @param {string=} symbol Currency symbol or identifier to be displayed. + * @param {number=} fractionSize Number of decimal places to round the amount to, defaults to default max fraction size for current locale * @returns {string} Formatted number. * * * @example - + -
    +

    default currency symbol ($): {{amount | currency}}
    - custom currency identifier (USD$): {{amount | currency:"USD$"}} + custom currency identifier (USD$): {{amount | currency:"USD$"}} + no fractions (0): {{amount | currency:"USD$":0}}
    it('should init with 1234.56', function() { expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); - expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('USD$1,234.56'); + expect(element(by.id('currency-custom')).getText()).toBe('USD$1,234.56'); + expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235'); }); it('should update', function() { if (browser.params.browser == 'safari') { @@ -14593,7 +16835,8 @@ function filterFilter() { element(by.model('amount')).clear(); element(by.model('amount')).sendKeys('-1234'); expect(element(by.id('currency-default')).getText()).toBe('($1,234.00)'); - expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('(USD$1,234.00)'); + expect(element(by.id('currency-custom')).getText()).toBe('(USD$1,234.00)'); + expect(element(by.id('currency-no-fractions')).getText()).toBe('(USD$1,234)'); }); @@ -14601,17 +16844,27 @@ function filterFilter() { currencyFilter.$inject = ['$locale']; function currencyFilter($locale) { var formats = $locale.NUMBER_FORMATS; - return function(amount, currencySymbol){ - if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; - return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). - replace(/\u00A4/g, currencySymbol); + return function(amount, currencySymbol, fractionSize) { + if (isUndefined(currencySymbol)) { + currencySymbol = formats.CURRENCY_SYM; + } + + if (isUndefined(fractionSize)) { + fractionSize = formats.PATTERNS[1].maxFrac; + } + + // if null or undefined pass it through + return (amount == null) + ? amount + : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize). + replace(/\u00A4/g, currencySymbol); }; } /** * @ngdoc filter * @name number - * @function + * @kind function * * @description * Formats a number as text. @@ -14625,14 +16878,15 @@ function currencyFilter($locale) { * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. * * @example - + -
    +
    Enter number:
    Default formatting: {{val | number}}
    No fractions: {{val | number:0}}
    @@ -14662,14 +16916,18 @@ numberFilter.$inject = ['$locale']; function numberFilter($locale) { var formats = $locale.NUMBER_FORMATS; return function(number, fractionSize) { - return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, - fractionSize); + + // if null or undefined pass it through + return (number == null) + ? number + : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, + fractionSize); }; } var DECIMAL_SEP = '.'; function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (number == null || !isFinite(number) || isObject(number)) return ''; + if (!isFinite(number) || isObject(number)) return ''; var isNegative = number < 0; number = Math.abs(number); @@ -14681,7 +16939,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { if (numStr.indexOf('e') !== -1) { var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); if (match && match[2] == '-' && match[3] > fractionSize + 1) { - numStr = '0'; + number = 0; } else { formatedText = numStr; hasExponent = true; @@ -14696,8 +16954,11 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); } - var pow = Math.pow(10, fractionSize); - number = Math.round(number * pow) / pow; + // safely round numbers in JS without hitting imprecisions of floating-point arithmetics + // inspired by: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round + number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize); + var fraction = ('' + number).split(DECIMAL_SEP); var whole = fraction[0]; fraction = fraction[1] || ''; @@ -14709,7 +16970,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { if (whole.length >= (lgroup + group)) { pos = whole.length - lgroup; for (i = 0; i < pos; i++) { - if ((pos - i)%group === 0 && i !== 0) { + if ((pos - i) % group === 0 && i !== 0) { formatedText += groupSep; } formatedText += whole.charAt(i); @@ -14717,28 +16978,32 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { } for (i = pos; i < whole.length; i++) { - if ((whole.length - i)%lgroup === 0 && i !== 0) { + if ((whole.length - i) % lgroup === 0 && i !== 0) { formatedText += groupSep; } formatedText += whole.charAt(i); } // format fraction part. - while(fraction.length < fractionSize) { + while (fraction.length < fractionSize) { fraction += '0'; } if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); } else { - - if (fractionSize > 0 && number > -1 && number < 1) { + if (fractionSize > 0 && number < 1) { formatedText = number.toFixed(fractionSize); + number = parseFloat(formatedText); } } - parts.push(isNegative ? pattern.negPre : pattern.posPre); - parts.push(formatedText); - parts.push(isNegative ? pattern.negSuf : pattern.posSuf); + if (number === 0) { + isNegative = false; + } + + parts.push(isNegative ? pattern.negPre : pattern.posPre, + formatedText, + isNegative ? pattern.negSuf : pattern.posSuf); return parts.join(''); } @@ -14749,7 +17014,7 @@ function padNumber(num, digits, trim) { num = -num; } num = '' + num; - while(num.length < digits) num = '0' + num; + while (num.length < digits) num = '0' + num; if (trim) num = num.substr(num.length - digits); return neg + num; @@ -14762,7 +17027,7 @@ function dateGetter(name, size, offset, trim) { var value = date['get' + name](); if (offset > 0 || value > -offset) value += offset; - if (value === 0 && offset == -12 ) value = 12; + if (value === 0 && offset == -12) value = 12; return padNumber(value, size, trim); }; } @@ -14786,6 +17051,32 @@ function timeZoneGetter(date) { return paddedZone; } +function getFirstThursdayOfYear(year) { + // 0 = index of January + var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay(); + // 4 = index of Thursday (+1 to account for 1st = 5) + // 11 = index of *next* Thursday (+1 account for 1st = 12) + return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst); +} + +function getThursdayThisWeek(datetime) { + return new Date(datetime.getFullYear(), datetime.getMonth(), + // 4 = index of Thursday + datetime.getDate() + (4 - datetime.getDay())); +} + +function weekGetter(size) { + return function(date) { + var firstThurs = getFirstThursdayOfYear(date.getFullYear()), + thisThurs = getThursdayThisWeek(date); + + var diff = +thisThurs - +firstThurs, + result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week + + return padNumber(result, size); + }; +} + function ampmGetter(date, formats) { return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; } @@ -14814,16 +17105,18 @@ var DATE_FORMATS = { EEEE: dateStrGetter('Day'), EEE: dateStrGetter('Day', true), a: ampmGetter, - Z: timeZoneGetter + Z: timeZoneGetter, + ww: weekGetter(2), + w: weekGetter(1) }; -var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, +var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEw']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|w+))(.*)/, NUMBER_STRING = /^\-?\d+$/; /** * @ngdoc filter * @name date - * @function + * @kind function * * @description * Formats `date` to a string based on the requested `format`. @@ -14843,40 +17136,44 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * * `'EEE'`: Day in Week, (Sun-Sat) * * `'HH'`: Hour in day, padded (00-23) * * `'H'`: Hour in day (0-23) - * * `'hh'`: Hour in am/pm, padded (01-12) - * * `'h'`: Hour in am/pm, (1-12) + * * `'hh'`: Hour in AM/PM, padded (01-12) + * * `'h'`: Hour in AM/PM, (1-12) * * `'mm'`: Minute in hour, padded (00-59) * * `'m'`: Minute in hour (0-59) * * `'ss'`: Second in minute, padded (00-59) * * `'s'`: Second in minute (0-59) - * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999) - * * `'a'`: am/pm marker + * * `'sss'`: Millisecond in second, padded (000-999) + * * `'a'`: AM/PM marker * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) + * * `'ww'`: Week of year, padded (00-53). Week 01 is the week with the first Thursday of the year + * * `'w'`: Week of year (0-53). Week 1 is the week with the first Thursday of the year * * `format` string can also be one of the following predefined * {@link guide/i18n localizable formats}: * * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale - * (e.g. Sep 3, 2010 12:05:08 pm) - * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) - * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale + * (e.g. Sep 3, 2010 12:05:08 PM) + * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM) + * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale * (e.g. Friday, September 3, 2010) * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) - * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) - * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) + * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM) + * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM) * - * `format` string can contain literal values. These need to be quoted with single quotes (e.g. - * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence + * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g. + * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence * (e.g. `"h 'o''clock'"`). * * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or - * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and its + * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is * specified in the string input, the time is considered to be in the local timezone. * @param {string=} format Formatting rules (see Description). If not specified, * `mediumDate` is used. + * @param {string=} timezone Timezone to be used for formatting. Right now, only `'UTC'` is supported. + * If not specified, the timezone of the browser will be used. * @returns {string} Formatted string or the input if input is not recognized as date/millis. * * @example @@ -14888,6 +17185,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
    {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
    + {{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}: + {{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}
    it('should format date', function() { @@ -14897,6 +17196,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); + expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). + toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/); }); @@ -14921,10 +17222,10 @@ function dateFilter($locale) { tzMin = int(match[9] + match[11]); } dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3])); - var h = int(match[4]||0) - tzHour; - var m = int(match[5]||0) - tzMin; - var s = int(match[6]||0); - var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000); + var h = int(match[4] || 0) - tzHour; + var m = int(match[5] || 0) - tzMin; + var s = int(match[6] || 0); + var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000); timeSetter.call(date, h, m, s, ms); return date; } @@ -14932,7 +17233,7 @@ function dateFilter($locale) { } - return function(date, format) { + return function(date, format, timezone) { var text = '', parts = [], fn, match; @@ -14940,11 +17241,7 @@ function dateFilter($locale) { format = format || 'mediumDate'; format = $locale.DATETIME_FORMATS[format] || format; if (isString(date)) { - if (NUMBER_STRING.test(date)) { - date = int(date); - } else { - date = jsonStringToDate(date); - } + date = NUMBER_STRING.test(date) ? int(date) : jsonStringToDate(date); } if (isNumber(date)) { @@ -14955,7 +17252,7 @@ function dateFilter($locale) { return date; } - while(format) { + while (format) { match = DATE_FORMATS_SPLIT.exec(format); if (match) { parts = concat(parts, match, 1); @@ -14966,7 +17263,11 @@ function dateFilter($locale) { } } - forEach(parts, function(value){ + if (timezone && timezone === 'UTC') { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + date.getTimezoneOffset()); + } + forEach(parts, function(value) { fn = DATE_FORMATS[value]; text += fn ? fn(date, $locale.DATETIME_FORMATS) : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); @@ -14980,7 +17281,7 @@ function dateFilter($locale) { /** * @ngdoc filter * @name json - * @function + * @kind function * * @description * Allows you to convert a JavaScript object into JSON string. @@ -14989,25 +17290,31 @@ function dateFilter($locale) { * the binding is automatically converted to JSON. * * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. + * @param {number=} spacing The number of spaces to use per indentation, defaults to 2. * @returns {string} JSON string. * * * @example -
    {{ {'name':'value'} | json }}
    +
    {{ {'name':'value'} | json }}
    +
    {{ {'name':'value'} | json:4 }}
    it('should jsonify filtered objects', function() { - expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); });
    * */ function jsonFilter() { - return function(object) { - return toJson(object, true); + return function(object, spacing) { + if (isUndefined(spacing)) { + spacing = 2; + } + return toJson(object, spacing); }; } @@ -15015,7 +17322,7 @@ function jsonFilter() { /** * @ngdoc filter * @name lowercase - * @function + * @kind function * @description * Converts string to lowercase. * @see angular.lowercase @@ -15026,7 +17333,7 @@ var lowercaseFilter = valueFn(lowercase); /** * @ngdoc filter * @name uppercase - * @function + * @kind function * @description * Converts string to uppercase. * @see angular.uppercase @@ -15036,14 +17343,15 @@ var uppercaseFilter = valueFn(uppercase); /** * @ngdoc filter * @name limitTo - * @function + * @kind function * * @description * Creates a new array or string containing only a specified number of elements. The elements - * are taken from either the beginning or the end of the source array or string, as specified by - * the value and sign (positive or negative) of `limit`. + * are taken from either the beginning or the end of the source array, string or number, as specified by + * the value and sign (positive or negative) of `limit`. If a number is used as input, it is + * converted to a string. * - * @param {Array|string} input Source array or string to be limited. + * @param {Array|string|number} input Source array, string or number to be limited. * @param {string|number} limit The length of the returned array or string. If the `limit` number * is positive, `limit` number of items from the beginning of the source array/string are copied. * If the number is negative, `limit` number of items from the end of the source array/string @@ -15052,136 +17360,142 @@ var uppercaseFilter = valueFn(uppercase); * had less than `limit` elements. * * @example - + -
    - Limit {{numbers}} to: +
    + Limit {{numbers}} to:

    Output numbers: {{ numbers | limitTo:numLimit }}

    - Limit {{letters}} to: + Limit {{letters}} to:

    Output letters: {{ letters | limitTo:letterLimit }}

    + Limit {{longNumber}} to: +

    Output long number: {{ longNumber | limitTo:longNumberLimit }}

    var numLimitInput = element(by.model('numLimit')); var letterLimitInput = element(by.model('letterLimit')); + var longNumberLimitInput = element(by.model('longNumberLimit')); var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); + var limitedLongNumber = element(by.binding('longNumber | limitTo:longNumberLimit')); it('should limit the number array to first three items', function() { expect(numLimitInput.getAttribute('value')).toBe('3'); expect(letterLimitInput.getAttribute('value')).toBe('3'); + expect(longNumberLimitInput.getAttribute('value')).toBe('3'); expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); expect(limitedLetters.getText()).toEqual('Output letters: abc'); + expect(limitedLongNumber.getText()).toEqual('Output long number: 234'); }); - it('should update the output when -3 is entered', function() { - numLimitInput.clear(); - numLimitInput.sendKeys('-3'); - letterLimitInput.clear(); - letterLimitInput.sendKeys('-3'); - expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); - expect(limitedLetters.getText()).toEqual('Output letters: ghi'); - }); + // There is a bug in safari and protractor that doesn't like the minus key + // it('should update the output when -3 is entered', function() { + // numLimitInput.clear(); + // numLimitInput.sendKeys('-3'); + // letterLimitInput.clear(); + // letterLimitInput.sendKeys('-3'); + // longNumberLimitInput.clear(); + // longNumberLimitInput.sendKeys('-3'); + // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); + // expect(limitedLetters.getText()).toEqual('Output letters: ghi'); + // expect(limitedLongNumber.getText()).toEqual('Output long number: 342'); + // }); it('should not exceed the maximum size of input array', function() { numLimitInput.clear(); numLimitInput.sendKeys('100'); letterLimitInput.clear(); letterLimitInput.sendKeys('100'); + longNumberLimitInput.clear(); + longNumberLimitInput.sendKeys('100'); expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); + expect(limitedLongNumber.getText()).toEqual('Output long number: 2345432342'); }); - */ -function limitToFilter(){ +*/ +function limitToFilter() { return function(input, limit) { + if (isNumber(input)) input = input.toString(); if (!isArray(input) && !isString(input)) return input; - limit = int(limit); - - if (isString(input)) { - //NaN check on limit - if (limit) { - return limit >= 0 ? input.slice(0, limit) : input.slice(limit, input.length); - } else { - return ""; - } - } - - var out = [], - i, n; - - // if abs(limit) exceeds maximum length, trim it - if (limit > input.length) - limit = input.length; - else if (limit < -input.length) - limit = -input.length; - - if (limit > 0) { - i = 0; - n = limit; + if (Math.abs(Number(limit)) === Infinity) { + limit = Number(limit); } else { - i = input.length + limit; - n = input.length; + limit = int(limit); } - for (; i 0 ? input.slice(0, limit) : input.slice(limit); + } else { + return isString(input) ? "" : []; } - - return out; }; } /** * @ngdoc filter * @name orderBy - * @function + * @kind function * * @description - * Orders a specified `array` by the `expression` predicate. + * Orders a specified `array` by the `expression` predicate. It is ordered alphabetically + * for strings and numerically for numbers. Note: if you notice numbers are not being sorted + * correctly, make sure they are actually being saved as numbers and not strings. * * @param {Array} array The array to sort. - * @param {function(*)|string|Array.<(function(*)|string)>} expression A predicate to be + * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be * used by the comparator to determine the order of elements. * * Can be one of: * * - `function`: Getter function. The result of this function will be sorted using the * `<`, `=`, `>` operator. - * - `string`: An Angular expression which evaluates to an object to order by, such as 'name' - * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control - * ascending or descending sort order (for example, +name or -name). + * - `string`: An Angular expression. The result of this expression is used to compare elements + * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by + * 3 first characters of a property called `name`). The result of a constant expression + * is interpreted as a property name to be used in comparisons (for example `"special name"` + * to sort object by the value of their `special name` property). An expression can be + * optionally prefixed with `+` or `-` to control ascending or descending sort order + * (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array + * element itself is used to compare where sorting. * - `Array`: An array of function or string predicates. The first predicate in the array * is used for sorting, but when two items are equivalent, the next predicate is used. * + * If the predicate is missing or empty then it defaults to `'+'`. + * * @param {boolean=} reverse Reverse the order of the array. * @returns {Array} Sorted copy of the source array. * * @example - + -
    +
    Sorting predicate = {{predicate}}; reverse = {{reverse}}

    [
    unsorted ] @@ -15201,53 +17515,129 @@ function limitToFilter(){
    - */ -orderByFilter.$inject = ['$parse']; -function orderByFilter($parse){ - return function(array, sortPredicate, reverseOrder) { - if (!isArray(array)) return array; - if (!sortPredicate) return array; - sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; - sortPredicate = map(sortPredicate, function(predicate){ - var descending = false, get = predicate || identity; - if (isString(predicate)) { - if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { + * + * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the + * filter routine with `$filter('orderBy')`, and calling the returned filter routine with the + * desired parameters. + * + * Example: + * + * @example + + +
    + + + + + + + + + + + +
    Name + (^)Phone NumberAge
    {{friend.name}}{{friend.phone}}{{friend.age}}
    +
    +
    + + + angular.module('orderByExample', []) + .controller('ExampleController', ['$scope', '$filter', function($scope, $filter) { + var orderBy = $filter('orderBy'); + $scope.friends = [ + { name: 'John', phone: '555-1212', age: 10 }, + { name: 'Mary', phone: '555-9876', age: 19 }, + { name: 'Mike', phone: '555-4321', age: 21 }, + { name: 'Adam', phone: '555-5678', age: 35 }, + { name: 'Julie', phone: '555-8765', age: 29 } + ]; + $scope.order = function(predicate, reverse) { + $scope.friends = orderBy($scope.friends, predicate, reverse); + }; + $scope.order('-age',false); + }]); + +
    + */ +orderByFilter.$inject = ['$parse']; +function orderByFilter($parse) { + return function(array, sortPredicate, reverseOrder) { + if (!(isArrayLike(array))) return array; + sortPredicate = isArray(sortPredicate) ? sortPredicate : [sortPredicate]; + if (sortPredicate.length === 0) { sortPredicate = ['+']; } + sortPredicate = sortPredicate.map(function(predicate) { + var descending = false, get = predicate || identity; + if (isString(predicate)) { + if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { descending = predicate.charAt(0) == '-'; predicate = predicate.substring(1); } + if (predicate === '') { + // Effectively no predicate was passed so we compare identity + return reverseComparator(compare, descending); + } get = $parse(predicate); if (get.constant) { var key = get(); - return reverseComparator(function(a,b) { + return reverseComparator(function(a, b) { return compare(a[key], b[key]); }, descending); } } - return reverseComparator(function(a,b){ + return reverseComparator(function(a, b) { return compare(get(a),get(b)); }, descending); }); - var arrayCopy = []; - for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } - return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); + return slice.call(array).sort(reverseComparator(comparator, reverseOrder)); - function comparator(o1, o2){ - for ( var i = 0; i < sortPredicate.length; i++) { + function comparator(o1, o2) { + for (var i = 0; i < sortPredicate.length; i++) { var comp = sortPredicate[i](o1, o2); if (comp !== 0) return comp; } return 0; } function reverseComparator(comp, descending) { - return toBoolean(descending) - ? function(a,b){return comp(b,a);} + return descending + ? function(a, b) {return comp(b,a);} : comp; } - function compare(v1, v2){ + + function isPrimitive(value) { + switch (typeof value) { + case 'number': /* falls through */ + case 'boolean': /* falls through */ + case 'string': + return true; + default: + return false; + } + } + + function objectToString(value) { + if (value === null) return 'null'; + if (typeof value.valueOf === 'function') { + value = value.valueOf(); + if (isPrimitive(value)) return value; + } + if (typeof value.toString === 'function') { + value = value.toString(); + if (isPrimitive(value)) return value; + } + return ''; + } + + function compare(v1, v2) { var t1 = typeof v1; var t2 = typeof v2; - if (t1 == t2) { - if (t1 == "string") { + if (t1 === t2 && t1 === "object") { + v1 = objectToString(v1); + v2 = objectToString(v2); + } + if (t1 === t2) { + if (t1 === "string") { v1 = v1.toLowerCase(); v2 = v2.toLowerCase(); } @@ -15286,28 +17676,15 @@ function ngDirective(directive) { var htmlAnchorDirective = valueFn({ restrict: 'E', compile: function(element, attr) { - - if (msie <= 8) { - - // turn link into a stylable link in IE - // but only if it doesn't have name attribute, in which case it's an anchor - if (!attr.href && !attr.name) { - attr.$set('href', ''); - } - - // add a comment node to anchors to workaround IE bug that causes element content to be reset - // to new attribute content if attribute is updated with value containing @ and element also - // contains value with @ - // see issue #1949 - element.append(document.createComment('IE fix')); - } - if (!attr.href && !attr.xlinkHref && !attr.name) { return function(scope, element) { + // If the linked element is not an anchor tag anymore, do nothing + if (element[0].nodeName.toLowerCase() !== 'a') return; + // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? 'xlink:href' : 'href'; - element.on('click', function(event){ + element.on('click', function(event) { // if we have no href url, then don't navigate anywhere. if (!element.attr(href)) { event.preventDefault(); @@ -15329,18 +17706,17 @@ var htmlAnchorDirective = valueFn({ * make the link go to the wrong URL if the user clicks it before * Angular has a chance to replace the `{{hash}}` markup with its * value. Until Angular replaces the markup the link will be broken - * and will most likely return a 404 error. - * - * The `ngHref` directive solves this problem. + * and will most likely return a 404 error. The `ngHref` directive + * solves this problem. * * The wrong way to write it: * ```html - * + * link1 * ``` * * The correct way to write it: * ```html - * + * link1 * ``` * * @element A @@ -15384,7 +17760,7 @@ var htmlAnchorDirective = valueFn({ return browser.driver.getCurrentUrl().then(function(url) { return url.match(/\/123$/); }); - }, 1000, 'page should navigate to /123'); + }, 5000, 'page should navigate to /123'); }); xit('should execute ng-click but not reload when href empty string and name specified', function() { @@ -15412,7 +17788,7 @@ var htmlAnchorDirective = valueFn({ return browser.driver.getCurrentUrl().then(function(url) { return url.match(/\/6$/); }); - }, 1000, 'page should navigate to /6'); + }, 5000, 'page should navigate to /6'); }); @@ -15478,7 +17854,7 @@ var htmlAnchorDirective = valueFn({ * * @description * - * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: + * We shouldn't do this, because it will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: * ```html *
    * @@ -15669,6 +18045,7 @@ forEach(BOOLEAN_ATTR, function(propName, attrName) { var normalized = directiveNormalize('ng-' + attrName); ngAttributeAliasDirectives[normalized] = function() { return { + restrict: 'A', priority: 100, link: function(scope, element, attr) { scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { @@ -15679,6 +18056,29 @@ forEach(BOOLEAN_ATTR, function(propName, attrName) { }; }); +// aliased input attrs are evaluated +forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { + ngAttributeAliasDirectives[ngAttr] = function() { + return { + priority: 100, + link: function(scope, element, attr) { + //special case ngPattern when a literal regular expression value + //is used as the expression (this way we don't have to watch anything). + if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") { + var match = attr.ngPattern.match(REGEX_STRING_REGEXP); + if (match) { + attr.$set("ngPattern", new RegExp(match[1], match[2])); + return; + } + } + + scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) { + attr.$set(ngAttr, value); + }); + } + }; + }; +}); // ng-src, ng-srcset, ng-href are interpolated forEach(['src', 'srcset', 'href'], function(attrName) { @@ -15698,8 +18098,12 @@ forEach(['src', 'srcset', 'href'], function(attrName) { } attr.$observe(normalized, function(value) { - if (!value) - return; + if (!value) { + if (attrName === 'href') { + attr.$set(name, null); + } + return; + } attr.$set(name, value); @@ -15714,14 +18118,22 @@ forEach(['src', 'srcset', 'href'], function(attrName) { }; }); -/* global -nullFormCtrl */ +/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true + */ var nullFormCtrl = { $addControl: noop, + $$renameControl: nullFormRenameControl, $removeControl: noop, $setValidity: noop, $setDirty: noop, - $setPristine: noop -}; + $setPristine: noop, + $setSubmitted: noop +}, +SUBMITTED_CLASS = 'ng-submitted'; + +function nullFormRenameControl(control, name) { + control.$name = name; +} /** * @ngdoc type @@ -15731,13 +18143,13 @@ var nullFormCtrl = { * @property {boolean} $dirty True if user has already interacted with the form. * @property {boolean} $valid True if all of the containing forms and controls are valid. * @property {boolean} $invalid True if at least one containing control or form is invalid. + * @property {boolean} $submitted True if user has submitted the form even if its invalid. * - * @property {Object} $error Is an object hash, containing references to all invalid controls or - * forms, where: + * @property {Object} $error Is an object hash, containing references to controls or + * forms with failing validators, where: * * - keys are validation tokens (error names), - * - values are arrays of controls or forms that are invalid for given error name. - * + * - values are arrays of controls or forms that have a failing validator for given error name. * * Built-in validation tokens: * @@ -15750,9 +18162,14 @@ var nullFormCtrl = { * - `pattern` * - `required` * - `url` + * - `date` + * - `datetimelocal` + * - `time` + * - `week` + * - `month` * * @description - * `FormController` keeps track of all its controls and nested forms as well as state of them, + * `FormController` keeps track of all its controls and nested forms as well as the state of them, * such as being valid/invalid or dirty/pristine. * * Each {@link ng.directive:form form} directive creates an instance @@ -15760,33 +18177,59 @@ var nullFormCtrl = { * */ //asks for $scope to fool the BC controller module -FormController.$inject = ['$element', '$attrs', '$scope', '$animate']; -function FormController(element, attrs, $scope, $animate) { +FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; +function FormController(element, attrs, $scope, $animate, $interpolate) { var form = this, - parentForm = element.parent().controller('form') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - errors = form.$error = {}, controls = []; + var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl; + // init state - form.$name = attrs.name || attrs.ngForm; + form.$error = {}; + form.$$success = {}; + form.$pending = undefined; + form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope); form.$dirty = false; form.$pristine = true; form.$valid = true; form.$invalid = false; + form.$submitted = false; parentForm.$addControl(form); - // Setup initial state of the control - element.addClass(PRISTINE_CLASS); - toggleValidCss(true); + /** + * @ngdoc method + * @name form.FormController#$rollbackViewValue + * + * @description + * Rollback all form controls pending updates to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. This method is typically needed by the reset button of + * a form that uses `ng-model-options` to pend updates. + */ + form.$rollbackViewValue = function() { + forEach(controls, function(control) { + control.$rollbackViewValue(); + }); + }; - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $animate.removeClass(element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); - $animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); - } + /** + * @ngdoc method + * @name form.FormController#$commitViewValue + * + * @description + * Commit all form controls pending updates to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` + * usually handles calling this in response to input events. + */ + form.$commitViewValue = function() { + forEach(controls, function(control) { + control.$commitViewValue(); + }); + }; /** * @ngdoc method @@ -15808,6 +18251,17 @@ function FormController(element, attrs, $scope, $animate) { } }; + // Private API: rename a form control + form.$$renameControl = function(control, newName) { + var oldName = control.$name; + + if (form[oldName] === control) { + delete form[oldName]; + } + form[newName] = control; + control.$name = newName; + }; + /** * @ngdoc method * @name form.FormController#$removeControl @@ -15821,13 +18275,20 @@ function FormController(element, attrs, $scope, $animate) { if (control.$name && form[control.$name] === control) { delete form[control.$name]; } - forEach(errors, function(queue, validationToken) { - form.$setValidity(validationToken, true, control); + forEach(form.$pending, function(value, name) { + form.$setValidity(name, null, control); + }); + forEach(form.$error, function(value, name) { + form.$setValidity(name, null, control); + }); + forEach(form.$$success, function(value, name) { + form.$setValidity(name, null, control); }); arrayRemove(controls, control); }; + /** * @ngdoc method * @name form.FormController#$setValidity @@ -15837,43 +18298,33 @@ function FormController(element, attrs, $scope, $animate) { * * This method will also propagate to parent forms. */ - form.$setValidity = function(validationToken, isValid, control) { - var queue = errors[validationToken]; - - if (isValid) { - if (queue) { - arrayRemove(queue, control); - if (!queue.length) { - invalidCount--; - if (!invalidCount) { - toggleValidCss(isValid); - form.$valid = true; - form.$invalid = false; - } - errors[validationToken] = false; - toggleValidCss(true, validationToken); - parentForm.$setValidity(validationToken, true, form); + addSetValidityMethod({ + ctrl: this, + $element: element, + set: function(object, property, controller) { + var list = object[property]; + if (!list) { + object[property] = [controller]; + } else { + var index = list.indexOf(controller); + if (index === -1) { + list.push(controller); } } - - } else { - if (!invalidCount) { - toggleValidCss(isValid); + }, + unset: function(object, property, controller) { + var list = object[property]; + if (!list) { + return; } - if (queue) { - if (includes(queue, control)) return; - } else { - errors[validationToken] = queue = []; - invalidCount++; - toggleValidCss(false, validationToken); - parentForm.$setValidity(validationToken, false, form); + arrayRemove(list, controller); + if (list.length === 0) { + delete object[property]; } - queue.push(control); - - form.$valid = false; - form.$invalid = true; - } - }; + }, + parentForm: parentForm, + $animate: $animate + }); /** * @ngdoc method @@ -15907,17 +18358,48 @@ function FormController(element, attrs, $scope, $animate) { * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after * saving or resetting it. */ - form.$setPristine = function () { - $animate.removeClass(element, DIRTY_CLASS); - $animate.addClass(element, PRISTINE_CLASS); + form.$setPristine = function() { + $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); form.$dirty = false; form.$pristine = true; + form.$submitted = false; forEach(controls, function(control) { control.$setPristine(); }); }; -} + /** + * @ngdoc method + * @name form.FormController#$setUntouched + * + * @description + * Sets the form to its untouched state. + * + * This method can be called to remove the 'ng-touched' class and set the form controls to their + * untouched state (ng-untouched class). + * + * Setting a form controls back to their untouched state is often useful when setting the form + * back to its pristine state. + */ + form.$setUntouched = function() { + forEach(controls, function(control) { + control.$setUntouched(); + }); + }; + + /** + * @ngdoc method + * @name form.FormController#$setSubmitted + * + * @description + * Sets the form to its submitted state. + */ + form.$setSubmitted = function() { + $animate.addClass(element, SUBMITTED_CLASS); + form.$submitted = true; + parentForm.$setSubmitted(); + }; +} /** * @ngdoc directive @@ -15967,6 +18449,7 @@ function FormController(element, attrs, $scope, $animate) { * - `ng-invalid` is set if the form is invalid. * - `ng-pristine` is set if the form is pristine. * - `ng-dirty` is set if the form is dirty. + * - `ng-submitted` is set if the form was submitted. * * Keep in mind that ngAnimate can detect each of these classes when added and removed. * @@ -16000,8 +18483,9 @@ function FormController(element, attrs, $scope, $animate) { * hitting enter in any of the input fields will trigger the click handler on the *first* button or * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) * - * @param {string=} name Name of the form. If specified, the form controller will be published into - * related scope, under this name. + * Any pending `ngModelOptions` changes will take place immediately when an enclosing form is + * submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` + * to have access to the updated model. * * ## Animation Hooks * @@ -16028,12 +18512,13 @@ function FormController(element, attrs, $scope, $animate) { * * * @example - + - + userType: Required!
    userType = {{userType}}
    @@ -16078,6 +18563,8 @@ function FormController(element, attrs, $scope, $animate) {
    * + * @param {string=} name Name of the form. If specified, the form controller will be published into + * related scope, under this name. */ var formDirectiveFactory = function(isNgForm) { return ['$timeout', function($timeout) { @@ -16085,48 +18572,60 @@ var formDirectiveFactory = function(isNgForm) { name: 'form', restrict: isNgForm ? 'EAC' : 'E', controller: FormController, - compile: function() { + compile: function ngFormCompile(formElement) { + // Setup initial state of the control + formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS); + return { - pre: function(scope, formElement, attr, controller) { - if (!attr.action) { + pre: function ngFormPreLink(scope, formElement, attr, controller) { + // if `action` attr is not present on the form, prevent the default action (submission) + if (!('action' in attr)) { // we can't use jq events because if a form is destroyed during submission the default // action is not prevented. see #1238 // // IE 9 is not affected because it doesn't fire a submit event and try to do a full // page reload if the form was destroyed by submission of the form via a click handler // on a button in the form. Looks like an IE9 specific bug. - var preventDefaultListener = function(event) { - event.preventDefault - ? event.preventDefault() - : event.returnValue = false; // IE + var handleFormSubmission = function(event) { + scope.$apply(function() { + controller.$commitViewValue(); + controller.$setSubmitted(); + }); + + event.preventDefault(); }; - addEventListenerFn(formElement[0], 'submit', preventDefaultListener); + addEventListenerFn(formElement[0], 'submit', handleFormSubmission); // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. formElement.on('$destroy', function() { $timeout(function() { - removeEventListenerFn(formElement[0], 'submit', preventDefaultListener); + removeEventListenerFn(formElement[0], 'submit', handleFormSubmission); }, 0, false); }); } - var parentFormCtrl = formElement.parent().controller('form'), - alias = attr.name || attr.ngForm; + var parentFormCtrl = controller.$$parentForm, + alias = controller.$name; if (alias) { - setter(scope, alias, controller, alias); - } - if (parentFormCtrl) { - formElement.on('$destroy', function() { - parentFormCtrl.$removeControl(controller); - if (alias) { - setter(scope, alias, undefined, alias); - } - extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards + setter(scope, null, alias, controller, alias); + attr.$observe(attr.name ? 'name' : 'ngForm', function(newValue) { + if (alias === newValue) return; + setter(scope, null, alias, undefined, alias); + alias = newValue; + setter(scope, null, alias, controller, alias); + parentFormCtrl.$$renameControl(controller, alias); }); } + formElement.on('$destroy', function() { + parentFormCtrl.$removeControl(controller); + if (alias) { + setter(scope, null, alias, undefined, alias); + } + extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards + }); } }; } @@ -16139,17 +18638,25 @@ var formDirectiveFactory = function(isNgForm) { var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); -/* global - - -VALID_CLASS, - -INVALID_CLASS, - -PRISTINE_CLASS, - -DIRTY_CLASS +/* global VALID_CLASS: false, + INVALID_CLASS: false, + PRISTINE_CLASS: false, + DIRTY_CLASS: false, + UNTOUCHED_CLASS: false, + TOUCHED_CLASS: false, + $ngModelMinErr: false, */ +// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 +var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/; var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; -var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i; +var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; +var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/; +var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; +var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; +var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; +var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; var inputType = { @@ -16158,7 +18665,8 @@ var inputType = { * @name input[text] * * @description - * Standard HTML text input with angular data binding. + * Standard HTML text input with angular data binding, inherited by most of the `input` elements. + * * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. @@ -16169,32 +18677,43 @@ var inputType = { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * as in the ngPattern directive. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object then this is used directly. + * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` + * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. + * This parameter is ignored for input[type=password] controls, which will never trim the + * input. * * @example - + - - Single word: + + Single word: Required! Single word only! - text = {{text}}
    + text = {{example.text}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    @@ -16202,9 +18721,9 @@ var inputType = {
    - var text = element(by.binding('text')); + var text = element(by.binding('example.text')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('text')); + var input = element(by.model('example.text')); it('should initialize to model', function() { expect(text.getText()).toContain('guest'); @@ -16230,94 +18749,488 @@ var inputType = { */ 'text': textInputType, + /** + * @ngdoc input + * @name input[date] + * + * @description + * Input with date validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, text must be entered in a valid ISO-8601 + * date format (yyyy-MM-dd), for example: `2009-01-06`. Since many + * modern browsers do not yet support this input type, it is important to provide cues to users on the + * expected input format via a placeholder or label. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a + * valid ISO date string (yyyy-MM-dd). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be + * a valid ISO date string (yyyy-MM-dd). + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    + Pick a date in 2013: + + + Required! + + Not a valid date! + value = {{example.value | date: "yyyy-MM-dd"}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var value = element(by.binding('example.value | date: "yyyy-MM-dd"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('example.value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (see https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-10-22'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
    + */ + 'date': createDateInputType('date', DATE_REGEXP, + createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']), + 'yyyy-MM-dd'), + + /** + * @ngdoc input + * @name input[datetime-local] + * + * @description + * Input with datetime validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a + * valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be + * a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    + Pick a date between in 2013: + + + Required! + + Not a valid date! + value = {{example.value | date: "yyyy-MM-ddTHH:mm:ss"}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('example.value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2010-12-28T14:57:00'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01-01T23:59:00'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
    + */ + 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP, + createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']), + 'yyyy-MM-ddTHH:mm:ss.sss'), /** * @ngdoc input - * @name input[number] + * @name input[time] * * @description - * Text input with number validation and transformation. Sets the `number` validation - * error if not a valid number. + * Input with time validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a + * Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a + * valid ISO time format (HH:mm:ss). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be a + * valid ISO time format (HH:mm:ss). * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - - - -
    - Number: - - Required! - - Not valid number! - value = {{value}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    -
    -
    - - var value = element(by.binding('value')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('value')); + + + +
    + Pick a between 8am and 5pm: + + + Required! + + Not a valid date! + value = {{example.value | date: "HH:mm:ss"}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var value = element(by.binding('example.value | date: "HH:mm:ss"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('example.value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } - it('should initialize to model', function() { - expect(value.getText()).toContain('12'); - expect(valid.getText()).toContain('true'); - }); + it('should initialize to model', function() { + expect(value.getText()).toContain('14:57:00'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); - it('should be invalid if empty', function() { - input.clear(); - input.sendKeys(''); - expect(value.getText()).toEqual('value ='); - expect(valid.getText()).toContain('false'); - }); + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); - it('should be invalid if over max', function() { - input.clear(); - input.sendKeys('123'); - expect(value.getText()).toEqual('value ='); - expect(valid.getText()).toContain('false'); - }); - -
    + it('should be invalid if over max', function() { + setInput('23:59:00'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); +
    +
    */ - 'number': numberInputType, + 'time': createDateInputType('time', TIME_REGEXP, + createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']), + 'HH:mm:ss.sss'), + + /** + * @ngdoc input + * @name input[week] + * + * @description + * Input with week-of-the-year validation and transformation to Date. In browsers that do not yet support + * the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * week format (yyyy-W##), for example: `2013-W02`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a + * valid ISO week format (yyyy-W##). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be + * a valid ISO week format (yyyy-W##). + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    + Pick a date between in 2013: + + + Required! + + Not a valid date! + value = {{example.value | date: "yyyy-Www"}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var value = element(by.binding('example.value | date: "yyyy-Www"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('example.value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-W01'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-W01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
    + */ + 'week': createDateInputType('week', WEEK_REGEXP, weekParser, 'yyyy-Www'), /** * @ngdoc input - * @name input[url] + * @name input[month] * * @description - * Text input with URL validation. Sets the `url` validation error key if the content is not a - * valid URL. + * Input with month validation and transformation. In browsers that do not yet support + * the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * month format (yyyy-MM), for example: `2009-01`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * If the model is not set to the first of the month, the next view to model update will set it + * to the first of the month. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be + * a valid ISO month format (yyyy-MM). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must + * be a valid ISO month format (yyyy-MM). + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    + Pick a month in 2013: + + + Required! + + Not a valid month! + value = {{example.value | date: "yyyy-MM"}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var value = element(by.binding('example.value | date: "yyyy-MM"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('example.value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-10'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
    + */ + 'month': createDateInputType('month', MONTH_REGEXP, + createDateParser(MONTH_REGEXP, ['yyyy', 'MM']), + 'yyyy-MM'), + + /** + * @ngdoc input + * @name input[number] + * + * @description + * Text input with number validation and transformation. Sets the `number` validation + * error if not a valid number. + * + * The model must always be a number, otherwise Angular will throw an error. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of @@ -16325,28 +19238,127 @@ var inputType = { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * as in the ngPattern directive. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object then this is used directly. + * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` + * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - + +
    + Number: + + Required! + + Not valid number! + value = {{example.value}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var value = element(by.binding('example.value')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('example.value')); + + it('should initialize to model', function() { + expect(value.getText()).toContain('12'); + expect(valid.getText()).toContain('true'); + }); + + it('should be invalid if empty', function() { + input.clear(); + input.sendKeys(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('false'); + }); + + it('should be invalid if over max', function() { + input.clear(); + input.sendKeys('123'); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('false'); + }); + +
    + */ + 'number': numberInputType, + + + /** + * @ngdoc input + * @name input[url] + * + * @description + * Text input with URL validation. Sets the `url` validation error key if the content is not a + * valid URL. + * + *
    + * **Note:** `input[url]` uses a regex to validate urls that is derived from the regex + * used in Chromium. If you need stricter validation, you can use `ng-pattern` or modify + * the built-in validators (see the {@link guide/forms Forms guide}) + *
    + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * as in the ngPattern directive. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object then this is used directly. + * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` + * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + -
    - URL: + + URL: Required! Not valid url! - text = {{text}}
    + text = {{url.text}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    @@ -16355,9 +19367,9 @@ var inputType = {
    - var text = element(by.binding('text')); + var text = element(by.binding('url.text')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('text')); + var input = element(by.model('url.text')); it('should initialize to model', function() { expect(text.getText()).toContain('http://google.com/'); @@ -16392,6 +19404,12 @@ var inputType = { * Text input with email validation. Sets the `email` validation error key if not a valid email * address. * + *
    + * **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex + * used in Chromium. If you need stricter validation (e.g. requiring a top-level domain), you can + * use `ng-pattern` or modify the built-in validators (see the {@link guide/forms Forms guide}) + *
    + * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. @@ -16401,28 +19419,37 @@ var inputType = { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * as in the ngPattern directive. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object then this is used directly. + * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` + * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - + -
    - Email: + + Email: Required! Not valid email! - text = {{text}}
    + text = {{email.text}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    @@ -16431,9 +19458,9 @@ var inputType = {
    - var text = element(by.binding('text')); + var text = element(by.binding('email.text')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('text')); + var input = element(by.model('email.text')); it('should initialize to model', function() { expect(text.getText()).toContain('me@example.com'); @@ -16475,32 +19502,35 @@ var inputType = { * be set when selected. * * @example - + -
    - Red
    - Green
    - Blue
    - color = {{color | json}}
    + + Red
    + Green
    + Blue
    + color = {{color.name | json}}
    Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`.
    it('should change state', function() { - var color = element(by.binding('color')); + var color = element(by.binding('color.name')); expect(color.getText()).toContain('blue'); - element.all(by.model('color')).get(0).click(); + element.all(by.model('color.name')).get(0).click(); expect(color.getText()).toContain('red'); }); @@ -16519,38 +19549,41 @@ var inputType = { * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. - * @param {string=} ngTrueValue The value to which the expression should be set when selected. - * @param {string=} ngFalseValue The value to which the expression should be set when not selected. + * @param {expression=} ngTrueValue The value to which the expression should be set when selected. + * @param {expression=} ngFalseValue The value to which the expression should be set when not selected. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - + -
    - Value1:
    - Value2:
    - value1 = {{value1}}
    - value2 = {{value2}}
    + + Value1:
    + Value2:
    + value1 = {{checkboxModel.value1}}
    + value2 = {{checkboxModel.value2}}
    it('should change state', function() { - var value1 = element(by.binding('value1')); - var value2 = element(by.binding('value2')); + var value1 = element(by.binding('checkboxModel.value1')); + var value2 = element(by.binding('checkboxModel.value2')); expect(value1.getText()).toContain('true'); expect(value2.getText()).toContain('YES'); - element(by.model('value1')).click(); - element(by.model('value2')).click(); + element(by.model('checkboxModel.value1')).click(); + element(by.model('checkboxModel.value2')).click(); expect(value1.getText()).toContain('false'); expect(value2.getText()).toContain('NO'); @@ -16567,33 +19600,20 @@ var inputType = { 'file': noop }; -// A helper function to call $setValidity and return the value / undefined, -// a pattern that is repeated a lot in the input validation logic. -function validate(ctrl, validatorName, validity, value){ - ctrl.$setValidity(validatorName, validity); - return validity ? value : undefined; +function stringBasedInputType(ctrl) { + ctrl.$formatters.push(function(value) { + return ctrl.$isEmpty(value) ? value : value.toString(); + }); } - -function addNativeHtml5Validators(ctrl, validatorName, element) { - var validity = element.prop('validity'); - if (isObject(validity)) { - var validator = function(value) { - // Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can - // perform the required validation) - if (!ctrl.$error[validatorName] && (validity.badInput || validity.customError || - validity.typeMismatch) && !validity.valueMissing) { - ctrl.$setValidity(validatorName, false); - return; - } - return value; - }; - ctrl.$parsers.push(validator); - } +function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); } -function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { - var validity = element.prop('validity'); +function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { + var type = lowercase(element[0].type); + // In composition mode, users are still inputing intermediate text buffer, // hold the listener until composition is done. // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent @@ -16610,29 +19630,27 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { }); } - var listener = function() { + var listener = function(ev) { + if (timeout) { + $browser.defer.cancel(timeout); + timeout = null; + } if (composing) return; - var value = element.val(); + var value = element.val(), + event = ev && ev.type; // By default we will trim the value // If the attribute ng-trim exists we will avoid trimming - // e.g. - if (toBoolean(attr.ngTrim || 'T')) { + // If input type is 'password', the value is never trimmed + if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) { value = trim(value); } - if (ctrl.$viewValue !== value || - // If the value is still empty/falsy, and there is no `required` error, run validators - // again. This enables HTML5 constraint validation errors to affect Angular validation - // even when the first character entered causes an error. - (validity && value === '' && !validity.valueMissing)) { - if (scope.$$phase) { - ctrl.$setViewValue(value); - } else { - scope.$apply(function() { - ctrl.$setViewValue(value); - }); - } + // If a control is suffering from bad input (due to native validators), browsers discard its + // value, so it may be necessary to revalidate (by calling $setViewValue again) even if the + // control's value is the same empty value twice in a row. + if (ctrl.$viewValue !== value || (value === '' && ctrl.$$hasNativeValidators)) { + ctrl.$setViewValue(value, event); } }; @@ -16643,11 +19661,13 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { } else { var timeout; - var deferListener = function() { + var deferListener = function(ev, input, origValue) { if (!timeout) { timeout = $browser.defer(function() { - listener(); timeout = null; + if (!input || input.value !== origValue) { + listener(ev); + } }); } }; @@ -16659,7 +19679,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { // command modifiers arrows if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; - deferListener(); + deferListener(event, this, this.value); }); // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it @@ -16675,127 +19695,256 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { ctrl.$render = function() { element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); }; +} - // pattern validator - var pattern = attr.ngPattern, - patternValidator, - match; +function weekParser(isoWeek, existingDate) { + if (isDate(isoWeek)) { + return isoWeek; + } - if (pattern) { - var validateRegex = function(regexp, value) { - return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || regexp.test(value), value); - }; - match = pattern.match(/^\/(.*)\/([gim]*)$/); - if (match) { - pattern = new RegExp(match[1], match[2]); - patternValidator = function(value) { - return validateRegex(pattern, value); - }; - } else { - patternValidator = function(value) { - var patternObj = scope.$eval(pattern); + if (isString(isoWeek)) { + WEEK_REGEXP.lastIndex = 0; + var parts = WEEK_REGEXP.exec(isoWeek); + if (parts) { + var year = +parts[1], + week = +parts[2], + hours = 0, + minutes = 0, + seconds = 0, + milliseconds = 0, + firstThurs = getFirstThursdayOfYear(year), + addDays = (week - 1) * 7; + + if (existingDate) { + hours = existingDate.getHours(); + minutes = existingDate.getMinutes(); + seconds = existingDate.getSeconds(); + milliseconds = existingDate.getMilliseconds(); + } - if (!patternObj || !patternObj.test) { - throw minErr('ngPattern')('noregexp', - 'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern, - patternObj, startingTag(element)); + return new Date(year, 0, firstThurs.getDate() + addDays, hours, minutes, seconds, milliseconds); + } + } + + return NaN; +} + +function createDateParser(regexp, mapping) { + return function(iso, date) { + var parts, map; + + if (isDate(iso)) { + return iso; + } + + if (isString(iso)) { + // When a date is JSON'ified to wraps itself inside of an extra + // set of double quotes. This makes the date parsing code unable + // to match the date string and parse it as a date. + if (iso.charAt(0) == '"' && iso.charAt(iso.length - 1) == '"') { + iso = iso.substring(1, iso.length - 1); + } + if (ISO_DATE_REGEXP.test(iso)) { + return new Date(iso); + } + regexp.lastIndex = 0; + parts = regexp.exec(iso); + + if (parts) { + parts.shift(); + if (date) { + map = { + yyyy: date.getFullYear(), + MM: date.getMonth() + 1, + dd: date.getDate(), + HH: date.getHours(), + mm: date.getMinutes(), + ss: date.getSeconds(), + sss: date.getMilliseconds() / 1000 + }; + } else { + map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 }; } - return validateRegex(patternObj, value); - }; + + forEach(parts, function(part, index) { + if (index < mapping.length) { + map[mapping[index]] = +part; + } + }); + return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0); + } } - ctrl.$formatters.push(patternValidator); - ctrl.$parsers.push(patternValidator); - } + return NaN; + }; +} - // min length validator - if (attr.ngMinlength) { - var minlength = int(attr.ngMinlength); - var minLengthValidator = function(value) { - return validate(ctrl, 'minlength', ctrl.$isEmpty(value) || value.length >= minlength, value); - }; +function createDateInputType(type, regexp, parseDate, format) { + return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { + badInputChecker(scope, element, attr, ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; + var previousDate; + + ctrl.$$parserName = type; + ctrl.$parsers.push(function(value) { + if (ctrl.$isEmpty(value)) return null; + if (regexp.test(value)) { + // Note: We cannot read ctrl.$modelValue, as there might be a different + // parser/formatter in the processing chain so that the model + // contains some different data format! + var parsedDate = parseDate(value, previousDate); + if (timezone === 'UTC') { + parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset()); + } + return parsedDate; + } + return undefined; + }); - ctrl.$parsers.push(minLengthValidator); - ctrl.$formatters.push(minLengthValidator); - } + ctrl.$formatters.push(function(value) { + if (value && !isDate(value)) { + throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); + } + if (isValidDate(value)) { + previousDate = value; + if (previousDate && timezone === 'UTC') { + var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); + previousDate = new Date(previousDate.getTime() + timezoneOffset); + } + return $filter('date')(value, format, timezone); + } else { + previousDate = null; + return ''; + } + }); - // max length validator - if (attr.ngMaxlength) { - var maxlength = int(attr.ngMaxlength); - var maxLengthValidator = function(value) { - return validate(ctrl, 'maxlength', ctrl.$isEmpty(value) || value.length <= maxlength, value); - }; + if (isDefined(attr.min) || attr.ngMin) { + var minVal; + ctrl.$validators.min = function(value) { + return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal; + }; + attr.$observe('min', function(val) { + minVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } + + if (isDefined(attr.max) || attr.ngMax) { + var maxVal; + ctrl.$validators.max = function(value) { + return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; + }; + attr.$observe('max', function(val) { + maxVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } + + function isValidDate(value) { + // Invalid Date: getTime() returns NaN + return value && !(value.getTime && value.getTime() !== value.getTime()); + } + + function parseObservedDateValue(val) { + return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined; + } + }; +} - ctrl.$parsers.push(maxLengthValidator); - ctrl.$formatters.push(maxLengthValidator); +function badInputChecker(scope, element, attr, ctrl) { + var node = element[0]; + var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity); + if (nativeValidation) { + ctrl.$parsers.push(function(value) { + var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; + // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430): + // - also sets validity.badInput (should only be validity.typeMismatch). + // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email) + // - can ignore this case as we can still read out the erroneous email... + return validity.badInput && !validity.typeMismatch ? undefined : value; + }); } } function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); + badInputChecker(scope, element, attr, ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + ctrl.$$parserName = 'number'; ctrl.$parsers.push(function(value) { - var empty = ctrl.$isEmpty(value); - if (empty || NUMBER_REGEXP.test(value)) { - ctrl.$setValidity('number', true); - return value === '' ? null : (empty ? value : parseFloat(value)); - } else { - ctrl.$setValidity('number', false); - return undefined; - } + if (ctrl.$isEmpty(value)) return null; + if (NUMBER_REGEXP.test(value)) return parseFloat(value); + return undefined; }); - addNativeHtml5Validators(ctrl, 'number', element); - ctrl.$formatters.push(function(value) { - return ctrl.$isEmpty(value) ? '' : '' + value; + if (!ctrl.$isEmpty(value)) { + if (!isNumber(value)) { + throw $ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); + } + value = value.toString(); + } + return value; }); - if (attr.min) { - var minValidator = function(value) { - var min = parseFloat(attr.min); - return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value); + if (attr.min || attr.ngMin) { + var minVal; + ctrl.$validators.min = function(value) { + return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; }; - ctrl.$parsers.push(minValidator); - ctrl.$formatters.push(minValidator); + attr.$observe('min', function(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val, 10); + } + minVal = isNumber(val) && !isNaN(val) ? val : undefined; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); } - if (attr.max) { - var maxValidator = function(value) { - var max = parseFloat(attr.max); - return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value); + if (attr.max || attr.ngMax) { + var maxVal; + ctrl.$validators.max = function(value) { + return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; }; - ctrl.$parsers.push(maxValidator); - ctrl.$formatters.push(maxValidator); + attr.$observe('max', function(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val, 10); + } + maxVal = isNumber(val) && !isNaN(val) ? val : undefined; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); } - - ctrl.$formatters.push(function(value) { - return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value); - }); } function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); - - var urlValidator = function(value) { - return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value); + // Note: no badInputChecker here by purpose as `url` is only a validation + // in browsers, i.e. we can always read out input.value even if it is not valid! + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); + + ctrl.$$parserName = 'url'; + ctrl.$validators.url = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || URL_REGEXP.test(value); }; - - ctrl.$formatters.push(urlValidator); - ctrl.$parsers.push(urlValidator); } function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); - - var emailValidator = function(value) { - return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value); + // Note: no badInputChecker here by purpose as `url` is only a validation + // in browsers, i.e. we can always read out input.value even if it is not valid! + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); + + ctrl.$$parserName = 'email'; + ctrl.$validators.email = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); }; - - ctrl.$formatters.push(emailValidator); - ctrl.$parsers.push(emailValidator); } function radioInputType(scope, element, attr, ctrl) { @@ -16804,13 +19953,13 @@ function radioInputType(scope, element, attr, ctrl) { element.attr('name', nextUid()); } - element.on('click', function() { + var listener = function(ev) { if (element[0].checked) { - scope.$apply(function() { - ctrl.$setViewValue(attr.value); - }); + ctrl.$setViewValue(attr.value, ev && ev.type); } - }); + }; + + element.on('click', listener); ctrl.$render = function() { var value = attr.value; @@ -16820,30 +19969,42 @@ function radioInputType(scope, element, attr, ctrl) { attr.$observe('value', ctrl.$render); } -function checkboxInputType(scope, element, attr, ctrl) { - var trueValue = attr.ngTrueValue, - falseValue = attr.ngFalseValue; +function parseConstantExpr($parse, context, name, expression, fallback) { + var parseFn; + if (isDefined(expression)) { + parseFn = $parse(expression); + if (!parseFn.constant) { + throw minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' + + '`{1}`.', name, expression); + } + return parseFn(context); + } + return fallback; +} - if (!isString(trueValue)) trueValue = true; - if (!isString(falseValue)) falseValue = false; +function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { + var trueValue = parseConstantExpr($parse, scope, 'ngTrueValue', attr.ngTrueValue, true); + var falseValue = parseConstantExpr($parse, scope, 'ngFalseValue', attr.ngFalseValue, false); - element.on('click', function() { - scope.$apply(function() { - ctrl.$setViewValue(element[0].checked); - }); - }); + var listener = function(ev) { + ctrl.$setViewValue(element[0].checked, ev && ev.type); + }; + + element.on('click', listener); ctrl.$render = function() { element[0].checked = ctrl.$viewValue; }; - // Override the standard `$isEmpty` because a value of `false` means empty in a checkbox. + // Override the standard `$isEmpty` because the $viewValue of an empty checkbox is always set to `false` + // This is because of the parser below, which compares the `$modelValue` with `trueValue` to convert + // it to a boolean. ctrl.$isEmpty = function(value) { - return value !== trueValue; + return value === false; }; ctrl.$formatters.push(function(value) { - return value === trueValue; + return equals(value, trueValue); }); ctrl.$parsers.push(function(value) { @@ -16871,12 +20032,14 @@ function checkboxInputType(scope, element, attr, ctrl) { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any + * length. * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. */ @@ -16886,8 +20049,14 @@ function checkboxInputType(scope, element, attr, ctrl) { * @restrict E * * @description - * HTML input element control with angular data-binding. Input control follows HTML5 input types - * and polyfills the HTML5 validation behavior for older browsers. + * HTML input element control. When used together with {@link ngModel `ngModel`}, it provides data-binding, + * input state control, and validation. + * Input control follows HTML5 input types and polyfills the HTML5 validation behavior for older browsers. + * + *
    + * **Note:** Not every feature offered is available for all input types. + * Specifically, data binding and event handling via `ng-model` is unsupported for `input[file]`. + *
    * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. @@ -16896,22 +20065,27 @@ function checkboxInputType(scope, element, attr, ctrl) { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any + * length. * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. + * This parameter is ignored for input[type=password] controls, which will never trim the + * input. * * @example - + -
    +
    User name: @@ -16936,7 +20110,7 @@ function checkboxInputType(scope, element, attr, ctrl) {
    - var user = element(by.binding('{{user}}')); + var user = element(by.exactBinding('user')); var userNameValid = element(by.binding('myForm.userName.$valid')); var lastNameValid = element(by.binding('myForm.lastName.$valid')); var lastNameError = element(by.binding('myForm.lastName.$error')); @@ -16990,485 +20164,307 @@ function checkboxInputType(scope, element, attr, ctrl) { */ -var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { +var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', + function($browser, $sniffer, $filter, $parse) { return { restrict: 'E', - require: '?ngModel', - link: function(scope, element, attr, ctrl) { - if (ctrl) { - (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer, - $browser); + require: ['?ngModel'], + link: { + pre: function(scope, element, attr, ctrls) { + if (ctrls[0]) { + (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, + $browser, $filter, $parse); + } } } }; }]; -var VALID_CLASS = 'ng-valid', - INVALID_CLASS = 'ng-invalid', - PRISTINE_CLASS = 'ng-pristine', - DIRTY_CLASS = 'ng-dirty'; -/** - * @ngdoc type - * @name ngModel.NgModelController - * - * @property {string} $viewValue Actual string value in the view. - * @property {*} $modelValue The value in the model, that the control is bound to. - * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever - the control reads value from the DOM. Each function is called, in turn, passing the value - through to the next. The last return value is used to populate the model. - Used to sanitize / convert the value as well as validation. For validation, - the parsers should update the validity state using - {@link ngModel.NgModelController#$setValidity $setValidity()}, - and return `undefined` for invalid values. - * - * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever - the model value changes. Each function is called, in turn, passing the value through to the - next. Used to format / convert values for display in the control and validation. - * ```js - * function formatter(value) { - * if (value) { - * return value.toUpperCase(); - * } - * } - * ngModel.$formatters.push(formatter); - * ``` - * - * @property {Array.} $viewChangeListeners Array of functions to execute whenever the - * view value has changed. It is called with no arguments, and its return value is ignored. - * This can be used in place of additional $watches against the model value. - * - * @property {Object} $error An object hash with all errors as keys. - * - * @property {boolean} $pristine True if user has not interacted with the control yet. - * @property {boolean} $dirty True if user has already interacted with the control. - * @property {boolean} $valid True if there is no error. - * @property {boolean} $invalid True if at least one error on the control. +var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; +/** + * @ngdoc directive + * @name ngValue * * @description + * Binds the given expression to the value of `