1 var captionPlaceholder = {};
2 ko.bindingHandlers['options'] = {
3 'init': function(element) {
4 if (ko.utils.tagNameLower(element) !== "select")
5 throw new Error("options binding applies only to SELECT elements");
7 // Remove all existing <option>s.
8 while (element.length > 0) {
12 // Ensures that the binding processor doesn't try to bind the options
13 return { 'controlsDescendantBindings': true };
15 'update': function (element, valueAccessor, allBindings) {
16 function selectedOptions() {
17 return ko.utils.arrayFilter(element.options, function (node) { return node.selected; });
20 var selectWasPreviouslyEmpty = element.length == 0,
21 multiple = element.multiple,
22 previousScrollTop = (!selectWasPreviouslyEmpty && multiple) ? element.scrollTop : null,
23 unwrappedArray = ko.utils.unwrapObservable(valueAccessor()),
24 valueAllowUnset = allBindings.get('valueAllowUnset') && allBindings['has']('value'),
25 includeDestroyed = allBindings.get('optionsIncludeDestroyed'),
26 arrayToDomNodeChildrenOptions = {},
29 previousSelectedValues = [];
31 if (!valueAllowUnset) {
33 previousSelectedValues = ko.utils.arrayMap(selectedOptions(), ko.selectExtensions.readValue);
34 } else if (element.selectedIndex >= 0) {
35 previousSelectedValues.push(ko.selectExtensions.readValue(element.options[element.selectedIndex]));
40 if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
41 unwrappedArray = [unwrappedArray];
43 // Filter out any entries marked as destroyed
44 filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
45 return includeDestroyed || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
48 // If caption is included, add it to the array
49 if (allBindings['has']('optionsCaption')) {
50 captionValue = ko.utils.unwrapObservable(allBindings.get('optionsCaption'));
51 // If caption value is null or undefined, don't show a caption
52 if (captionValue !== null && captionValue !== undefined) {
53 filteredArray.unshift(captionPlaceholder);
57 // If a falsy value is provided (e.g. null), we'll simply empty the select element
60 function applyToObject(object, predicate, defaultValue) {
61 var predicateType = typeof predicate;
62 if (predicateType == "function") // Given a function; run it against the data value
63 return predicate(object);
64 else if (predicateType == "string") // Given a string; treat it as a property name on the data value
65 return object[predicate];
66 else // Given no optionsText arg; use the data value itself
70 // The following functions can run at two different times:
71 // The first is when the whole array is being updated directly from this binding handler.
72 // The second is when an observable value for a specific array entry is updated.
73 // oldOptions will be empty in the first case, but will be filled with the previously generated option in the second.
74 var itemUpdate = false;
75 function optionForArrayItem(arrayEntry, index, oldOptions) {
76 if (oldOptions.length) {
77 previousSelectedValues = !valueAllowUnset && oldOptions[0].selected ? [ ko.selectExtensions.readValue(oldOptions[0]) ] : [];
80 var option = element.ownerDocument.createElement("option");
81 if (arrayEntry === captionPlaceholder) {
82 ko.utils.setTextContent(option, allBindings.get('optionsCaption'));
83 ko.selectExtensions.writeValue(option, undefined);
85 // Apply a value to the option element
86 var optionValue = applyToObject(arrayEntry, allBindings.get('optionsValue'), arrayEntry);
87 ko.selectExtensions.writeValue(option, ko.utils.unwrapObservable(optionValue));
89 // Apply some text to the option element
90 var optionText = applyToObject(arrayEntry, allBindings.get('optionsText'), optionValue);
91 ko.utils.setTextContent(option, optionText);
96 // By using a beforeRemove callback, we delay the removal until after new items are added. This fixes a selection
97 // problem in IE<=8 and Firefox. See https://github.com/knockout/knockout/issues/1208
98 arrayToDomNodeChildrenOptions['beforeRemove'] =
100 element.removeChild(option);
103 function setSelectionCallback(arrayEntry, newOptions) {
104 if (itemUpdate && valueAllowUnset) {
105 // The model value is authoritative, so make sure its value is the one selected
106 // There is no need to use dependencyDetection.ignore since setDomNodeChildrenFromArrayMapping does so already.
107 ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */);
108 } else if (previousSelectedValues.length) {
109 // IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document.
110 // That's why we first added them without selection. Now it's time to set the selection.
111 var isSelected = ko.utils.arrayIndexOf(previousSelectedValues, ko.selectExtensions.readValue(newOptions[0])) >= 0;
112 ko.utils.setOptionNodeSelectionState(newOptions[0], isSelected);
114 // If this option was changed from being selected during a single-item update, notify the change
115 if (itemUpdate && !isSelected) {
116 ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
121 var callback = setSelectionCallback;
122 if (allBindings['has']('optionsAfterRender') && typeof allBindings.get('optionsAfterRender') == "function") {
123 callback = function(arrayEntry, newOptions) {
124 setSelectionCallback(arrayEntry, newOptions);
125 ko.dependencyDetection.ignore(allBindings.get('optionsAfterRender'), null, [newOptions[0], arrayEntry !== captionPlaceholder ? arrayEntry : undefined]);
129 ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, arrayToDomNodeChildrenOptions, callback);
131 ko.dependencyDetection.ignore(function () {
132 if (valueAllowUnset) {
133 // The model value is authoritative, so make sure its value is the one selected
134 ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */);
136 // Determine if the selection has changed as a result of updating the options list
137 var selectionChanged;
139 // For a multiple-select box, compare the new selection count to the previous one
140 // But if nothing was selected before, the selection can't have changed
141 selectionChanged = previousSelectedValues.length && selectedOptions().length < previousSelectedValues.length;
143 // For a single-select box, compare the current value to the previous value
144 // But if nothing was selected before or nothing is selected now, just look for a change in selection
145 selectionChanged = (previousSelectedValues.length && element.selectedIndex >= 0)
146 ? (ko.selectExtensions.readValue(element.options[element.selectedIndex]) !== previousSelectedValues[0])
147 : (previousSelectedValues.length || element.selectedIndex >= 0);
150 // Ensure consistency between model value and selected option.
151 // If the dropdown was changed so that selection is no longer the same,
152 // notify the value or selectedOptions binding.
153 if (selectionChanged) {
154 ko.utils.triggerEvent(element, "change");
159 // Workaround for IE bug
160 ko.utils.ensureSelectElementIsRenderedCorrectly(element);
162 if (previousScrollTop && Math.abs(previousScrollTop - element.scrollTop) > 20)
163 element.scrollTop = previousScrollTop;
166 ko.bindingHandlers['options'].optionValueDomDataKey = ko.utils.domData.nextKey();