4.27.2013

Fixing the Complex Dropdown Bug in SharePoint 2010

The Purpose of this Post
This post will show you how to get rid of pesky Complex Dropdowns from your SP2010 sites once and for all. If you just want the code, skip to the end. If you don't know what a Complex Dropdown is, or why you'd want to get rid of it, read the Quick Recap below.

Quick Recap
When you have a Lookup List that contains 19 or fewer items, SharePoint renders this as a normal dropdown. Simple, clean, elegant.

When your list reaches 20 items or more however, SharePoint has decided to automatically replace this simple, clean, and elegant dropdown with some messy, sloppy, and difficult-to-use Complex Dropdown. This is meant to be an upgrade. The text box, with jQuery behind it, allows users to type the item that they're looking for (which you can actually already do with a dropdown). However, in practice, it is prone to unexpected behavior:

1) You can already click on a dropdown and type, in order to search for an item. The complex dropdown doesn't add any useful functionality.
2) The dropdown doesn't always dismiss right away, forcing you to click more than once. It's slow, and not very helpful.
3) If a developer attempts to add code to pre-select a value, there are unintended side-effects that must be fixed with additional script.
4) In a recent update to SharePoint, a bug was introduced in which all Complex Dropdowns originate on the same location on the page. This means that if you have more than one dropdown on a page, all except one will appear to function incorrectly.

What are the Solutions?
In general, I don't like the complex dropdown for any reason. There are two main ways of solving this problem. This post focuses on the 2nd:
1) If you generate your forms in code using the FormField class, you can set FormField.InDesign = true; before rendering.
2) You can run JS on the page to fix everything. The original solution for this is here: An Egg in the SharePoint World

The Two Minor Shortcomings of the Solution Above
1) It fails on fields that are required to have values. A required Lookup field will automatically pre-select the first value in the dropdown. If the user doesn't change this value, the validator will mistakenly complain that they don't have a value selected. This is wrong for two reasons ... first, a value IS selected, and we probably shouldn't have pre-selected a value in the first place.

I fix this problem by adding an entry of  "Please select one..." at the top of the dropdown.

2) The second issue that this solution doesn't work with cascading dropdowns.

I fix this problem by reworking the code a little bit, and making sure that the values are written back and forth between the hidden complex dropdown, and the new simple dropdown. This is accomplished using the method updateNewField, which is commented below at the bottom of the post.

I wrap all of this into a ScriptLink feature, and simply enable this feature on any site where I don't want to see Complex Lookup Field Dropdowns. Don't know about creating ScriptLink features? More info here: SharePoint 2010's ScriptLink. This is a great way of packaging general-use solutions to problems.




The Code

// Comment
//http://stackoverflow.com/questions/11416099/sharepoint-drop-down-list-doesnt-display-properly-for-more-than-20-items-with-i

DisableComplexDropdowns.FixDropdowns = function () {
    $('.ms-lookuptypeintextbox').each(function () {
        DisableComplexDropdowns.OverrideDropDownList($(this).attr('title'));
    });

}

// Main Function
DisableComplexDropdowns.OverrideDropDownList = function (columnName) {
    // Construct a drop down list object
    var lookupDDL = new DisableComplexDropdowns.DropDownList(columnName);

    // Do this only in complex mode...
    if (lookupDDL.Type == "C") {
        // Hide the text box and drop down arrow
        lookupDDL.Obj.css('display', 'none');
        lookupDDL.Obj.next("img").css('display', 'none');

        // Construct the simple drop down field with change trigger
        var tempDDLName = "tempDDLName_" + columnName;
        if (lookupDDL.Obj.parent().find("select[ID='" + tempDDLName + "']").length == 0) {
            lookupDDL.Obj.parent().append("");

            var newDDL = lookupDDL.Obj.parent().find("select[ID='" + tempDDLName + "']");
            $(newDDL).bind("change", function () {
                DisableComplexDropdowns.updateOriginalField(columnName);
            });
        }

        DisableComplexDropdowns.updateNewField(columnName);
    }
};

// method to update the original and hidden field.
DisableComplexDropdowns.updateOriginalField = function (columnName) {
    var lookupDDL = new DisableComplexDropdowns.DropDownList(columnName);
    var newLookupDDL = new DisableComplexDropdowns.DropDownList("tempDDLName_" + columnName);

    // Set the text box
    if (lookupDDL.Obj.val() != newLookupDDL.Obj.find("option:selected").text())
        lookupDDL.Obj.val(newLookupDDL.Obj.find("option:selected").text());

    // Get Hidden ID
    var hiddenId = lookupDDL.Obj.attr("optHid");

    // Update the hidden variable
    if ($('input[name=' + hiddenId + ']').val() != newLookupDDL.Obj.find("option:selected").val())
        $('input[name=' + hiddenId + ']').val(newLookupDDL.Obj.find("option:selected").val());
};

//Call this method from the completefunc in SPServices.SPCascadeDropdowns like so:
//completefunc: function(){
//                if (typeof DisableComplexDropdowns != "undefined" && typeof DisableComplexDropdowns.updateNewField != 'undefined') {
//                    DisableComplexDropdowns.updateNewField(controlTitle);
//                };}
DisableComplexDropdowns.updateNewField = function (columnName) {
    var lookupDDL = new DisableComplexDropdowns.DropDownList(columnName);
    var newLookupDDL = new DisableComplexDropdowns.DropDownList("tempDDLName_" + columnName);

    if (newLookupDDL == null)
        return;

    // Get all the options
    var splittedChoices = lookupDDL.Obj.attr('choices').split("|");

    //If this is a required field, leave a "null" option at the top so that nothing is pre-selected
    if (splittedChoices[1] != "0") {
        splittedChoices.unshift("Select One ...", "0");
    }

    //Determine if the dropdowns are the same. If so, return without changing anything
    var i = 1;
    var different = false;
    newLookupDDL.Obj.children().each(function () {
        if ((i + 1) > splittedChoices.length) //Current dropdown has more items than the old dropdown
            different = true;
        if ($(this).val() != splittedChoices[i])
            different = true;
        i++;
        i++;
    });

    if (!different && (i - 1) == splittedChoices.length)
        return;

    // get selected value
    var hiddenVal = $('input[name=' + lookupDDL.Obj.attr("optHid") + ']').val()
    if (hiddenVal == "0") {
        hiddenVal = lookupDDL.Obj.attr("value")
    }

    newLookupDDL.Obj.children().each(function () {
        $(this).remove();
    });

    // Populate the drop down list
    for (var i = 0; i < splittedChoices.length; i++) {
        var optionVal = splittedChoices[i];
        i++;
        var optionId = splittedChoices[i];

        var selected = (optionId == hiddenVal) ? " selected='selected'" : "";
        newLookupDDL.Obj.append("");
    }
};

// just to construct a drop down box object. Idea token from SPServces
DisableComplexDropdowns.DropDownList = function (colName) {
    // Simple - when they are less than 20 items
    if ((this.Obj = $("select[Title='" + colName + "']")).html() != null) {
        this.Type = "S";
        // Compound - when they are more than 20 items
    } else if ((this.Obj = $("input[Title='" + colName + "']")).html() != null) {
        this.Type = "C";
        // Multi-select: This will find the multi-select column control on English and most other languages sites where the Title looks like 'Column Name possible values'
    } else if ((this.Obj = $("select[ID$='SelectCandidate'][Title^='" + colName + " ']")).html() != null) {
        this.Type = "M";
        // Multi-select: This will find the multi-select column control on a Russian site (and perhaps others) where the Title looks like 'Выбранных значений: Column Name'
    } else if ((this.Obj = $("select[ID$='SelectCandidate'][Title$=': " + colName + "']")).html() != null) {
        this.Type = "M";
    } else
        this.Type = null;
}; // End of function dropdownCtl


if (typeof $ != 'undefined') {
    $(function () {
        DisableComplexDropdowns.FixDropdowns();
    });
}