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:
"banana orange apple"
"banana apple orange"
"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.
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>
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>
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;
}
})
Related
I want to make a list of checkboxes on a UI that user's can use to toggle and filter a set of data results. The checkboxes can be cumulative so I store them as a string array for now. My code looks something like this.
export interface IMyObjectFromAPI {
status: {
id: number,
description: string,
location: string,
name: string,
imageUrl: string
}
}
var filteredByTerms: string[] = [];
var resultsFromAPI: IMyObjectFromAPI [] = [];
var filteredDataResults: IMyObjectFromAPI[] = [];
I save the return results from the api call into the resultsFromAPI array.
On the UI, I have a group of checkboxes based on countries that is populated with a loop through a countries array. On select of a checkbox, I fire off the following code. Again, the goal here is to add multiple things to the array of terms to filter by (so I want to filter by location + name).
filterDataResults(term: string) {
var indexOfTerm = this.filteredByTerms.indexOf(term);
// if the term is not in an array of terms to filter by, add it
if (indexOfTerm === -1) {
this.filteredByTerms.push(term);
this.filteredDataResults = this.resultsFromAPI.filter(x => x.location ===
this.filteredByTerms.includes(term));
}
else {
this.filteredByTerms.splice(indexOfTerm, 1);
this.filteredDataResults = this.resultsFromAPI.filter(x => x.location ===
this.filteredByTerms.includes(term));
}
}
I don't know if I'm explaining this correctly but I've attached a picture to help. A series of checkboxes on the left, a data set on the right, and the checkboxes can be cumulative (so in the image example, if I select ITContractor and Clinical Psychology, the filter function would look for something in the results returned from the API which statifies both conditions.
It seems like some HOFs and map of filters might help you organize your user determined logic/filtering.
const filters = {
lastHourFilter: (result) => result.postedDate > Date.now() - ms('1 hour'),
last24HoursFilter: (result) => result.postedDate > Date.now() - ms('24 hours'),
...
itContractorFilter: generateSpecialismFilter('IT Contractor'),
clinicalPsychologyFilter: generateSpecialismFilter('Clinical Psychology'),
...
fullTimeFilter: generateJobTypeFilter('Full Time'),
temporaryFilter: generateJobTypeFilter('Temporary')
}
Then you inspect the check boxes and determine which filters you should apply to the results. Something like:
function applyFilters(results) {
Object.keys(filters).forEach((key) => {
if (checkboxes[key].checked) results =
results.filter(filters[key]);
});
return results;
}
Here checkboxes is a map of checkboxes in the DOM indexed by the same keys as your filters.
I built a custom component that filters an array of objects. The filter uses buttons, sets from active to non-active and allows more than one option on/off at the same time.
StackBlitz of my attempt - https://stackblitz.com/edit/timeline-angular-7-ut6fxu
In my demo you will see 3 buttons/options of north, south and east. By clicking on one you make it active and the result should include or exclude a matching "location" either north, south and east.
I have created my methods and structure to do the filtering, I'm struggling with the final piece of logic.
So far I have created a method to create an array of filtered locations depending on what the user clicks from the 3 buttons.
Next this passes to my "filter array" that gets the logic that should compare this filtered array against the original to bring back the array of results that are still remaining.
Its not quite working and not sure why - I originally got this piece of functionality working by using a pipe, but fore reasons do not want to go in that direction.
//the action
toggle(location) {
let indexLocation = this.filteredLocations.indexOf(location);
if (indexLocation >= 0) {
this.filteredLocations = this.filteredLocations.filter(
i => i !== location
);
} else {
this.filteredLocations.push({ location });
}
this.filterTimeLine();
}
// the filter
filterTimeLine() {
this.filteredTimeline = this.timeLine.filter(x =>
this.contactMethodFilter(x)
);
}
//the logic
private contactMethodFilter(entry) {
const myArrayFiltered = this.timeLine.filter(el => {
return this.filteredLocations.some(f => {
return f.location === el.location;
});
});
}
https://stackblitz.com/edit/timeline-angular-7-ut6fxu
Sorry for my expression but u have a disaster in your code. jajaja!. maybe u lost that what u need but the logic in your functions in so wrong. comparing string with objects. filter a array that filter the same array inside... soo u need make a few changes.
One:
this.filteredLocations.push({location});
Your are pushing object. u need push only the string.
this.filteredLocations.push(location);
Two:
filterTimeLine() {
this.filteredTimeline = this.timeLine.filter(x =>
this.contactMethodFilter(x)
);
}
in this function you filter the timeLine array. and inside of contactMethodFilter you call filter method to timeLine again....
See a functional solution:
https://stackblitz.com/edit/timeline-angular-7-rg7k3j
private contactMethodFilter(entry) {
const myArrayFiltered = this.timeLine.filter(el => {
return this.filteredLocations.some(f => {
return f.location === el.location;
});
});
}
This function is not returning any value and is passed to the .filter
Consider returning a boolean based on your logic. Currently the filter gets undefined(falsy) and everything would be filtered out
I have the following code in my controller:
$scope.filteredTransactions = $scope.invoiceTransactionsObject.transactions.concat(); // make a copy of the initial array
if ($scope.searchTerm.message)
{
var search = $scope.searchTerm.message;
$scope.filteredTransactions = $filter('filter')($scope.filteredTransactions, ({ message: search } || { item: search }));
}
I want to be able to filter by typing some value and search either in the message column or item column. According to How to filter multiple values (OR operation) in angularJS it should work, but it doesn't and if I type something that can be found in the message, it works, but when I type something from the item, it returns empty array.
Do you see where is my mistake?
Update Deleted irrelevant/mistaken initial answer
Since you're applying $filter inside a JS script, and it doesn't use any of the advanced features of $filter, I'd switch over to the JS-native method of filtering an array:
$scope.filteredTransactions = $scope.invoiceTransactionsObject.transactions.concat(); // make a copy of the initial array
if ($scope.searchTerm.message)
{
var search = $scope.searchTerm.message;
$scope.filteredTransactions = $scope.filteredTransactions.filter(function (trans) {
return trans.message.toLowerCase().indexOf(search) >= 0 || trans.item.toLowerCase().indexOf(search) >= 0;
});
}
...assuming that $filter is case-insensitive and matches anywhere in the string. If that's not the functionality of $filter and/or not what you want, you'd adjust the code accordingly.
Actually the question is related to typeahead bootstrap
because I need to define an array of values to show in input text by using autocomplete.
Anyway, the goal is just to define a function which read an array of objects and return an array of string.
Here is my code(1).
The goal of (1) is:
1) get an array of strings from an array of objects.
2) filter this array rejecting some elements.
It does not work because the element I want to reject persists in the array.
In fact in the autocomplete I get false value, actually it brokes the code because false is not expected by typeahead.
How should fix the code and maybe improve it?
(1)
element.typeahead({
source: function ( {
var users = _.map(app.userCollection.models, function (model) {
if (model.get('id') === app.currentUser.id) {
return false;
}
return model.get('first_name') + ' ' + model.get('last_name');
});
console.log(users); // [false, 'some name'];
_.reject(users, function(name) {
return name === false;
});
console.log(users); // [false, 'some name'];
// why does the false value persist?
return users;
}
});
Underscore methods don't usually operate on the array itself, but they return a new array, but I suggest checking the underscore docs on each function individually for confirmation. In this case we can safely assume that reject returns some new array, according to this sentence in the underscore docs:
Returns the values in list without the elements that the truth test (iterator) passes.
What you are currently doing is:
_.reject(users, function(name) {
return name === false;
});
So, you don't actually save your results anywhere. To preserve a reference to the array without the unwanted elements do this:
users = _.reject(users, function(name) {
return name === false;
});
That would yield the result you want, but let me give you a refactoring hint:
Use backbone's own methods as far as you can, it'll make for more readable code
source: function() {
// filter the collection down to the users you want
var users = app.userCollection.filter(function(model) {
return model.id === app.currentUser.id;
});
// -> users is now an array of models
// use map to transform each wanted user to a string giving its full name
users = _.map(users, function(user) {
return user.get('first_name')+' '+user.get('last_name');
});
// -> users is now an array of strings
return users;
}
Hope this helps!
I am using jqgrid in 'multiselect' mode and without pagination. When the user selects individual records by using mouse click, is there any way that I can bring those selected records to the top of the grid?
Thanks in advance for your help.
After small discussion with you in comments I could reformulate your question so: "how one can implement sorting by multiselect column?"
The question find is very interesting so I invested some time and could suggest a solution in case of jqGrid which hold local data (datatype which is not 'xml' or 'json' or which has 'loadonce: true' option).
First of all the working demo which demonstrate my suggestion you can find here:
The implementation consist from two parts:
Making selection as part of local data. As the bonus of the selection will be hold during paging of local data. This feature is interesting independent on the sorting by multiselect column.
The implementation of sorting by multiselect column.
To implement of holding selection I suggest to extend local data parameter, which hold local data with the new boolean property cb (exactly the same name like the name of the multiselect column). Below you find the implementation:
multiselect: true,
onSelectRow: function (id) {
var p = this.p, item = p.data[p._index[id]];
if (typeof (item.cb) === "undefined") {
item.cb = true;
} else {
item.cb = !item.cb;
}
},
loadComplete: function () {
var p = this.p, data = p.data, item, $this = $(this), index = p._index, rowid;
for (rowid in index) {
if (index.hasOwnProperty(rowid)) {
item = data[index[rowid]];
if (typeof (item.cb) === "boolean" && item.cb) {
$this.jqGrid('setSelection', rowid, false);
}
}
}
}
To make 'cb' column (multiselect column) sortable I suggest to do following:
var $grid = $("#list");
// ... create the grid
$("#cb_" + $grid[0].id).hide();
$("#jqgh_" + $grid[0].id + "_cb").addClass("ui-jqgrid-sortable");
cbColModel = $grid.jqGrid('getColProp', 'cb');
cbColModel.sortable = true;
cbColModel.sorttype = function (value, item) {
return typeof (item.cb) === "boolean" && item.cb ? 1 : 0;
};
UPDATED: The demo contain a little improved code based on the same idea.
If you have the IDs of the row(s) you can do a special sort on server side by using following command for e.g. MySQL:
Select a,b,c
FROM t
ORDER BY FIND_IN_SET(yourColumnName, "5,10,44,29") DESC
or
ORDER BY FIELD(yourColumnName, "5") DESC