I'm working on a web game and need to check for which cells on the table have been selected by the user. Right now I'm just checking for the row and cell index value:
JavaScript
function checkForWin() {
var card = document.getElementById('card');
if ((card.rows[0].cells[0].marker && // 1st row
card.rows[0].cells[1].marker &&
card.rows[0].cells[2].marker &&
card.rows[0].cells[3].marker &&
card.rows[0].cells[4].marker)) {
youWin();
} else {
noWin();
}
}
Is there a more elegant of doing this with jQuery?
Just make some loop :
function checkForWin() {
var card = document.getElementById('card');
var win = true;
for (var i = 0; i < card.rows[0].cells.length; i++){
if(!card.rows[0].cells[i])
win = false;
}
if(win)
youWin();
else
noWin();
}
Using jQuery you could iterate over the list of marked cells or just get the list of marked cells like this:
var marked = $('#cards td.marked');
// If you have a special way to detect a cell is marked that
// needs more custom test than checking the class you can use .filter.
// Just as example I use the same condition.
//var marked = $('#cards td').filter(function () {
// return $(this).hasClass('marked');
//});
// If you want to just iterate the selected cells.
marked.each(function () {
var i = $(this).closest('tr').index();
var j = $(this).index();
console.log(i, j);
});
// If you want to the the array of selected cells.
var indexes = marked.map(function () {
return {
i: $(this).closest('tr').index(),
j: $(this).index()
};
}).get();
To make it easier I assumed that a cell with the marked class means a marked cell. However you can use the condition you want to get the list of marked cells.
See small demo
Related
I have two scripts which work perfectly when a page contains a single table. However, now I have need to put multiple tables on the same page which support the same functions.
I need some help converting these two scripts to work with multiple tables on the same page, while maintaining the same functionality.
The first script is called "TABLE DATA STATES".
The second script is called "SORT TABLE DATA".
Current JSBin:
https://jsbin.com/noyoluhasa/1/edit?html,js,output
// ===================================================================
// =================== TABLE DATA STATES =============================
// ===================================================================
// Answer to my question on Stackoverflow:
// http://stackoverflow.com/questions/33128718/change-data-attribute-on-click-of-html-elements
// JsFiddle: http://jsfiddle.net/pya9jzxm/14
// Get all rows into the array except the <thead> row
var tbody = document.querySelector('tbody');
var trs = tbody.querySelectorAll('tr');
var tr, index = 0, length = trs.length;
// Start the loop
for (; index < length; index++) {
tr = trs[index];
// Set the attributes to default state
tr.setAttribute('data-state', 'enabled');
tr.setAttribute('data-display', 'collapsed');
tr.addEventListener('click',
function () {
// If its the row alphabet-label, skip it
if (this.classList.contains('alphabet-label')) {
return;
}
// Conditional logic to make the rows reset after clicking away from highlighted row
var trIndex = 0, trLength = trs.length, hasExpanded = false;
var state = 'disabled';
if (tbody.querySelectorAll('[data-display="expanded"]').length > 0) {
hasExpanded = true;
state = 'enabled';
}
for (; trIndex < trLength; trIndex++) {
// Set all rows to disabled on click of any row
trs[trIndex].setAttribute('data-state', state);
// Reset the display of all rows
trs[trIndex].setAttribute('data-display', 'collapsed');
}
if (!hasExpanded) {
// Set the clicked row to active highlighted state
this.setAttribute('data-state', 'enabled');
this.setAttribute('data-display', 'expanded');
}
}
);
}
// ===================================================================
// =================== SORT TABLE DATA ===============================
// ===================================================================
// For reference:
// this.setAttribute('data-state', this.getAttribute('data-state').contains === "enabled" ? "disabled" : "enabled");
// Adds icon to clicked <th>
// VanillaJS version - opted for jquery.tablesorter plugin due to flexibility and ease of use
var thsort = document.querySelectorAll('th')
//console.log(thsort);
var sort, sortIndex = 0, sortlength = thsort.length;
for (; sortIndex < sortlength; sortIndex++) {
sort = thsort[sortIndex];
//console.log(sort);
// On click to sort table column, do this:
sort.addEventListener('click',
function () {
var rm, rmIndex = 0;
for (; rmIndex < sortlength; rmIndex++) {
rmsort = thsort[rmIndex];
// Remove sort icon from other <th> elements
rmsort.classList.remove('sort-key');
// Add sort icon to this <th>
this.classList.add('sort-key');
//console.log(rmsort);
// Conditional logic to switch asc desc label
var state = 'asc', prevState = 'desc', hasAsc, prevState;
if (this.classList.contains('asc')) {
hasAsc = true;
state = 'desc';
prevState = 'asc';
//console.log(prevState);
}
// Set all rows to disabled on click of any row
this.classList.add(state);
this.classList.remove(prevState);
//if (hasAsc) {
// // Set the clicked row to active highlighted state
// this.setAttribute('class', state);
//}
}
}
);
}
I tried wrapping my codes in this code, in addition to replacing instances of tbody with thisTable, but then the scripts only worked for the last occurence of the table:
var alltables = document.querySelectorAll('tbody')
console.log(alltables);
var thisTable, sortIndex = 0, sortlength = alltables.length;
for (; sortIndex < sortlength; sortIndex++) {
thisTable = alltables[sortIndex];
// original code here
}
So this is really just a scope issue. You are referencing tbody and this NodeList of trs in the event handlers but those values have changed over time because of multiple tables. When those handlers are called and it sees tbody it first checks if that variable is part of its current scope, which it isn't. So it checks the next scope up until it finds it. But what it finds is the last value of that variable as it changed over time.
The easiest way to fix this is to surround your original block of code in a function, to give it scope when its called, and call that function passing the current table to it for each table. Then the only thing that function has in its scope is the table we care about and every variable we create within that function like trs will be in the scope of only that specific function call.
Take a look at the code below and check out the fiddle and let me know if you have any questions about it. You can see I used your original idea of that loop of all tables only I found the tables based on the table class, queried the table for its tbody and passed that to our configureTable function.
Fiddle: https://jsfiddle.net/rbpc5vfu/
ConfigureTable Function:
function configureTable (tbody) {
var trs = tbody.querySelectorAll('tr');
var tr, index = 0,
length = trs.length;
// Start the loop
for (; index < length; index++) {
tr = trs[index];
// Set the attributes to default state
tr.setAttribute('data-state', 'enabled');
tr.setAttribute('data-display', 'collapsed');
tr.addEventListener('click',
function() {
// If its the row alphabet-label, skip it
if (this.classList.contains('alphabet-label')) {
return;
}
// Conditional logic to make the rows reset after clicking away from highlighted row
var trIndex = 0,
trLength = trs.length,
hasExpanded = false;
var state = 'disabled';
if (tbody.querySelectorAll('[data-display="expanded"]').length > 0) {
hasExpanded = true;
state = 'enabled';
}
for (; trIndex < trLength; trIndex++) {
// Set all rows to disabled on click of any row
trs[trIndex].setAttribute('data-state', state);
// Reset the display of all rows
trs[trIndex].setAttribute('data-display', 'collapsed');
}
if (!hasExpanded) {
// Set the clicked row to active highlighted state
this.setAttribute('data-state', 'enabled');
this.setAttribute('data-display', 'expanded');
}
}
);
}
// ===================================================================
// =================== SORT TABLE DATA ===============================
// ===================================================================
// For reference:
// this.setAttribute('data-state', this.getAttribute('data-state').contains === "enabled" ? "disabled" : "enabled");
// Adds icon to clicked <th>
// VanillaJS version - opted for jquery.tablesorter plugin due to flexibility and ease of use
var thsort = tbody.querySelectorAll('th');
//console.log(thsort);
var sort, sortIndex = 0,
sortlength = thsort.length;
for (; sortIndex < sortlength; sortIndex++) {
sort = thsort[sortIndex];
//console.log(sort);
// On click to sort table column, do this:
sort.addEventListener('click',
function() {
var rm, rmIndex = 0;
for (; rmIndex < sortlength; rmIndex++) {
rmsort = thsort[rmIndex];
// Remove sort icon from other <th> elements
rmsort.classList.remove('sort-key');
// Add sort icon to this <th>
this.classList.add('sort-key');
//console.log(rmsort);
// Conditional logic to switch asc desc label
var state = 'asc',
prevState = 'desc',
hasAsc, prevState;
if (this.classList.contains('asc')) {
hasAsc = true;
state = 'desc';
prevState = 'asc';
//console.log(prevState);
}
// Set all rows to disabled on click of any row
this.classList.add(state);
this.classList.remove(prevState);
//if (hasAsc) {
// // Set the clicked row to active highlighted state
// this.setAttribute('class', state);
//}
}
}
);
}
}
Initialize tables on load:
var alltables = document.querySelectorAll('.table');
var thisTable, sortIndex = 0, sortlength = alltables.length;
for (; sortIndex < sortlength; sortIndex++) {
thisTable = alltables[sortIndex];
var tbody = thisTable.querySelector('tbody');
configureTable(tbody);
}
As you can see I didn't really change much if you look. I just wrapped your original code in a function block. Then stole your loop from above, found all the tables, for each table found its tbody and called our new function passing the tbody to it. Voila. Scope!
I am doing the below to get certain nodes from a treeview followed by getting text from those nodes, filtering text to remove unique and then appending custom image to the duplicate nodes.
For this I am having to loop 4 times. Is there is a simpler way of doing this? I am worried about it's performance for large amount of data.
//Append duplicate item nodes with custom icon
function addRemoveForDuplicateItems() {
var treeView = $('#MyTree').data('t-TreeView li.t-item');
var myNodes = $("span.my-node", treeView);
var myNames = [];
$(myNodes).each(function () {
myNames.push($(this).text());
});
var duplicateItems = getDuplicateItems(myNames);
$(myNodes).each(function () {
if (duplicateItems.indexOf($(this).text()) > -1) {
$(this).parent().append(("<span class='remove'></span>"));
}
});
}
//Get all duplicate items removing unique ones
//Input [1,2,3,3,2,2,4,5,6,7,7,7,7] output [2,3,3,2,2,7,7,7,7]
function getDuplicateItems(myNames) {
var duplicateItems = [], itemOccurance = {};
for (var i = 0; i < myNames.length; i++) {
var dept = myNames[i];
itemOccurance[dept] = itemOccurance[dept] >= 1 ? itemOccurance[dept] + 1 : 1;
}
for (var item in itemOccurance) {
if (itemOccurance[item] > 1)
duplicateItems.push(item);
}
return duplicateItems;
}
If I understand correctly, the whole point here is simply to mark duplicates, right? You ought to be able to do this in two simpler passes:
var seen = {};
var SEEN_ONCE = 1;
var SEEN_DUPE = 2;
// First pass, build object
myNodes.each(function () {
var name = $(this).text();
var seen = seen[name];
seen[name] = seen ? SEEN_DUPE : SEEN_ONCE;
});
// Second pass, append node
myNodes.each(function () {
var name = $(this).text();
if (seen[name] === SEEN_DUPE) {
$(this).parent().append("<span class='remove'></span>");
}
});
If you're actually concerned about performance, note that iterating over DOM elements is much more of a performance concern than iterating over an in-memory array. The $(myNodes).each(...) calls are likely significantly more expensive than iteration over a comparable array of the same length. You can gain some efficiencies from this, by running the second pass over an array and only accessing DOM nodes as necessary:
var names = [];
var seen = {};
var SEEN_ONCE = 1;
var SEEN_DUPE = 2;
// First pass, build object
myNodes.each(function () {
var name = $(this).text();
var seen = seen[name];
names.push(name);
seen[name] = seen ? SEEN_DUPE : SEEN_ONCE;
});
// Second pass, append node only for dupes
names.forEach(function(name, index) {
if (seen[name] === SEEN_DUPE) {
myNodes.eq(index).parent()
.append("<span class='remove'></span>");
}
});
The approach of this code is to go through the list, using the property name to indicate whether the value is in the array. After execution, itemOccurance will have a list of all the names, no duplicates.
var i, dept, itemOccurance = {};
for (i = 0; i < myNames.length; i++) {
dept = myNames[i];
if (typeof itemOccurance[dept] == undefined) {
itemOccurance[dept] = true;
}
}
If you must keep getDuplicateItems() as a separate, generic function, then the first loop (from myNodes to myNames) and last loop (iterate myNodes again to add the span) would be unavoidable. But I am curious. According to your code, duplicateItems can just be a set! This would help simplify the 2 loops inside getDuplicateItems(). #user2182349's answer just needs one modification: add a return, e.g. return Object.keys(itemOccurance).
If you're only concerned with ascertaining duplication and not particularly concerned about the exact number of occurrences then you could consider refactoring your getDuplicateItems() function like so:
function getDuplicateItems(myNames) {
var duplicateItems = [], clonedArray = myNames.concat(), i, dept;
for(i=0;i<clonedArray.length;i+=1){
dept = clonedArray[i];
if(clonedArray.indexOf(dept) !== clonedArray.lastIndexOf(dept)){
if(duplicateItems.indexOf(dept) === -1){
duplicateItems.push(dept);
}
/* Remove duplicate found by lastIndexOf, since we've already established that it's a duplicate */
clonedArray.splice(clonedArray.lastIndexOf(dept), 1);
}
}
return duplicateItems;
}
Do you have to do anything special while passing in a dynamically created string as a clientID for document.getElementById?
I have a asp:gridview control that has a textbox column and a checkbox column. I added an onclick event to the checkboxes to set the textbox value of that row to the max value of all checked rows +1. I pass in the IDs of the grid and the controls of the row that was selected. I can getElementByID fine for these controls, but When I dynamically build the IDs of the other controls, I keep getting null, even though I know that the IDs are correct. My code is bellow.
function SetPriority(cbID, tbID, gridID) {
var cb = document.getElementById(cbID);
if (cb.checked) {
var tb = document.getElementById(tbID);
var grid = document.getElementById(gridID);
var maxv = 0;
for (var i = 0; i < grid.rows.length; i++) {
var indexID = 102 + i;
var cbClientID = 'LeaveInfo_pnlMain_wgbLeaveSummary_gridSubmitted_ct' + indexID + '_chkGroup';
var tbClientID = 'LeaveInfo_pnlMain_wgbLeaveSummary_gridSubmitted_ct' + indexID + '_txtPriority';
console.log("row" + i);
//just for example of how it should be working
console.log(cbID);
var cbx = document.getElementById(cbID);
console.log(cbx);
//get row checkbox
console.log(cbClientID);
var thisCB = document.getElementById(cbClientID);
console.log(thisCB);
//get row textbox
var thisTB = document.getElementById(tbClientID);
console.log(thisTB);
if (thisCB) {
if (thisCB.type == "checkbox") {
if (thisCB.checked) {
if (thisTB.value > maxv)
maxv = thisTB.value;
}
}
}
}
tb.value = parseInt(maxv) + 1;
}
}
Here is how its showing up in the console, where you can see the IDs for the first row are the same
For Those wondering about How I am calling the function, I am adding it on to a checkbox in a .net gridview control on row databind. It renders as follows:
<input id="LeaveInfo_pnlMain_wgbLeaveSummary_gridSubmitted_ctl02_chkGroup" type="checkbox" name="LeaveInfo$pnlMain$wgbLeaveSummary$gridSubmitted$ctl02$chkGroup" onclick="javascript:SetPriority('LeaveInfo_pnlMain_wgbLeaveSummary_gridSubmitted_ctl02_chkGroup','LeaveInfo_pnlMain_wgbLeaveSummary_gridSubmitted_ctl02_txtPriority','LeaveInfo_pnlMain_wgbLeaveSummary_gridSubmitted');">
The vb .net code to add the function is this...(on-_RowDataBound)
Dim chk As CheckBox = CType(e.Row.FindControl("chkGroup"), CheckBox)
Dim tb As TextBox = CType(e.Row.FindControl("txtPriority"), TextBox)
chk.Attributes.Add("onclick", String.Format("javascript:SetPriority('{0}','{1}','{2}');", chk.ClientID, tb.ClientID, gridSubmitted.ClientID))
No, you don't have to do anything special when dynamically building a string. A string in javascript is the same string whether it was built dynamically or specified directly in your code. If document.getElementById() is not working, then one of the following is likely the cause:
Your string isn't what you think it is so it doesn't match the target id.
Your DOM id isn't what you think it is.
You have multiple elements with the same id (not likely here because you won't get null)
You are calling getElementById() before the DOM is ready or before the desired elements have been added to the DOM.
In this case, it seems more likely that 1) or 2) are the issues here, but you don't show us any context to know whether 4) could be the problem.
Not 100% sure, but I think it could be a context issue. Try this:
function ( id ) {
var ID = document.getElementById;
this.id = id;
this.newvar = ID.call( document, this.id );
...
}
Also, this question may help you — it has a good explanation on context and assigning a var to getElementById Why can't I directly assign document.getElementById to a different function?
I couldnt figure out why my IDs that seemed identical were not. I will leave this question open for anyone to add insight on how to remedy this. I ended up just getting my elements by cell and not by ID.
function SetPriority(cbID, tbID, gridID) {
var cb = document.getElementById(cbID);
if (cb.checked) {
var tb = document.getElementById(tbID);
var grid = document.getElementById(gridID);
var maxv = 0;
if (grid.rows.length > 0) {
for (row = 1; row < grid.rows.length; row++) {
var thisCB = grid.rows[row].cells[5].childNodes[1];
if (thisCB == cb) {
continue;
}
var thisTB = grid.rows[row].cells[6].childNodes[1];
if (thisCB.type == "checkbox") {
if (thisCB.checked) {
if (thisTB.value > maxv)
maxv = thisTB.value;
}
}
}
}
tb.value = parseInt(maxv) + 1;
}
}
I am creating a program using JavaScript while a clicking of button it will select a seat and change its background color to green and at the same time the button value will be added to the text field and will toggle accordingly.
Issue: I am adding all the value to the text field using an array, which is successful but during toggling it cannot able to subtract the particular clicking button value from array.
Here I cannot able to use jQuery because this page is coming from a ajax-page load.
// JavaScript Document
var Cur_id;
var Cur_val;
function setId(id, value) {
Cur_id = id;
Cur_val = value;
var SeatVal = document.getElementById(id);
if (SeatVal.style.backgroundImage == "") {
SeatVal.style.backgroundImage = "url(\'themes/frontend/images/greenseat.png\')";
var txbtElementSeat = new Array(document.getElementById("selectedseat").value += Cur_val + ",");
} else if (SeatVal.style.backgroundImage == 'url("themes/frontend/images/greenseat.png")') {
SeatVal.style.backgroundImage = "url(\'themes/frontend/images/seat.png\')";
var txbtElementSeatnbg = document.getElementById("selectedseat").value;
removeSeat(txbtElementSeatnbg, Cur_val);
function removeSeat(txbtElementSeatnbg, value) {
for (var i = 0; i <= txbtElementSeatnbg.length; i++) {
if (txbtElementSeatnbg[i] == value) {
txbtElementSeatnbg.splice(i, 1);
break;
}
}
}
} else if (SeatVal.style.backgroundImage == 'url("themes/frontend/images/seat.png")') {
SeatVal.style.backgroundImage = "url(\'themes/frontend/images/greenseat.png\')";
var txbtElementseatnb = document.getElementById("selectedseat").value += Cur_val + ",";
}
}
Your main issue seems to be that you try to save an array into a textfield. That will actually work, but will convert the array to a string representation.
var a = document.getElementById('selectedseat').value; therefore loads the string representation of the array into a variable "a", not the array itself!
Why don't you use a variable in the outer context (not the local function scope of setId()!) to hold the array? Maybe somewhat like this:
// Create a variable for the array!
var selectedSeats = new Array();
// Build a function that will update the textfield.
// Call this, whenever the array gets changed!
function updateListOfSelectedSeats() {
document.getElementById('selectedseat').value = selectedSeats.join(',');
}
// Removes a seat from the list of selected seats
function removeSeatFromList(seat) {
for (var i = 0; i < selectedSeats.length; i++) {
if (selectedSeats[i] == seat) {
selectedSeats.splice(i, 1);
updateListOfSelectedSeats();
break;
}
}
}
// Now the function that reacts to clicks on a seat
function setId(id, value) {
var Seat = document.getElementById(id);
switch (Seat.style.backgroundImage) {
case 'url("themes/frontend/images/greenseat.png")':
// Seat is already selected and needs to be deselected
Seat.style.backgroundImage = 'url("themes/frontend/images/seat.png")';
removeSeatFromList(value);
break;
case '':
case 'url("themes/frontend/images/seat.png")':
// Seat is empty, select it!
Seat.style.backgroundImage = 'url("themes/frontend/images/greenseat.png")';
selectedSeats.push(value);
updateListOfSelectedSeats();
break;
}
}
To remove the seat from the list use this
//remove seat from list
function removeSeat(seatListElm, seatValue) {
var arr=seatListElm.value.split(',');
var p=arr.indexOf(seatValue);
if(p!=-1){
arr.splice(p, 1);
seatListElm.value=arr.join(',');
}
}
seatListElm would be the element that hold "b5,c7,d5,c2"
seatValue would be something like this "c7"
Working demo code: JSFIDDLE
I have two Select lists, between which you can move selected options. You can also move options up and down in the right list.
When I move options back over to the left list, I would like them to retain their original position in the list order, even if the list is missing some original options. This is solely for the purpose of making the list more convenient for the user.
I am currently defining an array with the original Select list onload.
What would be the best way to implement this?
You can store the original order in an array, and when inserting back, determine what's the latest element in the array that precedes the one to be inserted AND matches what's currently in the select list. Then insert after that.
A better solution is to just store the old array whole and re-populate on every insertion with desired elements as follows (warning: code not tested)
function init(selectId) {
var s = document.getElementById(selectId);
select_defaults[selectId] = [];
select_on[selectId] = [];
for (var i = 0; i < s.options.length; i++) {
select_defaults[selectId][i] = s.options[i];
select_on[selectId][i] = 1;
var value = list.options[i].value;
select_map_values[selectId][value] = i if you wish to add/remove by value.
var id = list.options[i].id; // if ID is defined for all options
select_map_ids[selectId][id] = i if you wish to add/remove by id.
}
}
function switch(selectId, num, id, value, to_add) { // You can pass number, value or id
if (num == null) {
if (id != null) {
num = select_map_ids[selectId][id]; // check if empty?
} else {
num = select_map_values[selectId][value]; // check if empty?
}
}
var old = select_on[selectId][num];
var newOption = (to_add) : 1 : 0;
if (old != newOption) {
select_on[selectId][num] = newOption;
redraw(selectId);
}
}
function add(selectId, num, id, value) {
switch(selectId, num, id, value, 1);
}
function remove(selectId, num, id, value) {
switch(selectId, num, id, value, 0);
}
function redraw(selectId) {
var s = document.getElementById(selectId);
s.options.length = 0; // empty out
for (var i = 0; i < select_on[selectId].length; i++) {
// can use global "initial_length" stored in init() instead of select_on[selectId].length
if (select_on[selectId][i] == 1) {
s.options.push(select_defaults[selectId][i]);
}
}
}
I would assign ascending values to the items so that you can insert an item back in the right place. The assigned value stays with the item no matter which list it's in.