Skip to content

Commit

Permalink
Add textInput autocomplete workaround for IE 10 and possibly others. F…
Browse files Browse the repository at this point in the history
…ixes #1167

Make textInput catch ESC changes in IE. Fixes #1678
  • Loading branch information
mbest committed Dec 3, 2016
1 parent 241c26c commit 11e3cb9
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 46 deletions.
12 changes: 12 additions & 0 deletions spec/defaultBindings/textInputBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,18 @@ describe('Binding: TextInput', function() {
}
});

it('Should update observable on blur event', function () {
var myobservable = new ko.observable(123);
testNode.innerHTML = "<input data-bind='textInput: someProp' /><input />";
ko.applyBindings({ someProp: myobservable }, testNode);
expect(testNode.childNodes[0].value).toEqual("123");

testNode.childNodes[0].focus();
testNode.childNodes[0].value = "some user-entered value";
testNode.childNodes[1].focus(); // focus on a different input to blur the previous one
expect(myobservable()).toEqual("some user-entered value");
});

it('Should write only changed values to observable', function () {
var model = { writtenValue: '' };

Expand Down
103 changes: 58 additions & 45 deletions src/binding/defaultBindings/textInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ if (window && window.navigator) {
};

// Detect various browser versions because some old versions don't fully support the 'input' event
var operaVersion = window.opera && window.opera.version && parseInt(window.opera.version()),
userAgent = window.navigator.userAgent,
safariVersion = parseVersion(userAgent.match(/^(?:(?!chrome).)*version\/([^ ]*) safari/i)),
firefoxVersion = parseVersion(userAgent.match(/Firefox\/([^ ]*)/));
var userAgent = window.navigator.userAgent,
operaVersion, chromeVersion, safariVersion, firefoxVersion, ieVersion;

(operaVersion = window.opera && window.opera.version && parseInt(window.opera.version()))
|| (chromeVersion = parseVersion(userAgent.match(/Chrome\/([^ ]+)/)))
|| (safariVersion = parseVersion(userAgent.match(/Version\/([^ ]+) Safari/)))
|| (firefoxVersion = parseVersion(userAgent.match(/Firefox\/([^ ]+)/)))
|| (ieVersion = ko.utils.ieVersion || parseVersion(userAgent.match(/MSIE ([^ ]+)/))) // Detects up to IE 10
|| (ieVersion = parseVersion(userAgent.match(/rv:([^ )]+)/))); // Detects IE 11
}

// IE 8 and 9 have bugs that prevent the normal events from firing when the value changes.
Expand All @@ -20,7 +25,7 @@ if (window && window.navigator) {
// fired at the document level only and doesn't directly indicate which element changed. We
// set up just one event handler for the document and use 'activeElement' to determine which
// element was changed.
if (ko.utils.ieVersion < 10) {
if (ieVersion >= 8 && ieVersion < 10) {
var selectionChangeRegisteredName = ko.utils.domData.nextKey(),
selectionChangeHandlerName = ko.utils.domData.nextKey();
var selectionChangeHandler = function(event) {
Expand Down Expand Up @@ -110,62 +115,70 @@ ko.bindingHandlers['textInput'] = {
}
});
} else {
if (ko.utils.ieVersion < 10) {
if (ieVersion) {
// All versions (including 11) of Internet Explorer have a bug that they don't generate an input or propertychange event when ESC is pressed
onEvent('keypress', updateModel);
}
if (ieVersion < 11) {
// Internet Explorer <= 8 doesn't support the 'input' event, but does include 'propertychange' that fires whenever
// any property of an element changes. Unlike 'input', it also fires if a property is changed from JavaScript code,
// but that's an acceptable compromise for this binding. IE 9 does support 'input', but since it doesn't fire it
// when using autocomplete, we'll use 'propertychange' for it also.
// but that's an acceptable compromise for this binding. IE 9 and 10 support 'input', but since they don't always
// fire it when using autocomplete, we'll use 'propertychange' for them also.
onEvent('propertychange', function(event) {
if (event.propertyName === 'value') {
ieUpdateModel(event);
}
});
}
if (ieVersion == 8) {
// IE 8 has a bug where it fails to fire 'propertychange' on the first update following a value change from
// JavaScript code. It also doesn't fire if you clear the entire value. To fix this, we bind to the following
// events too.
onEvent('keyup', updateModel); // A single keystoke
onEvent('keydown', updateModel); // The first character when a key is held down
}
if (registerForSelectionChangeEvent) {
// Internet Explorer 9 doesn't fire the 'input' event when deleting text, including using
// the backspace, delete, or ctrl-x keys, clicking the 'x' to clear the input, dragging text
// out of the field, and cutting or deleting text using the context menu. 'selectionchange'
// can detect all of those except dragging text out of the field, for which we use 'dragend'.
// These are also needed in IE8 because of the bug described above.
registerForSelectionChangeEvent(element, ieUpdateModel); // 'selectionchange' covers cut, paste, drop, delete, etc.
onEvent('dragend', deferUpdateModel);
}

if (ko.utils.ieVersion == 8) {
// IE 8 has a bug where it fails to fire 'propertychange' on the first update following a value change from
// JavaScript code. It also doesn't fire if you clear the entire value. To fix this, we bind to the following
// events too.
onEvent('keyup', updateModel); // A single keystoke
onEvent('keydown', updateModel); // The first character when a key is held down
}
if (ko.utils.ieVersion >= 8) {
// Internet Explorer 9 doesn't fire the 'input' event when deleting text, including using
// the backspace, delete, or ctrl-x keys, clicking the 'x' to clear the input, dragging text
// out of the field, and cutting or deleting text using the context menu. 'selectionchange'
// can detect all of those except dragging text out of the field, for which we use 'dragend'.
// These are also needed in IE8 because of the bug described above.
registerForSelectionChangeEvent(element, ieUpdateModel); // 'selectionchange' covers cut, paste, drop, delete, etc.
onEvent('dragend', deferUpdateModel);
}
} else {
if (!ieVersion || ieVersion >= 9) {
// All other supported browsers support the 'input' event, which fires whenever the content of the element is changed
// through the user interface.
onEvent('input', updateModel);

if (safariVersion < 5 && ko.utils.tagNameLower(element) === "textarea") {
// Safari <5 doesn't fire the 'input' event for <textarea> elements (it does fire 'textInput'
// but only when typing). So we'll just catch as much as we can with keydown, cut, and paste.
onEvent('keydown', deferUpdateModel);
onEvent('paste', deferUpdateModel);
onEvent('cut', deferUpdateModel);
} else if (operaVersion < 11) {
// Opera 10 doesn't always fire the 'input' event for cut, paste, undo & drop operations.
// We can try to catch some of those using 'keydown'.
onEvent('keydown', deferUpdateModel);
} else if (firefoxVersion < 4.0) {
// Firefox <= 3.6 doesn't fire the 'input' event when text is filled in through autocomplete
onEvent('DOMAutoComplete', updateModel);

// Firefox <=3.5 doesn't fire the 'input' event when text is dropped into the input.
onEvent('dragdrop', updateModel); // <3.5
onEvent('drop', updateModel); // 3.5
}
onEvent('input', ieUpdateModel);
}

if (safariVersion < 5 && ko.utils.tagNameLower(element) === "textarea") {
// Safari <5 doesn't fire the 'input' event for <textarea> elements (it does fire 'textInput'
// but only when typing). So we'll just catch as much as we can with keydown, cut, and paste.
onEvent('keydown', deferUpdateModel);
onEvent('paste', deferUpdateModel);
onEvent('cut', deferUpdateModel);
} else if (operaVersion < 11) {
// Opera 10 doesn't always fire the 'input' event for cut, paste, undo & drop operations.
// We can try to catch some of those using 'keydown'.
onEvent('keydown', deferUpdateModel);
} else if (firefoxVersion < 4.0) {
// Firefox <= 3.6 doesn't fire the 'input' event when text is filled in through autocomplete
onEvent('DOMAutoComplete', updateModel);

// Firefox <=3.5 doesn't fire the 'input' event when text is dropped into the input.
onEvent('dragdrop', updateModel); // <3.5
onEvent('drop', updateModel); // 3.5
}
}

// Bind to the change event so that we can catch programmatic updates of the value that fire this event.
onEvent('change', updateModel);

// To deal with browsers that don't notify any kind of event for some changes (IE, Safari, etc.)
onEvent('blur', updateModel);

ko.computed(updateView, null, { disposeWhenNodeIsRemoved: element });
}
};
Expand Down
2 changes: 1 addition & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ ko.utils = (function () {
registerEventHandler: function (element, eventType, handler) {
var wrappedHandler = ko.utils.catchFunctionErrors(handler);

var mustUseAttachEvent = ieVersion && eventsThatMustBeRegisteredUsingAttachEvent[eventType];
var mustUseAttachEvent = eventsThatMustBeRegisteredUsingAttachEvent[eventType];
if (!ko.options['useOnlyNativeEvents'] && !mustUseAttachEvent && jQueryInstance) {
jQueryInstance(element)['bind'](eventType, wrappedHandler);
} else if (!mustUseAttachEvent && typeof element.addEventListener == "function")
Expand Down

0 comments on commit 11e3cb9

Please sign in to comment.