From f6aa26bd07f662038400c2137cf886ffb077ea64 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Mon, 5 Aug 2013 19:12:33 +0200 Subject: [PATCH 0001/1728] 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 0002/1728] 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 0003/1728] 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 0004/1728] 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 0005/1728] 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 0006/1728] 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 0007/1728] 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 0008/1728] 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 0009/1728] 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 0010/1728] 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 0011/1728] 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 0012/1728] 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 0013/1728] 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 0014/1728] 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 %>
    -
    -
    -
    -
    -
    - - - <% }); %> + + + <% }); %> +
    +