Recursive Checkbox Selection in Treeview | Telerik MVC - javascript

I recently encountered an issue dealing with the Telerik Treeview component and handling recursive selection with checkboxes when handling larger data sets (500-1000+ nodes).
With smaller data sets (100-300 nodes) the Treeview and its selection methods work as follows (as they should):
Initially - all nodes are selected.
Clicking a parent node toggles selection of all of the child nodes.
Unselecting a single child node will unselect the parent (and any grandparent / top-level nodes)
Most of these things I believe are fairly commonplace when dealing with Treeviews and selection. The current method that is being used isn't the cleanest and calls an unnecessary amount of additional events to be fired during the selection process.
I was just curious if anyone had handled an issue similar to this before I began tearing apart the current code (available below).
Existing Selection Code:
$('#TreeView').find("li").find('> div > .t-checkbox :checkbox').bind('click', function (e) {
var isChecked = $(e.target).is(':checked');
var treeView = $($(e.target).closest('.t-treeview')).data('tTreeView');
var item = $(e.target).closest('.t-item');
var checkboxes = item.find('.t-checkbox :checkbox');
$.each(checkboxes, function (index, checkbox) { $(checkbox).attr('checked', isChecked ? true : false); treeView.checkboxClick(e, checkbox); });
var siblings = item.parent().find('> li .t-checkbox');
var siblingsLength = siblings.length;
var checkedLength = siblings.find(':checked').length;
if (siblingsLength == checkedLength) {
var parentCheckBox = item.parent().closest('.t-item').find('> div .t-checkbox :checkbox');
var grandparentCheckBox = item.parent().parent().parent().closest('.t-item').find('> div .t-checkbox :checkbox');
parentCheckBox.attr('checked', true)
grandparentCheckBox.attr('checked', true)
treeView.checkboxClick(e, parentCheckBox)
treeView.checkboxClick(e, grandparentCheckBox)
}
else {
var parentCheckBox = item.parent().closest('.t-item').find('> div .t-checkbox :checkbox');
var grandparentCheckBox = item.parent().parent().parent().closest('.t-item').find('> div .t-checkbox :checkbox');
parentCheckBox.attr('checked', false)
grandparentCheckBox.attr('checked', false)
treeView.checkboxClick(e, parentCheckBox)
treeView.checkboxClick(e, grandparentCheckBox)
}
});

I found a solution that I feel will function at least effectively as of right now and works a great deal faster than the existing solution, even when dealing with very large data sets.
I created a simple function that fires upon the Checked event within the TreeView and handles all the necessary child selection:
function TreeView_Checked(e) {
var current = $(e.item).find(':checkbox:eq(0)');
//Check or uncheck all child nodes from this one
var children = $(e.item).find(':checkbox');
current.is(':checked') ? children.attr('checked', 'checked') : children.removeAttr('checked');
}
which is implemented by adding the following within the TreeView declaration:
TreeView().Name("TreeView").ClientEvents(e => e.OnChecked("TreeView_Checked"))

This code will select or deselect the parent along with all of its children. If you want the grand-parent, you can probably do .closest(".t-item")
$(document).ready(function () {
$("#btnSelectChildren").click(function () {
nodeCheck(true);
});
$("#btnDeselectChildren").click(function () {
nodeCheck(false);
});
});
function nodeCheck(isChecked) {
var treeView = $('#TreeView').data('tTreeView');
var selected = $('#TreeView .t-state-selected');
treeView.nodeCheck(selected, isChecked);
selected.closest('li').find(".t-item").each(function () {
treeView.nodeCheck($(this), isChecked);
});
}

Related

How do I target siblings in vanilla javascript?

I'm creating a toggle review form that displays when you hit Edit, but I only want this to work on on the review the button is a sibling of, as opposed to all review edit forms on the page (as there may be multiple).
I know how to do this in jquery but am trying to keep this project all vanilla js.
const button = document.querySelector('.toggle-edit-form');
const form = document.querySelector('.edit-review-form');
button.onclick = function() {
// toggle the edit button text on click
button.innerHTML === 'Edit' ? button.innerHTML = "Cancel" : button.innerHTML = "Edit";
// toggle visibility of edit review form
form.classList.toggle('toggle')
};
I could use nextSibling I suppose (as it is currently the next sibling) but would prefer a solution that won't break the code if change the order / html.
Thanks!
#Mike you can try creating a function that uses a while loop to keep track of siblings , I hope this helps
var getSiblings = function (elem) {
// Setup siblings array and get the first sibling
var siblings = [];
var sibling = elem.parentNode.firstChild;
// Loop through each sibling and push to the array
while (sibling) {
if (sibling.nodeType === 1 && sibling !== elem) {
siblings.push(sibling);
}
sibling = sibling.nextSibling
}
return siblings;
};

How do I get the shaded elements in a jstree checkbox?

I currently have a jstree in which I get the selection elements with the code:
var selectedElmsIds = [];
var selectedElms = $('#PermisosjsTree').jstree("get_selected", true);
$.each(selectedElms, function () {
selectedElmsIds.push(this.id);
console.log(this.id);
});
but I need to obtain the overlaid elements (marked with red below), how do I get them?
The 'parents' property of the node should give you the parent and its ancestors.
$.each(selectedElms, function () {
selectedElmsIds.push(this.id);
console.log(this.id);
console.log(this.parents);
});

hover(): Trigger only when leaving foreign element

am constructing a slightly more complex drop down menu system using Jquery's slideDown() and slideUp() animations as well as the "hover()" event.
Now I have a certain element which triggers by "hover()", that another element is being displayed. Unfortunately it's not possible to make those two elements, the only childs of another element (since the trigger is in another table).
Still I want this new element which has been displayed, to show until my mouse leaves BOTH the new element as well as the trigger element.
Is there a way to achieve this?
Thanks in advance :)
I used .mouseenter and .mouseleave to achieve what may you want:
jsFiddle
var groups = {};
groups[1] = {
main: false,
sub: false
};
$('.menu').mouseenter(function(e) {
var $target = $(e.target);
var group = $target.attr('data-group');
var type = $target.attr('data-type');
if (!(groups[group].sub || groups[group].main)) {
$('.sub[data-group='+ group +']').toggle(true);
}
groups[group][type] = true;
});
$('.menu').mouseleave(function(e) {
var $target = $(e.target);
var group = $target.attr('data-group');
var type = $target.attr('data-type');
groups[group][type] = false;
if (!(groups[group].sub || groups[group].main)) {
$('.sub[data-group='+ group +']').toggle(false);
}
});
Just track the group of main and sub item. A little ugly, but hope it may helps.

High performance selectable cells in large table - IE6

I am working on an application with a firm business requirement to display an html table with up to 60 rows and up to 50 columns.
Ideally, users would be able to select individual table cells, or click and drag to select multiple cells.
My problem is that I'm limited to using IE6 at the moment, and I have been having real trouble finding (or coding) a way to allow for this kind of selection on this many cells without severe performance degradation.
My current method looks basically like this:
$(document).ready(function() {
var selecting = false;
var colStart, rowStart;
var tableContainer = $("#tableContainer")
tableContainer.delegate("td", "mousedown", function() {
//Clear Selection
tableContainer.find("td.selected").removeClass("selected");
$(this).addClass("selected");
colStart = $(this).index();
rowStart = $(this).parents("tr").index();
selecting = true;
}).delegate("td", "mouseover", function() {
if (selecting) {
//Clear Selection
tableContainer.find("td.selected").removeClass("selected");
var theCell = $(this);
// Get the row and column numbers of the current cell
var colEnd = theCell.index();
var rowEnd = theCell.parents("tr").index();
// Account for rowEnd being smaller than rowStart
var rowSliceStart = Math.min(rowStart, rowEnd);
var rowSliceEnd = Math.max(rowStart, rowEnd);
tableContainer.find("tr").slice(rowSliceStart, rowSliceEnd + 1).each(function() {
var colSliceStart = Math.min(colStart, colEnd);
var colSliceEnd = Math.max(colStart, colEnd);
// Add the required class to the children
$(this).children().slice(colSliceStart, colSliceEnd + 1).addClass("selected");
});
}
}).delegate("td", "mouseup", function() {
selecting = false;
});
});​
Does anybody have any suggestions for a method to improve the performance of this function? I believe the adding/removing of classes is taking up most of the performance overhead, so I'm especially hoping to find efficiencies there.
Tables are the overhead themselves, especially when they contain lots of stuff. Tables also render only when they are complete. Consider paginations if possible.
Constant DOM manipulations, repaints (change appearance) and reflows (change in dimensions) is also an overhead.
IE6 itself wasn't built to do heavy JS operations. IE6 is what? 10 years old? What was JS 10 years ago? validations and pop-ups right?
Repeated function calls. in jQuery, it's best to cache the value of function calls like the $(this) instead of calling it repeatedly.
As what I understand in your code, you are running $.each(), slice and some random math during mouseover. That's heavy.
consider using a newer jQuery
Also, I have cleaned a bit of your code:
$(function() {
var selecting = false,
tableContainer = $("#tableContainer"),
colStart, rowStart;
tableContainer.on("mousedown", 'td', function() {
var $this = $(this); //reference this
colStart = $this.index();
rowStart = $this.closest("tr").index(); //use closest instead of parents to avoid going up to root
$(".selected", tableContainer).removeClass("selected"); //context instead of find
$this.addClass("selected");
selecting = true;
}).on("mouseover", 'td', function() {
if (selecting) {
var theCell = $(this),
colEnd = theCell.index(),
rowEnd = theCell.closest("tr").index(), //use closest
rowSliceStart = Math.min(rowStart, rowEnd),
rowSliceEnd = Math.max(rowStart, rowEnd);
$(".selected", tableContainer).removeClass("selected");
$("tr", tableContainer).slice(rowSliceStart, rowSliceEnd + 1).each(function() {
var colSliceStart = Math.min(colStart, colEnd),
colSliceEnd = Math.max(colStart, colEnd);
$('> *', this).slice(colSliceStart, colSliceEnd + 1).addClass("selected"); //using selector to get children instead of $(this).children()
});
}
}).on("mouseup", 'td', function() {
selecting = false;
});
});​
It doesn't actually look too bad. The only thing I could think of off the top of my head would be to calculate just the deltas on mouseover. That is, store the previous start and end columns/rows and on the next mouseover event, update the classes of just the elements which have changed.
Other minor things:
cache $(this) in the mousedown handler
I'm not 100% sure about this one for IE6, but you could try changing the selector from .find('td.selected') to just .find('.selected'). The first has two conditions to check, vs just one. In modern browsers, the second would definitely be faster, since jQuery can leverage getElementsByClassName, but that doesn't exist in IE6, so who knows?
You could also experiment with making a more targetted selector, especially if the contents of the cells contain further DOM elements. .find('> tr > .selected')
throttle the mouseover handler.

confusion in jquery parents selector with hasClass function

var allChecked = $('.inboxCheckbox:checked');
if(allChecked.length > 0){
var messageIds = new Array();
var parentRow = null;
allChecked.each(
function(){
parentRow = $(this).parents('tr');
if(!(parentRow.hasClass('gradeA'))){
parentRow.addClass('gradeA');
increaseUnreadMessage();
}
parentRow = null;
messageIds.push($(this).val());
}
);
}else{
showInsMessage('<b class="redTxt">Please Select At Least One Message</b>');
}
i have multiple rows with once checkbox in each row... i was trying to add class gradeA to each row if checkbox is checked.... i do not want to call addClass if it already has class gradeA.... when i select multiple rows then it adds class to only one row. does that mean
lets say i have three rows with checkbox in each row and i select each checkbox when i run
$(':checked').each(
$(this).parents('tr')
)does it select all the rows with checked boxes or only the specfic parent row.... my assuption was it only gives the specific parent row..... if it gives specific row then it should work .. but once i add a class to parent row and move to another row then parentRow.hasClass('gradeA') return true... i am confused now if it checks all the row with checkboxes then is there any way to select specific parent row......
Thanks for reading
Would be nice to see the markup, are there more tables nested?
However,
parentRow = $(this).closest('tr');
should be a better choice.
API says that .parents() method search through all ancestors of the elements.
.parent() travels only a single level up the DOM tree.
If your checkbox is a direct child (not a deep descendant) of 'tr' then you can try
parentRow = $(this).parent('tr');
Your code should work. I suspect that the problem is happening because your function increaseUnreadMessage() is throwing an error, which is causing the rest of the each() loop to be skipped.
But, to answer your specific question: yes, you can select all rows (<td>s) that contain checked checkboxes. Using jquery's :has selector, like this:
var allCheckedRows = $('tr:has(.inboxCheckbox:checked)');
from there, of course, you can just use addClass() to apply your classname to all of them:
allCheckedRows.addClass('gradeA');
of course, you've got other things going on in your each() loop, so you probably can't throw out the each() entirely. As I said above, your code works... but something like this might be cleaner, and easier to understand.
var messageIds = new Array();
var allCheckedRows = $('tr:has(.inboxCheckbox:checked)');
allCheckedRows.addClass('gradeA');
allCheckedRows.find('.inboxCheckbox').each( function() {
var cb = $(this);
increaseUnreadMessage();
messageIds.push( cb.val() );
});
if( messageIds.length === 0 ) {
showInsMessage('<b class="redTxt">Please Select At Least One Message</b>');
}
BTW, I think you can do it more jQuerish style :
var messageIds = [];
$('tr.youTrs').toggleClass('gradeA', function () {
var checkbox = $(this).find('.inboxCheckbox');
if (checkbox.is(':checked')){
messageIds.push(checkbox.val());
return true;
}
else{
showInsMessage('<b class="redTxt">Please Select At Least One Message</b>');
return false;
}
});

Categories

Resources