Created
June 18, 2013 14:27
-
-
Save hjc/5805799 to your computer and use it in GitHub Desktop.
Simple Knockout Binding Handler to enable auto focusing fields (i.e. when one field gets max input, jump to the other).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This binding has required parameters and should be an object, they are: | |
* | |
* observable - the observable we want to look at and evaluate to see if we | |
* should change focus | |
* | |
* evaluator - a function to determine if we should change focus. It should | |
* return true whenever we want to change focus and false otherwise, see | |
* below for how to add extra arguments onto the function. Any function can | |
* be used as long as you can provide the correct scope for it from the HTML | |
* binding. This likely has to be global scope. Alternatively, this can just | |
* be a number which corresponds to a length check where the focus is changed | |
* whenever the observable's value's length is >= to the number passed in. | |
* | |
* target - a CSS selector that tells JS where to focus next. It will be run | |
* through $, which will be whatever you set it to (i.e. jQuery or Prototype). | |
* | |
* note: can use an evaluator function. It should take one argument which is analogous to $data | |
* or the current value of the observable that has been bound (you can then reference it as a | |
* value, it is not an observable) | |
* note: if you want to pass extra arguments to your evaluator function, include them in the function | |
* and then use the .bind function in the data-bind attribute. For example, if my function was: | |
* self.checkEquals = function(value, equalor) { return value === equalor } | |
* and I wanted to change focus when field 1 (an observable named field1) and field 2 (another | |
* observable named field2) were equal, I would bind field1 like so: | |
* data-bind="autoFocus: {observable: field1, target: '#field2', evaluator.bind($data, field1(), field2()) | |
* Now, when field1() gets updated and happens to equal field2(), we swap to field2! | |
* NOTE: $data should always be the first parameter to .bind so the scope of the binding is | |
* correct. If you have a LOT of parameters to pass your evaluator function and you're not sure | |
* what value (or where said value will come from) to pass it, you can make the value passed by | |
* KO your last parameter and then not bind that parameter (i.e. call bind with X number of | |
* arguments where X is the number of args your function has [NOTE: 1 ARG IS FOR THIS!!! X + 1 | |
* ARGS TO BIND WOULD BIND ALL ARGS!!!]). The binding will pass the value of the observable to | |
* the function, and correctly. | |
* note: if a disabled element is going to be the target of an autofocus, it needs to load in with | |
* disabled, otherwise this binding gets run before knockout disables it, i.e. if your UI starts | |
* out disabled and with data populated | |
* note: when using this with the value binding, you need to change the value | |
* update to update on keyup, otherwise this just doesn't work. Add this line: | |
* valueUpdate: 'keyup', | |
* target overrides focus | |
* @type {{update: Function}} | |
*/ | |
ko.bindingHandlers.autoFocus = { | |
//params: element: actual DOM element | |
// valueAccessor: gets current model property (function) | |
// allBindingsAccessor: function that you can use to get ALL the model's properties. | |
// data: the viewModel the bindings were applied to | |
// ctx: an object that holds the binding context availabe to this element's bindings, has goodies like $parent and $root | |
update: function(element, valueAccessor, allBindingsAccessor, data, ctx) { | |
var options = valueAccessor() || {}; | |
//we need an observable to watch, otherwise we can't check values or | |
// get anything dynamically | |
if (options.hasOwnProperty('observable')) { | |
//the value we're going to use to test will be stored under val and | |
// SHOULD be an observable, but that's not needed | |
var val = options.observable; | |
//if this is a function, run it with no arguments to get the true value | |
// we need to evaluate under, otherwise leave it val untouched | |
if (typeof options.observable == "function") { | |
val = options.observable(); | |
} | |
//now we need SOME sort of evaluator. For us it can be either a | |
// function or a number. If its a function, we will run the | |
// function and pass it the value of the observable. If the evaluator | |
// returns true, we change focus, otherwise do nothing. If the | |
// evaluator is a number we will make an assumption: that the user | |
// wants the field to change focus whenever the string in that box | |
// gets to that length (where length === evaluator). We will do | |
// this since this is a VERY common use of autoFocus (consider a | |
// phone number split into 3 fields) | |
if (options.hasOwnProperty('evaluator')) { | |
//dont want to overwrite eval.... | |
var _eval = options.evaluator; | |
if (typeof _eval == "function") { | |
} else if (typeof _eval == "number") { | |
//they gave us a number, they want a length check, so lets | |
// set it up | |
var length = parseInt(_eval, 10); | |
//go ahead and set eval to our new function for convenience, | |
// that way our code is DRY and we use the same logic to | |
// evaluate no matter what | |
_eval = function() { | |
return val.length >= length; | |
}; | |
} | |
//allow users to key the focus target under target or focus to make | |
// the binding a bit more intuitive | |
if (options.hasOwnProperty('target') || options.hasOwnProperty('focus')) { | |
var target = options.target || options.focus; | |
if (_eval(val)) { | |
var elem = $(target); | |
//if the target element has been disabled or is hidden, | |
// we don't want to focus is it since that makes no sense | |
if (this.firstRun) { | |
this.firstRun = false; | |
return; | |
} | |
if (elem.attr('disabled') === undefined && elem.css('display') !== 'none') { | |
$(target).focus(); | |
$(target).select(); | |
} | |
} | |
} else { | |
console.log('Warning: autoFocus binding NEEDS a DOM element to swap focus to when evaluator is true!' + | |
' This should be a string selector keyed under: "target".'); | |
return; | |
} | |
} else { | |
console.log('Warning: autoFocus binding NEEDS an evaluation function keyed under the "evaluator" key to work!' + | |
' This function should return a true when we should change focus and a false if not.'); | |
return; | |
} | |
} else { | |
console.log('Warning: autoFocus binding NEEDS an observable keyed under the "observable" key to work!'); | |
return; | |
} | |
//if this is a function, subscribe to it | |
/*if (typeof options.observable == 'function') { | |
var subscription = options.observable.subscribe(this); | |
}*/ | |
}, | |
init: function(element, valueAccessor, allBindingsAccessor, data, ctx) { | |
return; | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment