Unified
Split
Showing
with
194 additions
and 28 deletions.
- +9 −13 src/ng/directive/input.js
- +3 −1 src/ng/sniffer.js
- +1 −1 src/ngScenario/dsl.js
- +179 −12 test/ng/directive/inputSpec.js
- +2 −1 test/ng/snifferSpec.js
| @@ -954,7 +954,6 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
| } | ||
|
|
||
| function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
| var placeholder = element[0].placeholder, noevent = {}; | ||
| var type = lowercase(element[0].type); | ||
|
|
||
| // In composition mode, users are still inputing intermediate text buffer, | ||
| @@ -974,19 +973,14 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
| } | ||
|
|
||
| var listener = function(ev) { | ||
| if (timeout) { | ||
| $browser.defer.cancel(timeout); | ||
| timeout = null; | ||
| } | ||
| if (composing) return; | ||
| var value = element.val(), | ||
| event = ev && ev.type; | ||
|
|
||
| // IE (11 and under) seem to emit an 'input' event if the placeholder value changes. | ||
| // We don't want to dirty the value when this happens, so we abort here. Unfortunately, | ||
| // IE also sends input events for other non-input-related things, (such as focusing on a | ||
| // form control), so this change is not entirely enough to solve this. | ||
| if (msie && (ev || noevent).type === 'input' && element[0].placeholder !== placeholder) { | ||
| placeholder = element[0].placeholder; | ||
| return; | ||
| } | ||
|
|
||
| // By default we will trim the value | ||
| // If the attribute ng-trim exists we will avoid trimming | ||
| // If input type is 'password', the value is never trimmed | ||
| @@ -1009,11 +1003,13 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
| } else { | ||
| var timeout; | ||
|
|
||
| var deferListener = function(ev) { | ||
| var deferListener = function(ev, input, origValue) { | ||
| if (!timeout) { | ||
| timeout = $browser.defer(function() { | ||
| listener(ev); | ||
| timeout = null; | ||
| if (!input || input.value !== origValue) { | ||
| listener(ev); | ||
| } | ||
| }); | ||
| } | ||
| }; | ||
| @@ -1025,7 +1021,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
| // command modifiers arrows | ||
| if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; | ||
|
|
||
| deferListener(event); | ||
| deferListener(event, this, this.value); | ||
| }); | ||
|
|
||
| // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it | ||
| @@ -67,7 +67,9 @@ function $SnifferProvider() { | ||
| // 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'); | ||
| @@ -199,7 +199,7 @@ angular.scenario.dsl('binding', function() { | ||
| */ | ||
| angular.scenario.dsl('input', function() { | ||
| var chain = {}; | ||
| var supportInputEvent = 'oninput' in document.createElement('div') && msie != 9; | ||
| var supportInputEvent = 'oninput' in document.createElement('div') && !(msie && msie <= 11); | ||
|
|
||
| chain.enter = function(value, event) { | ||
| return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", | ||
| @@ -1549,22 +1549,170 @@ describe('input', function() { | ||
| expect(scope.name).toEqual('caitp'); | ||
| }); | ||
|
|
||
| it('should not dirty the model on an input event in response to a placeholder change', inject(function($sniffer) { | ||
| if (msie && $sniffer.hasEvent('input')) { | ||
| compileInput('<input type="text" ng-model="name" name="name" />'); | ||
| inputElm.attr('placeholder', 'Test'); | ||
| browserTrigger(inputElm, 'input'); | ||
|
|
||
| describe("IE placeholder input events", function() { | ||
| //IE fires an input event whenever a placeholder visually changes, essentially treating it as a value | ||
| //Events: | ||
| // placeholder attribute change: *input* | ||
| // focus (which visually removes the placeholder value): focusin focus *input* | ||
| // blur (which visually creates the placeholder value): focusout *input* blur | ||
| //However none of these occur if the placeholder is not visible at the time of the event. | ||
| //These tests try simulate various scenerios which do/do-not fire the extra input event | ||
|
|
||
| it('should not dirty the model on an input event in response to a placeholder change', function() { | ||
| compileInput('<input type="text" placeholder="Test" attr-capture ng-model="unsetValue" name="name" />'); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm.attr('placeholder')).toBe('Test'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| inputElm.attr('placeholder', 'Test Again'); | ||
| browserTrigger(inputElm, 'input'); | ||
| attrs.$set('placeholder', ''); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm.attr('placeholder')).toBe(''); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| attrs.$set('placeholder', 'Test Again'); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm.attr('placeholder')).toBe('Test Again'); | ||
| expect(inputElm).toBePristine(); | ||
| } | ||
| })); | ||
|
|
||
| attrs.$set('placeholder', undefined); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm.attr('placeholder')).toBe(undefined); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| changeInputValueTo('foo'); | ||
| expect(inputElm).toBeDirty(); | ||
| }); | ||
|
|
||
| it('should not dirty the model on an input event in response to a interpolated placeholder change', inject(function($rootScope) { | ||
| compileInput('<input type="text" placeholder="{{ph}}" ng-model="unsetValue" name="name" />'); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| $rootScope.ph = 1; | ||
| $rootScope.$digest(); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| $rootScope.ph = ""; | ||
| $rootScope.$digest(); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| changeInputValueTo('foo'); | ||
| expect(inputElm).toBeDirty(); | ||
| })); | ||
|
|
||
| it('should dirty the model on an input event while in focus even if the placeholder changes', inject(function($rootScope) { | ||
| $rootScope.ph = 'Test'; | ||
| compileInput('<input type="text" ng-attr-placeholder="{{ph}}" ng-model="unsetValue" name="name" />'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| browserTrigger(inputElm, 'focusin'); | ||
| browserTrigger(inputElm, 'focus'); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm.attr('placeholder')).toBe('Test'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| $rootScope.ph = 'Test Again'; | ||
| $rootScope.$digest(); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| changeInputValueTo('foo'); | ||
| expect(inputElm).toBeDirty(); | ||
| })); | ||
|
|
||
| it('should not dirty the model on an input event in response to a ng-attr-placeholder change', inject(function($rootScope) { | ||
| compileInput('<input type="text" ng-attr-placeholder="{{ph}}" ng-model="unsetValue" name="name" />'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| $rootScope.ph = 1; | ||
| $rootScope.$digest(); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| $rootScope.ph = ""; | ||
| $rootScope.$digest(); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| changeInputValueTo('foo'); | ||
| expect(inputElm).toBeDirty(); | ||
| })); | ||
|
|
||
| it('should not dirty the model on an input event in response to a focus', inject(function($sniffer) { | ||
| compileInput('<input type="text" placeholder="Test" ng-model="unsetValue" name="name" />'); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm.attr('placeholder')).toBe('Test'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| browserTrigger(inputElm, 'focusin'); | ||
| browserTrigger(inputElm, 'focus'); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm.attr('placeholder')).toBe('Test'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| changeInputValueTo('foo'); | ||
| expect(inputElm).toBeDirty(); | ||
| })); | ||
|
|
||
| it('should not dirty the model on an input event in response to a blur', inject(function($sniffer) { | ||
| compileInput('<input type="text" placeholder="Test" ng-model="unsetValue" name="name" />'); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm.attr('placeholder')).toBe('Test'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| browserTrigger(inputElm, 'focusin'); | ||
| browserTrigger(inputElm, 'focus'); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| browserTrigger(inputElm, 'focusout'); | ||
| msie && browserTrigger(inputElm, 'input'); | ||
| browserTrigger(inputElm, 'blur'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| changeInputValueTo('foo'); | ||
| expect(inputElm).toBeDirty(); | ||
| })); | ||
|
|
||
| it('should dirty the model on an input event if there is a placeholder and value', inject(function($rootScope) { | ||
| $rootScope.name = 'foo'; | ||
| compileInput('<input type="text" placeholder="Test" ng-model="name" value="init" name="name" />'); | ||
| expect(inputElm.val()).toBe($rootScope.name); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| changeInputValueTo('bar'); | ||
| expect(inputElm).toBeDirty(); | ||
| })); | ||
|
|
||
| it('should dirty the model on an input event if there is a placeholder and value after focusing', inject(function($rootScope) { | ||
| $rootScope.name = 'foo'; | ||
| compileInput('<input type="text" placeholder="Test" ng-model="name" value="init" name="name" />'); | ||
| expect(inputElm.val()).toBe($rootScope.name); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| browserTrigger(inputElm, 'focusin'); | ||
| browserTrigger(inputElm, 'focus'); | ||
| changeInputValueTo('bar'); | ||
| expect(inputElm).toBeDirty(); | ||
| })); | ||
|
|
||
| it('should dirty the model on an input event if there is a placeholder and value after bluring', inject(function($rootScope) { | ||
| $rootScope.name = 'foo'; | ||
| compileInput('<input type="text" placeholder="Test" ng-model="name" value="init" name="name" />'); | ||
| expect(inputElm.val()).toBe($rootScope.name); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| browserTrigger(inputElm, 'focusin'); | ||
| browserTrigger(inputElm, 'focus'); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| browserTrigger(inputElm, 'focusout'); | ||
| browserTrigger(inputElm, 'blur'); | ||
| changeInputValueTo('bar'); | ||
| expect(inputElm).toBeDirty(); | ||
| })); | ||
| }); | ||
|
|
||
|
|
||
| it('should interpolate input names', function() { | ||
| @@ -1656,17 +1804,21 @@ describe('input', function() { | ||
| } | ||
| }); | ||
|
|
||
| describe('"paste" and "cut" events', function() { | ||
| describe('"keydown", "paste" and "cut" events', function() { | ||
| beforeEach(function() { | ||
| // Force browser to report a lack of an 'input' event | ||
| $sniffer.hasEvent = function(eventName) { | ||
| return eventName !== 'input'; | ||
| }; | ||
| }); | ||
|
|
||
| it('should update the model on "paste" event', function() { | ||
| it('should update the model on "paste" event if the input value changes', function() { | ||
| compileInput('<input type="text" ng-model="name" name="alias" ng-change="change()" />'); | ||
|
|
||
| browserTrigger(inputElm, 'keydown'); | ||
| $browser.defer.flush(); | ||
| expect(inputElm).toBePristine(); | ||
|
|
||
| inputElm.val('mark'); | ||
| browserTrigger(inputElm, 'paste'); | ||
| $browser.defer.flush(); | ||
| @@ -1682,6 +1834,21 @@ describe('input', function() { | ||
| expect(scope.name).toEqual('john'); | ||
| }); | ||
|
|
||
| it('should cancel the delayed dirty if a change occurs', function() { | ||
| compileInput('<input type="text" ng-model="name" />'); | ||
| var ctrl = inputElm.controller('ngModel'); | ||
|
|
||
| browserTrigger(inputElm, 'keydown', {target: inputElm[0]}); | ||
| inputElm.val('f'); | ||
| browserTrigger(inputElm, 'change'); | ||
| expect(inputElm).toBeDirty(); | ||
|
|
||
| ctrl.$setPristine(); | ||
| scope.$apply(); | ||
|
|
||
| $browser.defer.flush(); | ||
| expect(inputElm).toBePristine(); | ||
| }); | ||
| }); | ||
|
|
||
|
|
||
| @@ -64,9 +64,10 @@ describe('$sniffer', function() { | ||
|
|
||
| it('should claim that IE9 doesn\'t have support for "oninput"', function() { | ||
| // IE9 implementation is fubared, so it's better to pretend that it doesn't have the support | ||
| // IE10+ implementation is fubared when mixed with placeholders | ||
| mockDivElement = {oninput: noop}; | ||
|
|
||
| expect($sniffer.hasEvent('input')).toBe((msie == 9) ? false : true); | ||
| expect($sniffer.hasEvent('input')).toBe(!(msie && msie <= 11)); | ||
| }); | ||
| }); | ||
|
|
||