10

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();
            }
        }
    });
MikeOShay
  • 522
  • 1
  • 7
  • 17
  • have you figured this out yet? Running into the same issues. – grant Nov 19 '15 at 19:32
  • No dice, I already exhausted most of my troubleshooting capabilities before I posted here. If you know anything else I should tag on this to help people find it who might be able to answer, let me know and I'll edit it into the post. – MikeOShay Nov 19 '15 at 23:33
  • 1
    appreciate it - I've spent some time banging at it as well with no luck. I'll let you know if I figure anything out :) – grant Nov 20 '15 at 02:31

9 Answers9

12

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);
});
Sniffdk
  • 316
  • 3
  • 5
  • Clever! So you still catch the keydown event while the close event's going on, and then set it programmatically. You still need to manually set the value of the templateresult if you're templating it though. I'll accept yours and update the question to show my modifications. – MikeOShay Nov 27 '15 at 21:10
  • OOF that's ugly but still seems to be the best option out there (darned if I know better). Thanks so much. – neanderslob Mar 18 '16 at 08:55
  • Thank you for your code. This helped me a lot! Thanks! – dns_nx Jul 06 '16 at 12:11
  • It appears this solution does not work for multi-select. JP's answer (http://stackoverflow.com/a/35493230/1299792) addresses multi-select with a limit to one field per page. – Marklar Jan 18 '17 at 06:12
12

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.

Dylan Smith
  • 491
  • 5
  • 11
  • with selectOnClose, the option is selected even if you click elsewhere in the body of the page (not clicking the option in the select). If this was a problem for you, were you able to find a way around it? – Marklar Jan 18 '17 at 02:43
  • In my case that wasn't a problem so I haven't looked into a way around it. My guess is that you would have to use @Sniffdk 's solution in order to only respond to TAB but not other ways of closing the select. – Dylan Smith Jan 18 '17 at 13:38
  • Thanks for taking the time to reply. For anyone else in the same situation, Sniffdk's solution doesn't work for multi-select. JP's solution does but the usual DOM events aren't fired. – Marklar Jan 18 '17 at 22:36
  • 1
    selectOnClose is ugly in that it forces at least one selection, and also selects on esc. – mmking Jun 20 '17 at 13:11
  • For anyone creating a data entry screen, this is a must have. Data entry types don't like using the mouse... ever. At least, not the ones I'm working with currently. Well done sir! – Mike Devenney Nov 30 '18 at 01:18
2

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

JP.
  • 257
  • 1
  • 3
  • 8
  • 1
    Thanks for the solution. Just a note for others, calling `trigger("change")` won't fire the DOM events (`select2:open`, `select2:close`, etc) usually seen when selecting an option. – Marklar Jan 18 '17 at 06:33
  • I'm seeing some iffy keydown behavior, where sometimes it is called five times when a key is pressed, sometimes not at all. Do you have any idea what's wrong? – mmking Jun 19 '17 at 21:28
  • @mmking Anecdotally, and from referencing discussions like [this](https://stackoverflow.com/questions/9098168/jquery-keypress-event-fires-repeatedly-when-key-is-held-but-not-on-all-keys) I believe that that's normal behavior for the keydown event. – JP. Jun 21 '17 at 16:06
2

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.

adameska
  • 51
  • 5
2

I am using the select2 version 4.0.6-rc.1 with vue, this is what I did to keep the binding safe:

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

      $(document).on("keydown.select2", function(e) {
          if (e.which === 9) {
              var highlighted = context
                  .data("select2")
                  .$dropdown.find(".select2-results__option--highlighted");

              if (highlighted) {
                  $.fn.select2.amd.require(["select2/utils"], function(Utils) {
                      var data = Utils.__cache[highlighted.data().select2Id].data;
                      var $select2 = context.data('select2');
                      $select2.trigger("select", {data: data});
                  });
              }
          }
      });

      setTimeout(function() {
        $(document).off("keydown.select2");
      }, 1);
  });

For me the key was the Utils helper which is part of the library, retrieve the list from the cache of the current element and then forcing the select with the new value.

Good luck! :)

levieraf
  • 96
  • 1
  • 3
1

Similar to @Semen Shekhovtsov's solution, but with a twist if you want the TAB to actually jump to the next field as well as make the selection (more like a normal input select would). Seperate out the KEYS.ENTER and KEYS.TAB into their own else if blocks and leave out the evt.preventDefaults(). As per below in core.js or in the select2.full.js (if you don't want to recompile it).

if (key === KEYS.ESC || (key === KEYS.UP && evt.altKey)) {
  self.close();

  evt.preventDefault();
} else if (key === KEYS.ENTER){
  self.trigger('results:select', {});
  evt.preventDefault();
} else if (key === KEYS.TAB){
  self.trigger('results:select', {});
  // leave out the prevent default if you want it to go to the next form field after selection
  //evt.preventDefault();
}
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');
        }
    });
}
0

You can simply change the source of Select2 control, only one line:

else if (key === KEYS.ENTER)

else if (key === KEYS.ENTER || key === KEYS.TAB)

From this:

 this.on('keypress', function (evt) {
   var key = evt.which;

   if (self.isOpen()) {
     if (key === KEYS.ESC || key === KEYS.TAB ||
         (key === KEYS.UP && evt.altKey)) {
       self.close();

       evt.preventDefault();
     } else if (key === KEYS.ENTER) {
       self.trigger('results:select', {});

       evt.preventDefault();

To this

this.on('keypress', function (evt) {
  var key = evt.which;

  if (self.isOpen()) {
    if (key === KEYS.ESC || (key === KEYS.UP && evt.altKey)) {
      self.close();

      evt.preventDefault();
    } else if (key === KEYS.ENTER || key === KEYS.TAB) {
      self.trigger('results:select', {});

      evt.preventDefault();

The change can be done in the source file src/js/select2/core.js, or in the compiled version. When I was applying this change, I've modified the src/js/select2/core.js and executed gruntfile.js to compile select2 library again. This solution is not a workaround, but nice feature for select2.

Community
  • 1
  • 1
  • I believe this solution was submitted as a pull request but the maintainer of select2 said it was a breaking change and would not be merged: https://github.com/select2/select2/pull/4325#issuecomment-229234605 – Marklar Jan 18 '17 at 06:08
  • @Marklar , all the tests and everything seems to be fine with this change, there is no problem at all. I don't know who called that as a 'breaking change' and why. So far I've been using proposed solution without any problem. – Semen Shekhovtsov Jan 19 '17 at 08:20
  • Thanks for taking the time to reply and providing the solution. I guess it possibly being a breaking change is something to keep in mind if you ever run into any unexplained problems. If you look at the link I provided in my last comment you can see that Kevin Brown (the maintainer of select2) stated that it was a breaking change and that selectOnClose was his recommendation. – Marklar Jan 19 '17 at 22:14
0

I mixed some of the solutions proposed here, with others to open the select2 dropdown when the element is focused.

I also wanted the TAB key to make the selection and focus immediately the next select2 field. SHIFT-TAB instead will focus the previous select2 field.

Here is my final code (select2 4.0.5, tested on FF and Chrome). I assume that your select fields have the "select2" class:

$('.select2').select2().on('select2:close', function (e) {
    var target = $(e.target);

    $(document).on('keydown.select2', function(e) {
        if (e.which === 9) { // tab
            var highlighted = target
                              .data('select2')
                              .$dropdown
                              .find('.select2-results__option--highlighted');
            if (highlighted) {
                // select the option
                var id = highlighted.data('data').id;
                target.val(id);
                target.trigger('change');
                // focus the next (or the previous) field with "select2" class
                var set = $('.select2');
                var current_index = set.index(target);
                var next_index = current_index + 1;
                if (e.shiftKey) {
                    next_index = current_index - 1;
                }
                var next = set.eq(next_index)
                next.focus();
            }
        }
    });

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

});

// on focus, open the menu
$(document).on('focus', '.select2-selection.select2-selection--single', function (e) {
    $(this).closest(".select2-container").siblings('select:enabled').select2('open');
});
Augusto Destrero
  • 4,095
  • 1
  • 23
  • 25