Select2 - Sorting results by query
Asked Answered
A

3

16

I'm using Select2 version 4.0.0.

If my results contain multiple words, and the user enters one of those words, I want to display the results sorted by where the entered word is within the result.

For example, a user enters "apple", and my results are:

  1. "banana orange apple"
  2. "banana apple orange"
  3. "apple banana orange"

Then "apple banana orange" should appear first in the list of select2 results, because that is the result in which "apple" appears earliest within the result. I don't care so much about the ordering past that.

What do I override or configure to get something like this? It seems that matcher doesn't handle ordering, and sorter doesn't contain query data.

Apo answered 13/8, 2015 at 14:33 Comment(6)
It may be easier to arrange the elements in the original list. And presumably they will be displayed as sorted and during filter.Amnesia
I don't understand your comment. How would I know how to arrange the list before the user begins the search?Apo
I mean. Which ordering list you receive during search if you initially sort your list alphabetically?Amnesia
The ordering during the search is alphabetical by default. That's what I'd like to change.Apo
Are you loading your options via AJAX?Paleobotany
I don't think this is fully clear, do you want the results to appear first if the term starts with what your searching?Interoceptor
N
14

You could grab the search query from the value of the input box generated by Select2 by identifying it with the select2-search__field class. That's probably going to break across versions, but since they don't provide a hook to get the query some sort of hack will be needed. You could submit an issue to have them add support for accessing the query during sort, especially since it looks like it was possible in Select2 3.5.2.

$('#fruit').select2({
  width: '200px',
  sorter: function(results) {
    var query = $('.select2-search__field').val().toLowerCase();
    return results.sort(function(a, b) {
      return a.text.toLowerCase().indexOf(query) -
        b.text.toLowerCase().indexOf(query);
    });
  }
});
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.min.js"></script>
<link href="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.min.css" rel="stylesheet" />

<select id="fruit">
  <option>Banana Orange Apple</option>
  <option>Banana Apple Orange</option>
  <option>Apple Banana Orange</option>
  <option>Achocha Apple Apricot</option>
  <option>Durian Mango Papaya</option>
  <option>Papaya</option>
  <option>Tomato Papaya</option>
  <option>Durian Tomato Papaya</option>
</select>
Nunley answered 19/8, 2015 at 17:54 Comment(7)
Just a heads up, you can pretty much guarantee that the text is going to contain the search term. The only exception is if you are using a custom matcher, at which point you most likely know what the exceptions are.Herb
Thanks for the comment @KevinBrown. I'll take your word that text will contain the search term, so I simplified the code accordingly.Nunley
Thanks, this almost works, but it doesn't seem to sort through the whole list. Like, if I have multiple results where "apple" is the first word, it will put one at the top, then leave the other results unsorted below results that aren't as relevant. I'm trying @KevinBrown's method now.Apo
@Apo Huh. Do you only want to match whole words? E.g., if you're searching for apple and you have two results: banana orange apple and horseapple tomato, do you want banana orange apple to show up first?Nunley
@heenenee, no, full-word matching isn't so important, but it shouldn't behave like this: i.imgur.com/2JKKIOK.png Notice how I've searched for "proj", and it correctly puts "Project Management Tool" up top, but what you don't see in the pic is other results like "Project Rebirth" and "Project Releases" that should be immediately below "Project Management Tool".Apo
@Apo It's probably because it's you're searching with a lowercase p but the results have uppercase Ps. I modified my answer to be case-insensitive. Please try it out.Nunley
@Nunley it works perfectly now, thanks! I was actually trying Kevin Brown's method, which was giving me the same results, even when I added a case-insensitive hack to it, so I was about to assume that yours would have the same behavior. Thanks a lot... I've been messing with this for so long.Apo
H
9

The problem here is that Select2, in the 4.0.0 release, separated the querying of results from the display of results. Because of this, the sorter option which you would normally use to sort the results does not pass in the query that was made (which includes the search term).

So you are going to need to find a way to cache the query that was made so you can use it when sorting. In my answer about underlining the search term in results, I cache the query through the loading templating method, which is always triggered whenever a search is being made. That same method can be used here as well.

var query = {};

$element.select2({
  language: {
    searching: function (params) {
      // Intercept the query as it is happening
      query = params;

      // Change this to be appropriate for your application
      return 'Searching…';
    }
  }
});

So now you can build a custom sorter method which uses the saved query (and using query.term as the search term). For my example sorting method, I'm using the position within the text where the search result is to sort results. This is probably similar to what you are looking for, but this is a pretty brute force method of going about it.

function sortBySearchTerm (results) {
  // Don't alter the results being passed in, make a copy
  var sorted = results.slice(0);

  // Array.sort is an in-place sort
  sorted.sort(function (first, second) {
    query.term = query.term || "";

    var firstPosition = first.text.toUpperCase().indexOf(
      query.term.toUpperCase()
    );
    var secondPosition = second.text.toUpperCase().indexOf(
      query.term.toUpperCase()
    );

    return firstPosition - secondPosition;
  });

  return sorted;
};

And this will sort things the way that you are looking to do it. You can find a full example with all of the parts connected together below. It's using the three example options that you mentioned in your question.

var query = {};
var $element = $('select');

function sortBySearchTerm (results) {
  // Don't alter the results being passed in, make a copy
  var sorted = results.slice(0);
  
  // Array.sort is an in-place sort
  sorted.sort(function (first, second) {
    query.term = query.term || "";

    var firstPosition = first.text.toUpperCase().indexOf(
      query.term.toUpperCase()
    );
    var secondPosition = second.text.toUpperCase().indexOf(
      query.term.toUpperCase()
    );
    
    return firstPosition - secondPosition;
  });
  
  return sorted;
};

$element.select2({
  sorter: sortBySearchTerm,
  language: {
    searching: function (params) {
      // Intercept the query as it is happening
      query = params;

      // Change this to be appropriate for your application
      return 'Searching…';
    }
  }
});
<link href="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.css" rel="stylesheet"/>

<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.js"></script>

<select style="width: 50%">
  <option>banana orange apple</option>
  <option>banana apple orange</option>
  <option>apple banana orange</option>
</select>
Herb answered 19/8, 2015 at 22:40 Comment(0)
F
0

No needs to keep term:

$element.select2({
    sorter: function (data) {
        if(data && data.length>1 && data[0].rank){
            data.sort(function(a,b) {return (a.rank > b.rank) ? -1 : ((b.rank > a.rank) ? 1 : 0);} );
        }
        return data;
    }
    ,
    matcher:function(params, data) {
        // If there are no search terms, return all of the data
        if ($.trim(params.term) === '') {
          return data;
        }

        // Do not display the item if there is no 'text' property
        if (typeof data.text === 'undefined') {
          return null;
        }

        // `params.term` should be the term that is used for searching
        // `data.text` is the text that is displayed for the data object
        var idx = data.text.toLowerCase().indexOf(params.term.toLowerCase());
        if (idx > -1) {
          var modifiedData = $.extend({
            // `rank` is higher when match is more similar. If equal rank = 1
              'rank':(params.term.length / data.text.length)+ (data.text.length-params.term.length-idx)/(3*data.text.length)
          }, data, true);

          // You can return modified objects from here
          // This includes matching the `children` how you want in nested data sets
          return modifiedData;
        }

        // Return `null` if the term should not be displayed
        return null;
    }

})

Flex answered 25/4, 2018 at 21:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.