Tablesorter can't apply search criteria on updateComplete - javascript

I've got a "tablesorter table" containing a category names in the first column. I filter the rows using shortcut buttons which trigger the specific "search" actions depending on which one is active:
function do_filtering() {
var category = $('#categories .active a').attr('data-filter-text');
var columns = [];
columns[0] = category;
$('table').trigger('search', [columns]);
}
Contents of the table may change. Until now I triggered on the table two events: "updateCell" and "search" and it got properly updated.
I need to separate these calls:
"updateCell" would be triggered by the cell editor (attached by a "sorter" for that specific columns' content)
"search" would be triggered after the cell is updated.
Unfortunately filtering in the "updateComplete" handler doesn't work, please see: http://jsfiddle.net/8cg4f/352/
How can I retain the search criteria after updating the table?

Well, the problem seems to be that since the filter inputs are empty, the filter widget is showing all rows after the update. I have two solutions, the second one probably being the better one because the first method causes flickering:
1) Add a setTimeout in the callback, with a time of 1 millisecond (demo):
$('td').click(function () {
resort = false;
$("table").trigger('updateCell', [$(this).closest('td'), resort, function(table){
setTimeout(function(){ do_filtering(); }, 1);
}]);
});
2) Update the filters, and hide the filter row (demo):
CSS
.tablesorter-filter {
display: none;
}
Code
function do_filtering() {
var category = $('#categories .active a').attr('data-filter-text');
$('.tablesorter-filter:first').val(category).trigger('search');
}
$('td').click(function () {
resort = false;
$("table").trigger('updateCell', [$(this).closest('td'), resort, function(table){
do_filtering();
}]);
});

Related

Checkboxes that display results with jQuery

I am trying to have the user check the boxes they are interested in getting resources for and then click the button to get a list of those resources that are hyperlinked to those resources. The hyperlinks (ul id="results” in HTML) are hidden until they called upon by the button “Get Resources”.
Plus I would like to add text to it before results saying “You have indicated an interest in:” (line break) then a listing the hyperlinks (line break) “Please click on the links to learn more”. If no check box is selected the div id=“alert” displays, which I got to work.
I think I am very close, I just can’t seem to get the list of resources.
Here is a link to my coding:
JSFiddle Code sample
$(document).ready(function() {
$('#alert').hide();
$('#results > li').hide();
/* Get the checkboxes values based on the parent div id */
$("#resourcesButton").click(function() {
getValue();
});
});
function getValue(){
var chkArray = [];
/* look for all checkboxes that have a parent id called 'checkboxlist' attached to it and check if it was checked */
$("#checkBoxes input:checked").each(function() {
chkArray.push($(this).val());
});
/* we join the array separated by the comma */
var selected;
selected = chkArray.join(',') + ",";
/* check if there is selected checkboxes, by default the length is 1 as it contains one single comma */
if(selected.length > 1){
// Would like it to say something before and after what is displayed
$('#results > li.' + $(this).attr('value')).show();
} else {
$('#alert').show();
}
}
I'd ditch the selected variable and just check the chkArray contents against the list item classes like:
function getValue() {
var chkArray = [];
/* look for all checkboxes that have a parent id called 'checkboxlist' attached to it and check if it was checked */
$("#checkBoxes input:checked").each(function () {
chkArray.push($(this).val());
});
$('#results li').each(function () {
if ($.inArray($(this).attr('class'), chkArray) > -1) $(this).show()
else($(this).hide())
})
/* check if there is selected checkboxes, by default the length is 1 as it contains one single comma */
if (!chkArray.length) {
$('#alert').show();
//alert("Please at least one of the checkbox");
}
}
jsFiddle example
I found a straightforward way of achieving what you want. DEMO: https://jsfiddle.net/erkaner/oagc50gy/8/
Here is my approach: I looped through all checkboxes. This way I could get the index of the current item in the original list, i, and use this index to display the corresponding item in the second list. I filter the checked items by using .is(':checked') condition, and then added them item to the array:
function getValue() {
var chkArray = [];
$("#checkBoxes input").each(function (i) {//now we can get the original index anytime
if($(this).is(':checked')){//is the item checked?
chkArray.push($(this).val());//if so add it to the array
var selected;
selected = chkArray.join(", ");
if (selected.length) {
$('#results').find('li').eq(i).show();//show the corresponding link by using `i`
} else {
$('#alert').show();
}
}
});
}
Last thing in your $(document).ready function, add:
$("#checkBoxes input:checkbox").click(function() {
$('li.' + $(this).val().replace(/ /g, '.')).show()
});
JSFiddle
Explanation:
On document ready, add a click handler to the checkboxes that shows the corresponding hidden list item below. The tricky thing here is the spaces in the list names. This makes each word a separate classname, so simply combine the list names with a dot . which results in a sequential classname call in jQuery.
By using <li class="Fitness & Recreation"> as a list item classname, you are giving this item 3 classnames: Fitness, &, and Recreation. In jQuery you select elements with multiple classnames by including each name preceded by a dot .. For example, selecting a list item element with the classnames foo, bar, and baz:
$('li.foo.bar.baz').show()
In the case of <li class="Fitness & Recreation">:
$('li.Fitness.&.Recreation').show()
Since these values are stored in the value attribute of the checkboxes we use jQuery to pull these values: $(this).val(), replace the spaces with dots: .replace(/ /g, '.'), and concatenate the result to the li. portion to access the appropriate list item.

JQuery Datatables search within input and select

Using Jquery Datatables with inputs and selects as shown here: http://datatables.net/examples/api/form.html
or if I used a custom column render handler to produce the input and selects how can I make the global table search work?
If you view the example you'll notice that only the first column, the read only one, is included in the search, what can I do to include the other columns in the search?
If you view the example in the link in my question and type "Tokyo" into the search all rows are returned. This is because "Tokyo" is an option in all dropdowns. I would want only rows with Tokyo selected to show. If you type in "33" you see no rows even though the first row has a value of "33" in the first column.
I can't seem to find any documentation on how to define what the search value is for a particular cell in a datatable.
It is not very well documented. And it seems to work differently, or not work at all, between (sub)versions. I think dataTables is intended to automatically detect HTML-columns, but for some reason, most of the times, it doesnt. The safest way is to create your own search-filter :
$.fn.dataTableExt.ofnSearch['html-input'] = function(value) {
return $(value).val();
};
This will return 33 on <input>'s with value 33, and Tokyo on <select>'s where Tokyo is selected. Then define the desired columns as of type html-input ;
var table = $("#example").DataTable({
columnDefs: [
{ "type": "html-input", "targets": [1, 2, 3] }
]
});
see demo based on http://datatables.net/examples/api/form.html -> http://jsfiddle.net/a3o3yqkw/
Regarding live data: The issue is, that the type based filter only is called once. dataTables then caches the returned values so it not need to "calculate" all the values over and over. Luckily, dataTables 1.10.x has a built-in function for cells, rows and pages called invalidate that forces dataTables to reset the cache for the selected items.
However, when dealing with <input>'s there is also the problem, that editing the value not is changing the value attribute itself. So even if you call invalidate(), you will still end up in filtering on the old "hardcoded" value.
But I have found a solution for this. Force the <input>'s value attribute to be changed with the <input>'s current value (the new value) and then call invalidate :
$("#example td input").on('change', function() {
var $td = $(this).closest('td');
$td.find('input').attr('value', this.value);
table.cell($td).invalidate();
});
For textareas use text() instead :
$("#example td textarea").on('change', function() {
var $td = $(this).closest('td');
$td.find('textarea').text(this.value);
table.cell($td).invalidate();
});
This is also the case when dealing with <select>'s. You will need to update the selected attribute for the relevant <option>'s and then invalidate() the cell as well :
$("#example td select").on('change', function() {
var $td = $(this).closest('td');
var value = this.value;
$td.find('option').each(function(i, o) {
$(o).removeAttr('selected');
if ($(o).val() == value) $(o).attr('selected', true);
})
table.cell($td).invalidate();
});
forked fiddle -> http://jsfiddle.net/s2gbafuz/ Try change content of the inputs and/or the dropdowns, and search for the new values ...
If the point here is to search through all the inputs within a table based on the live values (and "regular" cells), you might want to build your own custom search ($.fn.DataTable.ext.search.push()):
//custom search function
$.fn.DataTable.ext.search.push((_,__,i) => {
//get current row
const currentTr = dataTable.row(i).node();
//look for all <input>, <select> nodes within
//that row and check whether current value of
//any of those contains searched string
const inputMatch = $(currentTr)
.find('select,input')
.toArray()
.some(input => $(input).val().toLowerCase().includes($('#search').val().toLowerCase()));
//check whether "regular" cells contain the
//value being searched
const textMatch = $(currentTr)
.children()
.not('td:has("input,select")')
.toArray()
.some(td => $(td).text().toLowerCase().includes($('#search').val().toLowerCase()))
//make final decision about match
return inputMatch || textMatch || $('#search').val() == ''
});
The complete DEMO of this approach you may find below:
const srcData = [{id:1,item:'apple',category:'fruit'},{id:2,item:'banana',category:'fruit'},{id:3,item:'goosberry',category:'berry'},{id:4,item:'eggplant',category:'vegie'},{id:5,item:'carrot',category:'vegie'}];
const dataTable = $('table').DataTable({dom:'t',data:srcData,columns:[{title:'Id',data:'id'},{title:'Item',data:'item',render:data=>`<input value="${data}"></input>`},{title:'Category',data:'category',render:data=>`<select>${['fruit', 'vegie', 'berry'].reduce((options, item) => options+='<option value="'+item+'" '+(item == data ? 'selected' : '')+'>'+item+'</option>', '<option value=""></option>')}</select>`}]});
$.fn.DataTable.ext.search.push((_,__,i) => {
const currentTr = dataTable.row(i).node();
const inputMatch = $(currentTr)
.find('select,input')
.toArray()
.some(input => $(input).val().toLowerCase().includes( $('#search').val().toLowerCase()));
const textMatch = $(currentTr)
.children()
.not('td:has("input,select")')
.toArray()
.some(td => $(td).text().toLowerCase().includes($('#search').val().toLowerCase()))
return inputMatch || textMatch || $('#search').val() == ''
});
$('#search').on('keyup', () => dataTable.draw());
<!doctype html><html><head><script type="application/javascript" src="https://code.jquery.com/jquery-3.3.1.min.js"></script><script type="application/javascript" src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script><link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css"></head><body><input id="search"></input><table></table></body></html>
This should search the entire table instead of specific column(s).
var table = $('#table').DataTable();
$('#input').on('keyup', function() {
table.search(this.val).draw();
});
Best thing to do here is just update the cell container to the new value from the input and keep the datatable data object sync with the UI input:
$("#pagesTable td input,#pagesTable td select").on('change', function () {
var td = $(this).closest("td");
dataTable.api().cell(td).data(this.value);
});
Replace you input by Textarea, and add the css below. It will make your textarea looks like an input.
textarea{
height: 30px !important;
padding: 2px;
overflow: hidden;
}

Modify function to filter by several data-attributes simultaneously

The function below allows users to filter products by data-attributes, and accommodates filtering by multiple values simultaneously. It does this by creating an array of the values selected, and when any of the values are clicked (in this case checked/unchecked) it hides all the items and then re-shows those that match the values in the updated array.
It works correctly when filtering for one data-attribute, but when combined to filter by more than one attribute it no longer shows all results matching any of the values and instead only shows results matching all the specified values.
I've posted a fiddle which demonstrates the problem here: http://jsfiddle.net/chayacooper/WZpMh/94/ All but one of the items have the values of both data-style="V-Neck" and data-color="Black" and they should therefore remain visible if either of the filters are selected, but if another value from a different data-attribute some of the items are hidden.
$(document).ready(function () {
var selected = [];
$('#attributes-Colors *').click(function () {
var attrColor = $(this).data('color');
var $this = $(this);
if ($this.parent().hasClass("active")) {
$this.parent().removeClass("active");
selected.splice(selected.indexOf(attrColor),1);
}
else {
$this.parent().addClass("active");
selected.push(attrColor);
}
$("#content").find("*").hide();
$.each(selected, function(index,item) {
$('#content').find('[data-color *="' + item + '"]').show();
});
return false;
});
$('#attributes-Silhouettes *').click(function () {
var attrStyle = $(this).data('style');
var $this = $(this);
if ($this.parent().hasClass("active")) {
$this.parent().removeClass("active");
selected.splice(selected.indexOf(attrStyle),1);
}
else {
$this.parent().addClass("active");
selected.push(attrStyle);
}
$("#content").find("*").hide();
$.each(selected, function(index,item) {
$('#content').find('[data-style *="' + item + '"]').show();
});
return false;
});
});
Both of your handlers are updating the selected array, but only one handler executes on a click. The first one if a color was (de)selected, the second if a style. Let's say you've clicked on "Black" and "Crew Neck". At that time your selected array would look like this: [ "Black", "Crew_Neck" ]. The next time you make a selection, let's say you click "Short Sleeves", the second (style) handler executes. Here's what is happening:
Short_Sleeves gets added to the selected array.
All of the items are hidden using $("#content").find("*").hide();
The selected array is iterated and items are shown again based on a dynamic selector.
Number 3 is the problem. In the above example, a style was clicked so the style handler is executing. Any items in the selected array that are colors will fail because, for example, no elements will be found with a selector such as $('#content').find('[data-style *="Black"]').show();.
I would suggest 2 things.
Keep 2 arrays of selections, one for color, one for style.
Combine your code to use only a single handler for both groups.
Here's a (mostly) working example.
Note that I added a data-type="color|style" to your .filterOptions containers to allow for combining to use a single handler and still know which group was changed.
Here's the full script:
$(document).ready(function () {
// use 2 arrays so the combined handler uses correct group
var selected = { color: [], style: [] };
// code was similar enough to combine to 1 handler for both groups
$('.filterOptions').on("click", "a", function (e) {
// figure out which group...
var type = $(e.delegateTarget).data("type");
var $this = $(this);
// ...and the value of the checkbox checked
var attrValue = $this.data(type);
// same as before but using 'type' to access the correct array
if ($this.parent().hasClass("active")) {
$this.parent().removeClass("active");
selected[type].splice(selected[type].indexOf(attrValue),1);
}
else {
$this.parent().addClass("active");
selected[type].push(attrValue);
}
// also showing all again if no more boxes are checked
if (attrValue == 'All' || $(".active", ".filterOptions").length == 0) {
$('#content').find('*').show();
}
else {
// hide 'em all
$("#content").find("*").hide();
// go through both style and color arrays
for (var key in selected) {
// and show any that have been checked
$.each(selected[key], function(index,item) {
$('#content').find('[data-' + key + ' *="' + item + '"]').show();
});
}
}
});
});
UPDATE: incorporating suggestions from comments
To make the handler work with checkboxes instead of links was a small change to the event binding code. It now uses the change method instead of click and listens for :checkbox elements instead of a:
$('.filterOptions').on("change", ":checkbox", function (e) {
// handler code
});
The "All" options "hiccup" was a little harder to fix than I thought it would be. Here's what I ended up with:
// get a jQuery object with all the options the user selected
var checked = $(":checked", ".filterOptions");
// show all of the available options if...
if (checked.length == 0 // ...no boxes are checked
|| // ...or...
checked.filter(".all").length > 0) // ...at least one "All" box is checked...
{
// remainder of code, including else block, unchanged
}
I also added an all class to the appropriate checkbox elements to simplify the above conditional.
Updated Fiddle

How to toggle checkbox selection rather than only selecting?

I currently am using this JavaScript code snippet to select 3 checkboxes at a time
$(document).ready(function() {
var $cbs = $('input:checkbox[name="select[]"]'),
$links = $('a[name="check"]');
$links.click(function() {
var start = $links.index(this) * 3,
end = start + 3;
$cbs.slice(start,end).prop("checked",true);
});
});
Currently this code only selects the checkboxes, however I was wondering if anyone knew how to modify it so that it toggles the checkbox selection on and off?
Here's an example of my current code: "jsfiddle" - click the 1-3, 4-6 links etc to check the checkboxes.
Make the second argument to the prop("checked", ...) call depend on the "checked" status of the first (or other) checkbox in the slice:
// ...
$cbs.slice(start,end).prop("checked", !$cbs.slice(start).prop("checked"));
Here's an updated jsFiddle.
[Edit] Or to update each checkbox in the slice individually:
// ...
$cbs.slice(start,end).each(function() {
var $this = $(this);
$this.prop("checked", !$this.prop("checked"));
});
http://jsfiddle.net/ShZNF/3/
$cbs.slice(start,end).each(function() {
if ($(this).is(":checked")) {
$(this).removeProp("checked");
} else {
$(this).prop("checked",true);
}
});
http://jsfiddle.net/ShZNF/1/
Edit: Maeric's solution is better. I wasn't aware removeProp had this gotcha:
Note: Do not use this method to remove native properties such as
checked, disabled, or selected. This will remove the property
completely and, once removed, cannot be added again to element. Use
.prop() to set these properties to false instead.

How do I manipulate a jqGrid's search/filters?

I have a jqGrid with a navBar that has search: true and multipleSearch: true. I would like to add a button to my UI that automatically adds an additional rule to the search.
I've tried manipulating the postData for the filter directly, but values added this way don't show up in the search UI.
I've also tried accessing the search box directly using jQuery, like this:
$('#fbox_list').searchFilter().add();
$('#fbox_list .sf .data input').each(function(index) {
alert($(this).val());
});
But, in addition to feeling hackish, it only works if the user has already clicked on the search button (the fbox_list div is not constructed on load).
Has anyone else dealt with an issue like this?
For the sake of posterity, here is the hack I'm currently using. The grid has an ID of list and the pager has an ID of pager:
jQuery(document).ready(function() {
//Initialize grid.
//Initialize the navigation bar (#pager)
//Hack to force creation of the search grid.
//The filter's ID is of the form #fbox_<gridId>
jQuery('#pager .ui-icon-search').click();
jQuery('#fbox_list').searchFilter().close();
//Example button events for adding/clearing the filter.
jQuery("#btnAddFilter").click(function() {
//Adds a filter for the first column being equal to 'filterValue'.
var postFilters = jQuery("#list").jqGrid('getGridParam', 'postData').filters;
if (postFilters) {
$('#fbox_list').searchFilter().add();
}
var colModel = jQuery("#list").jqGrid('getGridParam', 'colModel');
//The index into the colModel array for the column we wish to filter.
var colNum = 0;
var col = colModel[colNum];
$('#fbox_list .sf .fields select').last().val(col.index).change();
$('#fbox_list .sf .data input').last().val('filterValue');
$('#fbox_list .sf .ops select.field' + colNum).last().val('eq').change();
$('#fbox_list').searchFilter().search();
});
jQuery("#btnClearFilter").click(function() {
$('#fbox_list').searchFilter().reset();
});
});
If you mean the filter toolbar, you can do this: (status is the col name -- so, replace "#gs_status" w/ "#gs_" + your_col_name
jQuery("#distributor_grid").jqGrid('showCol',['status']);
jQuery(".ui-search-toolbar #gs_status")
.val('ALL')
;
$('#distributor_grid').RefreshData(); // triggers toolbar
to clear inputs, selects and reset grid
$("td#refresh_navGrid").click();

Categories