KO groupedOptions

by Jorge Bustos Pereda

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-debug.js"></script>
<select data-bind="groupedOptions: cars, optionsText: 'model', optionsValue:'id', optionsGroup: 'maker', optionsCaption:'titulo'">

</select>

<br>
<input type="button" data-bind="click: agregar" value="agregar"/>
<input type="button" data-bind="click: quitar" value="quitar"/>

JavaScript

var captionPlaceholder = {};
ko.bindingHandlers['groupedOptions'] = {
    '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) {

				// Si hay optgroups, se deben mover al elemento raíz
        /*for (var idx = element.length-1; idx>=0; idx--) {
            // recorre los hijos del elemento, para ver si son optgroup
            optgroup = element[idx]; 
            // si son optgroup
            if (optgroup.tagName === 'OPTGROUP') {
                // mueve sus hijos al elemento
                for (var k = child.length-1; k>=0; k--) {
                    element.insertBefore(optgroup[k], optgroup);
                }
                element.removeChild(optgroup);
            }
        }*/
        
    
        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 ? [...