/** * @license SWITCH/CASE binding for Knockout http://knockoutjs.com/ * (c) Michael Best * License: MIT (http://www.opensource.org/licenses/mit-license.php) * Version 2.1.0 */ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['knockout'], factory); } else { // Browser globals factory(root.ko); } }(this, function(ko) { var undefined; if (!ko.virtualElements) throw Error('Switch-case requires at least Knockout 2.1'); var ko_virtualElements = ko.virtualElements; var ko_bindingFlags = ko.bindingFlags || {}; var ko_bindingRewriteValidators = ko.bindingRewriteValidators || ko.jsonExpressionRewriting.bindingRewriteValidators; var ko_unwrap = ko.utils.unwrapObservable; var ko_bindingHandlers = ko.bindingHandlers; var defaultvalue = {}; ko_bindingHandlers['switch'] = { flags: ko_bindingFlags.contentBind | ko_bindingFlags.canUseVirtual | ko_bindingFlags.noValue, init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { var switchSkipNextArray = [], switchBindings = { // these properties are internal $switchSkipNextArray: switchSkipNextArray, $switchValueAccessor: valueAccessor, $switchDefault: ko.observable(true), // these properties are public $default: defaultvalue, $else: defaultvalue }, contexts = []; // Update $value in each context when it changes ko.computed(function() { var value = ko_unwrap(valueAccessor()); switchBindings.$value = value; ko.utils.arrayForEach(contexts, function(context) { context.$value = value; }); }, null, {disposeWhenNodeIsRemoved: element}); // Each child element gets a new binding context so it can set its own $switchIndex property. // The other properties will be shared since they're objects. var node, nextInQueue = ko_virtualElements.firstChild(element); while (node = nextInQueue) { nextInQueue = ko_virtualElements.nextSibling(node); switch (node.nodeType) { case 1: case 8: var newContext = bindingContext.extend(switchBindings); // Set initial value of context.$switchIndex to undefined newContext.$switchIndex = undefined; ko.applyBindings(newContext, node); // Add the context to the list to be updated if this section contained a case binding if (newContext.$switchIndex !== undefined) { contexts.push(newContext); } break; } } return { controlsDescendantBindings: true }; }, preprocess: function(value) { return value || 'true'; } }; ko_bindingRewriteValidators['switch'] = false; // Can't rewrite control flow bindings ko_virtualElements.allowedBindings['switch'] = true; function checkCase(value, bindingContext) { // Check value and determine result: // If the control value is boolean, the result is the matching truthiness of the value // If value is boolean, the result is the value (allows expressions instead of just simple matching) // If value is an array, the result is true if the control value matches (strict) an item in the array // Otherwise, the result is true if value matches the control value (loose) var switchValue = ko_unwrap(bindingContext.$switchValueAccessor()); return (typeof switchValue == 'boolean') ? (value ? switchValue : !switchValue) : (typeof value == 'boolean') ? value : (value instanceof Array) ? (ko.utils.arrayIndexOf(value, switchValue) !== -1) : (value == switchValue); } function checkNotCase(value, bindingContext) { return !checkCase(value, bindingContext); } function defaultMakeValueAccessor(ifValue) { return function() { return ifValue }; } function makeCaseHandler(binding, isNot, makeValueAccessor) { var checkFunction = isNot ? checkNotCase : checkCase; binding || (binding = 'if'); makeValueAccessor || (makeValueAccessor = defaultMakeValueAccessor); return { // Inherit flags from the binding we're wrapping flags: ko_bindingHandlers[binding].flags, init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { if (!bindingContext.$switchSkipNextArray) throw Error("case binding must only be used with a switch binding"); if (bindingContext.$switchIndex !== undefined) throw Error("case binding cannot be nested"); // Initialize $switchIndex and push a new observable to $switchSkipNextArray bindingContext.$switchIndex = bindingContext.$switchSkipNextArray.push(ko.observable(false)) - 1; bindingContext.$caseValue = ko.observable(); ko.computed(function() { var index = bindingContext.$switchIndex, isLast = (index === bindingContext.$switchSkipNextArray.length - 1), result, skipNext, noDefault; if (index && bindingContext.$switchSkipNextArray[index-1]()) { // An earlier case binding matched: skip this one (and subsequent ones) result = false; skipNext = true; } else { var value = ko_unwrap(valueAccessor()); if (value === bindingContext.$else) { // If value is the special object $else, the result depends on the other case values. // If we're the last *case* item, the value must be true. $switchDefault will get // updated to *true* below, but that won't necessarily update us because it would // require a recursive update. result = bindingContext.$switchDefault() || isLast; skipNext = false; } else { // If result is true, we will skip the subsequent cases (and any default cases) noDefault = skipNext = result = checkFunction(value, bindingContext); } } // Set the observable used by the wrapped binding function bindingContext.$caseValue(result); // Update the observable "skip next" value; if the value is changed, this will update the // subsequent case item. bindingContext.$switchSkipNextArray[index](skipNext); // Update $switchDefault to false if a non-default case item has matched. // Update it to true if we're the last item and none of items have matched. // (Initially, every item will be the last, but it doesn't matter.) if (noDefault) bindingContext.$switchDefault(false); else if (!skipNext && isLast) bindingContext.$switchDefault(true); }, null, { disposeWhenNodeIsRemoved: element }); // Call init with the observable result value if (ko_bindingHandlers[binding].init) { return ko_bindingHandlers[binding].init(element, makeValueAccessor(bindingContext.$caseValue), allBindings, viewModel, bindingContext); } }, update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { // Call update with the observable result value if (ko_bindingHandlers[binding].update) { return ko_bindingHandlers[binding].update(element, makeValueAccessor(bindingContext.$caseValue), allBindings, viewModel, bindingContext); } } }; } // Support dynamically creating new case binding when using Punches plugin function getNamespacedHandler(bindingName, namespace, bindingKey) { if (ko_virtualElements.allowedBindings[bindingName]) ko_virtualElements.allowedBindings[bindingKey] = true; return makeCaseHandler(bindingName, namespace === 'casenot'); } // Support dynamically creating new case binding when using key.subkey plugin function makeSubkeyHandler(baseKey, subKey, bindingKey) { return getNamespacedHandler(subKey, baseKey, bindingKey); } function makeBaseHandler(name, isNot) { ko_bindingHandlers[name] = makeCaseHandler('if', isNot); ko_bindingRewriteValidators[name] = false; // Can't rewrite control flow bindings ko_virtualElements.allowedBindings[name] = true; ko_bindingHandlers[name].makeSubkeyHandler = makeSubkeyHandler; ko_bindingHandlers[name].getNamespacedHandler = getNamespacedHandler; } makeBaseHandler('case'); makeBaseHandler('casenot', true /*isNot*/); ko_bindingHandlers['case.visible'] = makeCaseHandler('visible'); ko_bindingHandlers['casenot.visible'] = makeCaseHandler('visible', true /*isNot*/); ko_bindingHandlers['switch'].makeCaseHandler = makeCaseHandler; }));