Select2 4.0.0 AJAX - Choose highlighted option with Tab

10,414

Solution 1

I have found that Sniffdk's accepted answer no longer works with the latest jquery and select2 libraries. It gives me an Uncaught TypeError: Cannot read property 'id' of undefined.

I came up with the following solution that works (for single-choice select2 dropdowns):

function pickSelect2OptionOnTab() {
    let $select;
    let optionSelected;
    let select2Closing = false;

    $('select').on('select2:closing', function(event) {
        select2Closing = true;
        $select = $(event.target);
        optionSelected = $('.select2-results__option--highlighted').text();
        setTimeout(function() {
            select2Closing = false;
        }, 1);
    });

    $(document).bind('keydown', function(event) {
        if (event.key === 'Tab' && select2Closing) {
            const val = $select.find('option').filter(function() {
                return $(this).text() === optionSelected;
            }).first().prop('value');
            $select.val(val);
            $select.trigger('change');
        }
    });
}

Solution 2

I've been trying to find a solution to this problem as well.
The main issue is that the select2 events doesn't provide any insight as to which key was pressed.

So I've come up with this hack to access the keydown event inside a select2 context.
I've been testing it to the best of my abilities and it seems to work perfectly.

selectElement
.select2({ options ... })
.on('select2:close', function(evt) {
    var context = $(evt.target);

    $(document).on('keydown.select2', function(e) {
        if (e.which === 9) { // tab
            var highlighted = context
                              .data('select2')
                              .$dropdown
                              .find('.select2-results__option--highlighted');
            if (highlighted) {
                var id = highlighted.data('data').id;
                context.val(id).trigger('change');
            }
        }
    });

    // unbind the event again to avoid binding multiple times
    setTimeout(function() {
        $(document).off('keydown.select2');
    }, 1);
});

Solution 3

The selectOnClose feature seems to be stable in 4.0.3, and a much simpler solution:

$("#user-select").select2({
  ...
  selectOnClose: true
});

It's possible that the use of templates interferes with this feature, I'm not using one so I haven't tested that.

Solution 4

For anyone looking to get tab-select working with multi-select, this worked for me:

$("#selected_ids").select2({ multiple: true }).on('select2:open', function(e) { selectOnTab(e) });

function selectOnTab(event){

  var $selected_id_field = $(event.target);

  $(".select2-search__field").on('keydown', function (e) {
    if (e.which === 9) {
      var highlighted = $('.select2-results__option--highlighted');

      if (highlighted) {
        var data = highlighted.data('data');
        var vals = $selected_id_field.val();
        if (vals === null){
          vals = [];
        }
        vals.push(data.id)
        $selected_id_field.val(vals).trigger("change")
      }
    }
  });
}

Currently this limits me to one field per page, but it's doing the job.

Thank you MikeOShay and Sniffdk for digging into this.
Currently there's an open issue that may resolve this for us:

https://github.com/select2/select2/issues/3359

Solution 5

After playing around with all these solutions, this one seems to catch the most cases and work the best for me. Note I am using select2 4.0.3 but did not like the selectOnClose, if you have multiple select2 boxes with multiple it can wreak havoc!

var fixSelect2MissingTab = function (event) {
    var $selected_id_field = $(event.target);

    var selectHighlighted = function (e) {
        if (e.which === 9) {
            var highlighted = $selected_id_field.data('select2').$dropdown.find('.select2-results__option--highlighted');

            if (highlighted) {
                var data = highlighted.data('data');
                if (data) {
                    var vals = $selected_id_field.val();
                    if (vals === null) {
                        vals = [];
                    }
                    if (vals.constructor === Array) {
                        vals.push(data.id);
                    } else {
                        vals = data.id;
                    }
                    $selected_id_field.val(vals).trigger("change");
                }
            }
        }
    };

    $('.select2-search__field').on('keydown', selectHighlighted);       
}

$(document).on('select2:open', 'select', function (e) { fixSelect2MissingTab(e) });
$(document).on('select2:close', 'select', function (e) {
    //unbind to prevent multiple
    setTimeout(function () {
        $('.select2-search__field').off('keydown');
    }, 10);
});

The nice thing about this solution is it's generic and can be applied in framework code for that will work even for dynamically added select2 boxes.

Share:
10,414
MikeOShay
Author by

MikeOShay

Updated on June 12, 2022

Comments

  • MikeOShay
    MikeOShay about 2 years

    I'm fetching user IDs and names via AJAX and using Select2 to search through them, but my users have requested the ability to select from the typeahead dropdown by pressing Tab, effectively treating it like pressing Enter. Here is my select2 declaration:

    $("#user-select").select2({
        ajax: {
            url: "/api/User",
            method: "get",
            data: function (params) {
                return {
                    search: params.term
                };
            },
            beforeSend: function () {
                $(".loading-results").text("Loading...");
            },
            processResults: function (data) {
                return {
                    results: data
                };
            },
            cache: true
        },
        allowClear: true,
        placeholder: "Enter a User ID or Name",
        templateResult: function (data) {
            return "(" + data.id + ") " + data.name;
        },
        templateSelection: function (data) {
            return "(" + data.id + ") " + data.name;
        }
    

    ".select2-search__field" seems to be the focused element whenever the dropdown's visible, and the highlighted element gets the class "select2-results__option--highlighted".

    I've tried a few solutions, but nothing seems to have worked, especially because this element appears and disappears anytime the dropdown opens. Unfortunately I lost the code from my attempts, but they consisted mainly of doing preventDefault when Tab is hit on the focused input and then triggering a click event on the highlighted element or triggering the enter key on the input.

    I also tried adjusting the selectOnClose option, but that seems buggy in general and caused an endless loop when I had it running normally, much less trying to override it based on what key is being pressed.

    [Edit]
    The selected solution works, but doesn't account for the templateResult specified, instead showing "() undefined". So, I tweaked it to add the highlighted answer as the selected Option for the overlying Select, and then call the change event right on that Select.

    ...(Same as initial select2)

    }).on('select2:close', function (evt) {
        var context = $(evt.target);
    
        $(document).on('keydown.select2', function (e) {
            if (e.which === 9) { // tab
                var highlighted = context.data('select2').$dropdown.find('.select2-results__option--highlighted');
    
                if (highlighted) {
                    var data = highlighted.data('data');
    
                    var id = data.id;
                    var display = data.name;
    
                    $("#user-select").html("<option value='" + id + "' selected='selected'>" + display + "</option>");
                    $("#user-select").change();
                }
                else {
                    context.val("").change();
                }
            }
        });