jQuery to find all previous elements that match an expression - javascript

Using jQuery, how do you match elements that are prior to the current element in the DOM tree? Using prevAll() only matches previous siblings.
eg:
<table>
<tr>
<td class="findme">find this one</td>
</tr>
<tr>
<td>find the previous .findme</td>
</tr>
<tr>
<td class="findme">don't find this one</td>
</tr>
</table>
In my specific case, I'll be searching for the first .findme element prior to the link clicked.

Ok, here's what I've come up with - hopefully it'll be useful in many different situations. It's 2 extensions to jQuery that I call prevALL and nextALL. While the standard prevAll() matches previous siblings, prevALL() matches ALL previous elements all the way up the DOM tree, similarly for nextAll() and nextALL().
I'll try to explain it in the comments below:
// this is a small helper extension i stole from
// http://www.texotela.co.uk/code/jquery/reverse/
// it merely reverses the order of a jQuery set.
$.fn.reverse = function() {
return this.pushStack(this.get().reverse(), arguments);
};
// create two new functions: prevALL and nextALL. they're very similar, hence this style.
$.each( ['prev', 'next'], function(unusedIndex, name) {
$.fn[ name + 'ALL' ] = function(matchExpr) {
// get all the elements in the body, including the body.
var $all = $('body').find('*').andSelf();
// slice the $all object according to which way we're looking
$all = (name == 'prev')
? $all.slice(0, $all.index(this)).reverse()
: $all.slice($all.index(this) + 1)
;
// filter the matches if specified
if (matchExpr) $all = $all.filter(matchExpr);
return $all;
};
});
usage:
$('.myLinks').click(function() {
$(this)
.prevALL('.findme:first')
.html("You found me!")
;
// set previous nodes to blue
$(this).prevALL().css('backgroundColor', 'blue');
// set following nodes to red
$(this).nextALL().css('backgroundColor', 'red');
});
edit - function rewritten from scratch. I just thought of a much quicker and simpler way to do it. Take a look at the edit history to see my first iteration.
edit again - found an easier way to do it!

Presumably you are doing this inside an onclick handler so you have access to the element that was clicked. What I would do is do a prevAll to see if it is at the same level. If not, then I would do a parent().prevAll() to get the previous siblings of the parent element, then iterate through those backwards, checking their contents for the desired element. Continue going up the DOM tree until you find what you want or hit the root of the DOM. This a general algorithm.
If you know that it is inside a table, then you can simply get the row containing the element clicked and iterate backwards through the rows of the table from that row until you find one that contains the element desired.
I don't think there is a way to do it in one (chained) statement.

edit: this solution works for both your original problem, the problem you mention in your first comment, and the problem you detail in the comment after that.
$('.myLinks').click(function() {
var findMe = '';
$(this).parents().each(function() {
var a = $(this).find('.findme').is('.findme');
var b = $(this).find('.myLinks').is('.myLinks');
if (a && b) { // look for first parent that
// contains .findme and .myLinks
$(this).find('*').each(function() {
var name = $(this).attr('class');
if ( name == 'findme') {
findMe = $(this); // set element to last matching
// .findme
}
if ( name == 'myLinks') {
return false; // exit from the mess once we find
// .myLinks
}
});
return false;
}
});
alert(findMe.text() ); // alerts "find this one"
});
this works for your example in the OP as well as a modified example as explained in the comments:
<table>
<tr>
<td class="findme">don't find this one</td>
</tr>
<tr>
<td class="findme">find this one</td>
</tr>
<tr>
<td>find the previous .findme</td>
</tr>
<tr>
<td class="findme">don't find this one</td>
</tr>
</table>
as well as this test case which you added:
<table>
<tr>
<td class="findme">don't find this one</td>
</tr>
<tr>
<td class="findme">don't find this one</td>
</tr>
<tr>
<td class="findme">find this one</td>
</tr>
</table>
find the previous .findme

I usually number elements (1,2,3..) (rel="number"), so then i use this code to give class to all previous elements:
var num = $(this).attr("rel");
for (var i = 1; i<=num; i++)
{
$('.class[rel="'+i+'"]').addClass("newclass");
}

had the same problem, heres what i came up with. my function uses compareDocumentPosition. dont know how it compares to the other solutions in terms of performance though.
$.fn.findNext = function ( selector ) {
var found, self = this.get(0);
$( selector )
.each( function () {
if ( self.compareDocumentPosition( this ) === 4 ){
found = this;
return false;
}
})
return $(found);
}
of course one could change this quite easily to fetch ALL elements following the calling element.
$.fn.nextALL= function ( selector ) {
var found = [], self = this.get(0);
$( selector )
.each( function () {
if ( self.compareDocumentPosition( this ) === 4 )
found.push(this);
})
return $(found);
}
EDIT: streamlined version
$.fn.findNext = function( s ){
var m = this[0], f=function(n){return m.compareDocumentPosition(n)===4;};
return this.pushStack( $(s).get().filter(f) );
}

Related

Use jQuery arguments in JavaScript function

I want to use a jQuery argument in a JavaScript function. At the moment, my code looks like this:
window.nextGen = function(cell) {
// code...
}
window.prepareNextGen = function() {
nextGen($('#grundtabelle').rows[1].cells[1]);
}
But that doesn't work. Any help?
Simple Fix
To access the table object rows and cells you can simply add an array index like so:
nextGen( $('#grundtabelle')[0].rows[1].cells[1] );
See this previous question for more detail: How to get a DOM Element from a JQuery Selector
Run the snippet to try
nextGen( $('#grundtabelle')[0].rows[1].cells[1] );
function nextGen( cell ) {
console.info( cell.innerHTML ); // displays B1
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
<table id="grundtabelle">
<tr>
<td>A0</td>
<td>B0</td>
</tr>
<tr>
<td>A1</td>
<td>B1</td>
</tr>
<tr>
<td>A2</td>
<td>B2</td>
</tr>
</table>
A jQuery object does not have a rows or cells property. Assuming you're trying to get the second td of the second tr (note that the indexes are zero-based, so the item with index of 1 is the second), then you need to use jQuery's DOM traversal methods. In this case, find() and :eq. Try this:
nextGen($('#grundtabelle').find('tr:eq(1) td:eq(1)'));
If the nextGen() function is expecting a DOMElement instead of a jQuery object then you can retrieve that from the jQuery object like this:
nextGen($('#grundtabelle').find('tr:eq(1) td:eq(1)')[0]);
// or:
nextGen($('#grundtabelle').find('tr:eq(1) td:eq(1)').get());

Optimizing javascript performance with many event listeners

I am currently building a site that allows searching for elements, with results added to a big table (think hundreds of results). Each element in the result table can be manipulated in various ways (custom javascript 0-5 star rating, toggling a fold-out panel for additional options etc.).
If I use the site on my android tablet, performance of the javascript part is very sluggish, so I am wondering how I can improve performance.
One option I have considered is to not bind any event listeners on the result rows except for a single mouse-enter event, and then binding the actual event listeners only when the mouse is over a given element.
Any other ideas to improve performance would be greatly appreciated.
Most of my javascript code is jquery based, so if you have any jquery specific optimizations I would appreciate that also.
You might look into javaScript event delegation. There are many answers on SO (an example here) and many good articles ( here or here ).
Basically the idea you had is actually a good solution. So, instead of binding - let's say - one hundred rows with their own event handlers, you bind only their common parent which will fire an event when any of its child will receive a mouse input.
Roughly speaking instead of this:
$('tr').on("click", function() {});
You will do this:
$('table').on('click', 'tr', function() {});
This is obviously a very simplified example, but it should be enough to build a good pattern upon.
As a side note, it's a very interesting thing (well, at least for me...) to inspect the event that is fired doing something like:
$('table').on('click', 'tr', function(evt) {
console.log(evt);
});
And see how much information an event carries, and how much useful information you get out of the box with a simple click or mouse enter.
VanillaJs
Of course the same result can be achieved without any library.
A simple implementation using Vanilla JS can be taken from David Walsh's article linked at the beginning of the answer, it goes roughly like this:
// Get the element, add a click listener...
document.getElementById("#myTable").addEventListener("click", function(e) {
// e.target is the clicked element.
// Check if it was a <tr>...
if(e.target && e.target.nodeName == "TR") {
// Element has been found, do something with it
}
});
If you try this code though chances are that the actual target.element is the <td>, and not the <tr> we want.
This means we have to be a bit more clever and walk the DOM upwards to see if the clicked element (<td>) is contained in the one we really want (<tr>).
A rough implementation to get you started would look like this:
function walk(startEl, stopEl, targetNodeName) {
if (!startEl || startEl === stopEl || startEl.nodeName === 'BODY') {
return false;
}
if (startEl.nodeName === targetNodeName) {
// Found it, return the element
return startEl;
}
// Keep on looking...
return walk(startEl.parentNode, stopEl, targetNodeName);
}
const container = document.getElementById('myTable');
container.addEventListener('click', (e) => {
const clickedElm = walk(e.target, container, 'TR');
console.log(clickedElm);
if (clickedElm) {
clickedElm.classList.add('clicked');
}
})
See this fiddle or the snippet below:
function walk(startEl, stopEl, targetNodeName) {
if (!startEl || startEl === stopEl || startEl.nodeName === 'BODY') {
return false;
}
if (startEl.nodeName === targetNodeName) {
return startEl;
}
return walk(startEl.parentNode, stopEl, targetNodeName);
}
const elem = document.getElementById('myTable');
elem.addEventListener('click', (e) => {
const clickedElm = walk(e.target, elem, 'TR');
console.log(clickedElm);
if (clickedElm) {
clickedElm.classList.add('clicked');
}
})
table {
width: 100%;
}
tr {
background: #ddd;
}
.clicked {
background: teal;
}
<table id="myTable">
<tr>
<td>one</td>
</tr>
<tr>
<td>two</td>
</tr>
<tr>
<td>three</td>
</tr>
<tr>
<td>four</td>
</tr>
<tr>
<td>five</td>
</tr>
<tr>
<td>six</td>
</tr>
</table>

How to check if the parent element of an element is in itself the last child with javascript or jquery

How can I check if the parent, with a certain data attribute, of the element I'm clicking is a 'last child' itself? Basically, bearing in mind that they gonna be generated, most of the <tbody>'s have a data attribute - data-state="closed" - and I wanna check if one of them is the last one when I click on a child element, which is a link, that's inside it.
JS/jquery:
$('body').delegate('a[data-view="showhide"]', 'click', function (e) {
var thisElem = $(this),
thisTbody = thisElem.closest('tbody');
e.preventDefault();
//check to see if this element's parent is a last child
if (thisTbody.filter('[data-state]').is(':last')) {
console.log('this is the last tbody of its kind called \'data-state\'');
//...Do something
}
});
HTML:
<table>
<tbody data-state="closed">
<tr><td>cell 1</td></tr>
</tbody>
<tbody data-state="closed">
<tr><td>cell 2</td></tr>
</tbody>
<tbody data-state="closed">
<tr><td>cell 3</td></tr>
</tbody>
<tbody>
<tr><td>cell not important</td></tr>
</tbody>
</table>
Many thanks
I'd probably use nextAll:
if (!thisTbody.nextAll('tbody[data-state]')[0]) {
// It's the last `tbody` with a `data-state` attribute in its
// enclosing table
}
Or if you know that all of the tbody elements that have a data-state attribute are next to each other (e.g., that last one that doesn't have one is always at the end), you could just use next('tbody[data-state]') rather than nextAll('tbody[data-state]'). But it doesn't really buy you much, and it adds the assumption.
You can get the number of tbody elements that have the data-attribute and then check the index of the current tbody element:
$(document).delegate('a[data-view="showhide"]', 'click', function (e) {
var thisElem = $(this),
thisTbody = thisElem.closest('tbody[data-state]'),
thisIndex = thisTbody.index(),//get the index of the currently selected `tbody` element
thisCount = thisElem.closest('table').find('tbody[data-state]').length;//get the number of `tbody` elements with the `data-state` attribute
e.preventDefault();
//see if the current index is equal to the total number of `tbody[data-state]` elements (remember that `.length` starts at one)
if (thisIndex == (thisCount - 1)) { /*It has been found that this is the last element*/ }
});
Docs for .index(): http://api.jquery.com/index
On a side-note, your code will perform faster if you use classes instead of data-attributes:
HTML--
<table>
<tbody class="closed">
<tr><td>cell 1</td></tr>
</tbody>
<tbody class="closed">
<tr><td>cell 2</td></tr>
</tbody>
<tbody class="closed">
<tr><td>cell 3</td></tr>
</tbody>
<tbody>
<tr><td>cell not important</td></tr>
</tbody>
</table>
JS--
$(document).delegate('a.showhide', 'click', function (e) {
var thisElem = $(this),
thisTbody = thisElem.closest('tbody.closed'),
thisIndex = thisTbody.index(),
thisCount = thisElem.closest('table').find('tbody.closed');
e.preventDefault();
//check to see if this element's parent is a last child
if (thisIndex == (thisCount - 1)) { /*It has been found that this is the last element*/ }
});
If you use classes then you can take advantage of getElementByClass() which is much faster than searching by attributes (which require looking through every DOM node in the scope of the search).

jquery to find all exact td matches

$('#servertable td:eq(' + server + ')')
this finds only 1 (first I think) match, how to find all matches.
btw. td:contains will not work for me.
eq expects a numerical index to only return a single row. If you want to match a td by its contents, you have to use the :contains selector. Saying "it doesn't work" and throwing it away is not the right approach to the problem, as the selector is (most likely) not at fault (Do note its case sensitive, which might be it...)
Anyhow, if you have a table like this:
<table>
<tr>
<td>Hello</td>
<td>World</td>
</tr>
<tr>
<td>World</td>
<td>Hello</td>
</tr>
<tr>
<td>Hello</td>
<td>Hello</td>
</tr>
</table>
This jQuery code:
$(function() {
$("td:contains('Hello')").css('color','red');
});
Will turn all cells with "Hello" to red. Demo.
If you need a case insensitive match, you could do this, using the filter function:
$(function() {
var search = 'HELLO'.toLowerCase();
$("td").filter(function() {
return $(this).text().toLowerCase().indexOf(search) != -1;
}).css('color','red');
});
If you need to match the exact contents of the cell, you could use something similar to the above:
$(function() {
var search = 'HELLO'.toLowerCase();
$("td").filter(function() {
return $(this).text().toLowerCase() == search;
}).css('color','red');
});
The above is case insensitive (by turning both the search and the contents to lower case when comparing) otherwise you can just remove those if you want case sensitivity. Demo.
I could be wrong, but the :eq positional selector takes an integer n an finds the nth matching element.
So if you said td:eq(1) -- you'd get the 2nd TD element in the table (second because the first index is zero/0).
My guess is that you don't want to use the :contains selector because you're looking for an exact string match and don't want partial matches.
I'm not aware that jquery has a built in selector that will meet your needs (if so, please correct me). You could add one as an extension or use another method, such as an attribute selector to do the search for you.
If you are able to control the generated HTML, you could add an ID attribute to each TD like so:
<table id="servertable" border="1">
<thead>
<tr><th>Server</th><th>Memory</th></tr>
</thead>
<tbody>
<tr><td id="server_mars">Mars</td><td>4 GB</td></tr>
<tr><td id="server_venus">Venus</td><td>1 GB</td></tr>
<tr><td id="server_jupiter">Jupiter</td><td>2 GB</td></tr>
<tr><td id="server_uranus">Uranus</td><td>8 GB</td></tr>
<tr><td id="server_mars_2010">Mars_2010</td><td>4 GB</td></tr>
</tbody>
</table>
<form>
<label for="server">Find:</label><input type="text" id="server" />
<button id="find">Find</button>
</form>
The following attribute selector would locate the correct TD in the table:
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#find").click(function() {
var server = $("#server").val();
$("#servertable td").css("background-color", ""); // reset
$("#servertable td[id='server_" + server.toLowerCase() + "']").css("background-color", "#FFFF00");
return false;
});
});
</script>
If you instead want to target the entire row that has the TD that you're looking for, you can add additional selectors:
$("#servertable tbody tr").css("background-color", "");
$("#servertable tbody tr:has(td[id='server_" + server.toLowerCase() + "'])").css("background-color", "#FFFF00");
The tbody tag isn't completely necessary, it just helps to distinguish between rows in the table body and rows in the table header.
Try :containsExact
http://wowmotty.blogspot.com/2010/05/jquery-selectors-adding-contains-exact.html
$.extend( $.expr[":"], {
containsExact: $.expr.createPseudo ?
$.expr.createPseudo(function(text) {
return function(elem) {
return $.trim(elem.innerHTML.toLowerCase()) === text.toLowerCase();
};
}) :
// support: jQuery <1.8
function(elem, i, match) {
return $.trim(elem.innerHTML.toLowerCase()) === match[3].toLowerCase();
},
containsExactCase: $.expr.createPseudo ?
$.expr.createPseudo(function(text) {
return function(elem) {
return $.trim(elem.innerHTML) === text;
};
}) :
// support: jQuery <1.8
function(elem, i, match) {
return $.trim(elem.innerHTML) === match[3];
},
containsRegex: $.expr.createPseudo ?
$.expr.createPseudo(function(text) {
var reg = /^\/((?:\\\/|[^\/]) )\/([mig]{0,3})$/.exec(text);
return function(elem) {
return RegExp(reg[1], reg[2]).test($.trim(elem.innerHTML));
};
}) :
// support: jQuery <1.8
function(elem, i, match) {
var reg = /^\/((?:\\\/|[^\/]) )\/([mig]{0,3})$/.exec(match[3]);
return RegExp(reg[1], reg[2]).test($.trim(elem.innerHTML));
}
});
$('#servertable td')
will find all td elements, but it's not entirely clear what you expect.
I too encountered this very same problem as the original author. As Paulo the original question poser had. Which selector can I use to match elements based equality check on element contents. At least I presume that's what he did (as did I) try to achieve and that would also explain why he (as well as I) can't use contains for the obvious reasons ra170 pointed out in his comment. Anyhow if someone happens to stumble here looking for the answer to that question here's the short answer to it:
jQuery has no such matcher by default. The solution is to define your own matcher. To tackle the problem at hand see this excellent blog post by Motte.

get values from adjacent table rows

i have a function to handle click of clickedcell. I want to exctract value of sibling cell with class "desired". how exactly to specify?
<tr>
<td class="desired">someValue</td>
<td><span "clickedcell"></span></td>
<td></td>
/tr>
$("span.clickedcell").click(function() {
var desired = $(this).closest(".desired").text();
...
});
or more simply:
$("span.clickedcell").click(function() {
var desired = $(this).parent().prev().text();
...
});
Also, the selector changes if you want to click anywhere in the cell:
$("td:has(span.clickedcell)").click(function() {
var desired = $(this).prev().text(); // OR
var desired2 = $(this).closest(".desired").text();
...
});
This can change depending on the nature of the relationship from clickedcell to desired. For example, the second example assumes desired is always the previous sibling. If there can be intervening siblings or it can be after then obviously the traversal needs to change to reflect that.
$(".clickedcell").click(function(){
$(this).closest("td").siblings(".desired").text();
});
Try YUI's DOM collection
YAHOO.util.Dom.getNextSibling(curTD);

Categories

Resources