text: var captionPlaceholder = {};
  ko.bindingHandlers['options'] = {
      'init': function(element) {
          if (ko.utils.tagNameLower(element) !== "select")
              throw new Error("options binding applies only to SELECT elements");
  
          // Remove all existing <option>s.
          while (element.length > 0) {
              element.remove(0);
          }
  
          // Ensures that the binding processor doesn't try to bind the options
          return { 'controlsDescendantBindings': true };
      },
      'update': function (element, valueAccessor, allBindings) {
          function selectedOptions() {
              return ko.utils.arrayFilter(element.options, function (node) { return node.selected; });
          }
  
          var selectWasPreviouslyEmpty = element.length == 0;
          var previousScrollTop = (!selectWasPreviouslyEmpty && element.multiple) ? element.scrollTop : null;
          var unwrappedArray = ko.utils.unwrapObservable(valueAccessor());
          var includeDestroyed = allBindings.get('optionsIncludeDestroyed');
          var arrayToDomNodeChildrenOptions = {};
          var captionValue;
          var filteredArray;
          var previousSelectedValues;
  
          if (element.multiple) {
              previousSelectedValues = ko.utils.arrayMap(selectedOptions(), ko.selectExtensions.readValue);
          } else {
              previousSelectedValues = element.selectedIndex >= 0 ? [ ko.selectExtensions.readValue(element.options[element.selectedIndex]) ] : [];
          }
  
          if (unwrappedArray) {
              if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
                  unwrappedArray = [unwrappedArray];
  
              // Filter out any entries marked as destroyed
              filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
                  return includeDestroyed || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
              });
  
              // If caption is included, add it to the array
              if (allBindings['has']('optionsCaption')) {
                  captionValue = ko.utils.unwrapObservable(allBindings.get('optionsCaption'));
                  // If caption value is null or undefined, don't show a caption
                  if (captionValue !== null && captionValue !== undefined) {
                      filteredArray.unshift(captionPlaceholder);
                  }
              }
          } else {
              // If a falsy value is provided (e.g. null), we'll simply empty the select element
          }
  
          function applyToObject(object, predicate, defaultValue) {
              var predicateType = typeof predicate;
              if (predicateType == "function")    // Given a function; run it against the data value
                  return predicate(object);
              else if (predicateType == "string") // Given a string; treat it as a property name on the data value
                  return object[predicate];
              else                                // Given no optionsText arg; use the data value itself
                  return defaultValue;
          }
  
          // The following functions can run at two different times:
          // The first is when the whole array is being updated directly from this binding handler.
          // The second is when an observable value for a specific array entry is updated.
          // oldOptions will be empty in the first case, but will be filled with the previously generated option in the second.
          var itemUpdate = false;
          function optionForArrayItem(arrayEntry, index, oldOptions) {
              if (oldOptions.length) {
                  previousSelectedValues = oldOptions[0].selected ? [ ko.selectExtensions.readValue(oldOptions[0]) ] : [];
                  itemUpdate = true;
              }
              var option = element.ownerDocument.createElement("option");
              if (arrayEntry === captionPlaceholder) {
                  ko.utils.setTextContent(option, allBindings.get('optionsCaption'));
                  ko.selectExtensions.writeValue(option, undefined);
              } else {
                  // Apply a value to the option element
                  var optionValue = applyToObject(arrayEntry, allBindings.get('optionsValue'), arrayEntry);
                  ko.selectExtensions.writeValue(option, ko.utils.unwrapObservable(optionValue));
  
                  // Apply some text to the option element
                  var optionText = applyToObject(arrayEntry, allBindings.get('optionsText'), optionValue);
                  ko.utils.setTextContent(option, optionText);
              }
              return [option];
          }
  
          // By using a beforeRemove callback, we delay the removal until after new items are added. This fixes a selection
          // problem in IE<=8 and Firefox. See https://github.com/knockout/knockout/issues/1208
          arrayToDomNodeChildrenOptions['beforeRemove'] =
              function (option) {
                  element.removeChild(option);
              };
  
          function setSelectionCallback(arrayEntry, newOptions) {
              // IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document.
              // That's why we first added them without selection. Now it's time to set the selection.
              if (previousSelectedValues.length) {
                  var isSelected = ko.utils.arrayIndexOf(previousSelectedValues, ko.selectExtensions.readValue(newOptions[0])) >= 0;
                  ko.utils.setOptionNodeSelectionState(newOptions[0], isSelected);
  
                  // If this option was changed from being selected during a single-item update, notify the change
                  if (itemUpdate && !isSelected)
                      ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
              }
          }
  
          var callback = setSelectionCallback;
          if (allBindings['has']('optionsAfterRender')) {
              callback = function(arrayEntry, newOptions) {
                  setSelectionCallback(arrayEntry, newOptions);
                  ko.dependencyDetection.ignore(allBindings.get('optionsAfterRender'), null, [newOptions[0], arrayEntry !== captionPlaceholder ? arrayEntry : undefined]);
              }
          }
  
          ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, arrayToDomNodeChildrenOptions, callback);
  
          ko.dependencyDetection.ignore(function () {
              if (allBindings.get('valueAllowUnset') && allBindings['has']('value')) {
                  // The model value is authoritative, so make sure its value is the one selected
                  ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */);
              } else {
                  // Determine if the selection has changed as a result of updating the options list
                  var selectionChanged;
                  if (element.multiple) {
                      // For a multiple-select box, compare the new selection count to the previous one
                      // But if nothing was selected before, the selection can't have changed
                      selectionChanged = previousSelectedValues.length && selectedOptions().length < previousSelectedValues.length;
                  } else {
                      // For a single-select box, compare the current value to the previous value
                      // But if nothing was selected before or nothing is selected now, just look for a change in selection
                      selectionChanged = (previousSelectedValues.length && element.selectedIndex >= 0)
                          ? (ko.selectExtensions.readValue(element.options[element.selectedIndex]) !== previousSelectedValues[0])
                          : (previousSelectedValues.length || element.selectedIndex >= 0);
                  }
  
                  // Ensure consistency between model value and selected option.
                  // If the dropdown was changed so that selection is no longer the same,
                  // notify the value or selectedOptions binding.
                  if (selectionChanged) {
                      ko.utils.triggerEvent(element, "change");
                  }
              }
          });
  
          // Workaround for IE bug
          ko.utils.ensureSelectElementIsRenderedCorrectly(element);
  
          if (previousScrollTop && Math.abs(previousScrollTop - element.scrollTop) > 20)
              element.scrollTop = previousScrollTop;
      }
  };
  ko.bindingHandlers['options'].optionValueDomDataKey = ko.utils.domData.nextKey();
  