I'm trying to get the first and the last node of a given element.
document.getElementById('test').firstChild
document.getElementById('test').lastChild
http://codepen.io/FezVrasta/pen/gaPJXe
The problem is that the lastChild function return me just a carriage return instead of the last useful node.
What is the best way to return me the last element or text node excluding the carriage return?
lastChild and firstChild return the first and last child nodes of the element, that can be an element, a text node or a comment. If You want to get the first and last child elements you can use:
document.getElementById('test').firstElementChild
document.getElementById('test').lastElementChild
it should be supported in all modern browsers
Go through the previousSiblings until you find one that is an element node.
The nodeType == 1 means it's an element node... see documention for all types
var lastElement = document.getElementById('test').lastChild;
while (lastElement != null && lastElement.nodeType != 1) {
lastElement = lastElement.previousSibling;
}
if (lastElement != null) {
document.getElementById("output").innerHTML = lastElement.id;
}
<div id="test">
<div id="div1">a</div>
<div id="div2">b</div>
<div id="div3">c</div>
<div id="div4">d</div>
</div>
<div id="output"></div>
Related
I'm hoping to get the textContent of an element (including text within nested children) but ignore a specific element which has text.
For example, such that
<div class="container">
<div class="exclude-text">Exclude me</div>
<div class="another-class">Add this text</div>
And this text here
</div>
Performing element.textContent returns Exclude me Add this text And this text here whereas I'd just like add this text And this text here.
My possible solution would be to iterate through childNodes while checking if classList.contains("exclude-text") but how would I still get the And this text here?
Thanks in advance!
You're on the right track looping through nodes. Do it recursively, building up the text of the text nodes, and recursing into elements if they don't match an exclude selector (which could be a group [".a, .b"], if you want to exclude multiple classes or similar):
function getTextExcept(element, exclude) {
return worker(element);
function worker(node, text = "") {
if (node.nodeType === Node.TEXT_NODE) {
text += node.nodeValue;
} else if (node.nodeType === Node.ELEMENT_NODE) {
if (!node.matches(exclude)) {
for (const child of node.childNodes) {
text = worker(child, text);
}
}
}
return text;
}
}
console.log(getTextExcept(
document.querySelector(".container"),
".exclude-text"
));
<div class="container">
<div class="exclude-text">Exclude me</div>
<div class="another-class">Add this text</div>
And this text here
</div>
An alternative approach is to clone the entire structure, remove the elements you don't want the text of, and then use .textContent on the result. For a really large structure, that might be problematic in terms of memory (but it would have to be really large).
function getTextExcept(element, exclude) {
const clone = element.cloneNode(true);
for (const child of clone.querySelectorAll(exclude)) {
child.remove();
}
return clone.textContent;
}
console.log(getTextExcept(
document.querySelector(".container"),
".exclude-text"
));
<div class="container">
<div class="exclude-text">Exclude me</div>
<div class="another-class">Add this text</div>
And this text here
</div>
In this part of code, why nextSibling returns null ?
const formIt = () => {
const titles = document.querySelectorAll('h1');
document.getElementById('content').innerHTML = '';
titles.forEach(title => {
console.log(title.nextSibling);
let p = title.nextSibling; //Returns null
let pWrapper = document.createElement('div');
pWrapper.appendChild(p);
document.getElementById('content').appendChild(pWrapper);
});
};
formIt();
<div id='content'>
<h1>...</h1>
<p>...</p>
<h1>...</h1>
<p>...</p>
<h1>...</h1>
<p>...</p>
</div>
On line 3 you set the innerHTML of content to an empty string.
That removes all the h1 and p elements from the DOM.
They aren’t siblings after that.
——
Fiddle with innerHTML after you have finished the loop.
Simply because, by the time the forEach() runs, you've removed all those objects from the DOM:
document.getElementById('content').innerHTML = '';
...so they no longer have any siblings.
There are two properties to iterate on Nodes or elements:
nextSibling
nextElementSibling
See the note at documentation of nextSibling:
Note: Browsers insert Text nodes into a document to represent whitespace in the source markup. Therefore a node obtained, for example, using Node.firstChild or Node.previousSibling may refer to a whitespace text node rather than the actual element the author intended to get.
[..]
You can use Element.nextElementSibling to obtain the next element skipping any whitespace nodes, other between-element text, or comments.
(emphasis mine)
See similar question:
javascript nextsibling function
Example
const headings = document.querySelectorAll('h1');
console.log("headings (count):", headings.length);
let firstHeading = headings[0];
console.log("first h1 nextSibling (data):", firstHeading.nextSibling.data);
console.log("first h1 nextElementSibling (data):", firstHeading.nextElementSibling.data);
let secondHeading = headings[1];
console.log("second h1 nextSibling (data):", secondHeading.nextSibling.data);
console.log("second h1 nextElementSibling (data):", secondHeading.nextElementSibling.data);
<div id='content'>
<h1>heading_1</h1>text_1
<p>paragraph_1</p>
<h1>heading_2</h1>
<p>paragraph_2</p>
</div>
I want to "manage" the first h2 element inside a div, only if it's really the "first element"
<div id="test">
<h2>Try 1</h2>
Test Test Test
<h2>Try 2</h2>
</div>
here only h2 with text "Try 1" must be managed
<div id="test">
Test Test Test
<h2>Try 1</h2>
<h2>Try 2</h2>
</div>
Here no (there is text before).
How can I do it with jQuery?
No jquery needed for that, just take:
document.getElementById('test').firstChild.nodeName // gives the name of the node
This will give you the name of the very first node, even if it's not a tag but just a plain text-node!
optionally you could of course use document.querySelector() if you want to be more flexible with your selectors and know that most of the clients browser support it.
To be clear: if you add a newline, this will also be considered as a text-node, so the heading needs to start on the same line or you will get #text as result for both examples!
This will fail:
<div id="test">
<h2>Try 1</h2>
Test Test Test
<h2>Try 2</h2>
</div>
and this will work:
<div id="test"><h2>Try 1</h2>
Test Test Test
<h2>Try 2</h2>
</div>
a little demo for you
This is how you would filter to get only the header that is the first node, ignoring all blank text nodes:-
$("#test").children("h2").first().filter(function() {
var childNodes = this.parentNode.childNodes;
var i = 0;
var textNode = 3;
// No children
if(!childNodes.length) {
return false;
}
// Skip blank text node
if(childNodes[i].nodeType === textNode && childNodes[i].textContent.trim().length === 0) {
i ++;
}
// Check we have a match
return childNodes[i] === this;
});
Here is it in action http://jsfiddle.net/nmeXw/
I think I found myself a solution, a bit funny :
if($.trim($('#test').html()).substr(0,4).toLowerCase() == "<h2>")
{
$('#test h2:first').css('background-color', 'red');
}
What do you think about? :)
You can use .contents to conditionally ignore leading nodes that are only whitespace text. Then see if the first node is an <h2> (Fiddle):
function isFirstChildH2(selector) {
// get th efirst node, which may be a text node
var firstNode = $(selector).contents().first();
// if the first node is all whitespace text, ignore it and go to the next
if(firstNode[0].nodeType == 3 && firstNode.text().match(/\S/g) == null) {
firstNode = firstNode.next();
}
if(firstNode.is("h2")) {
// it's an h2; do your magic!
alert("h2 is the first thing on " + selector)
} else {
// first node is either non-whitespace text or an non-h2 element
// don't do your magic
alert("h2 is NOT the first thing on " + selector)
}
}
isFirstElementH2("#test");
The challenge we're facing here is that javascript recognizes whitespace as a text node as well. Therefore, from a javascript point of view, this HTML:
<div id="test">
<h2>Try 1</h2>
Test Test Test
<h2>Try 2</h2>
</div>
Is different from this HTML:
<div id="test"><h2>Try 1</h2>
Test Test Test
<h2>Try 2</h2>
</div>
In the first case, the first node inside the div is a textNode (nodeType == 3)
In the second HTML example, the first node inside the div is a h2 node.
I've come up with a solution for this, a handy function that loops through all elements combining jQuery and native javascript.
Solution
var objNodes = $(".wrapper").contents().get();
function loopNodes(objNodes, i) {
i = (typeof i === "undefined") ? 0 : i;
if (objNodes[i].nodeType !== 3) {
return {"isHeader":true, "first":$(objNodes[i])};
} else {
var strText = objNodes[i].innerText || objNodes[i].textContent;
if ($.trim(strText).length === 0) {
return loopNodes(objNodes, i+1);
} else {
return {"isHeader":false, "first":null};
}
}
}
Usage
var objResults = loopNodes(objNodes);
if (objResults.isHeader) {
console.log("Coolness");
objResults.first.text("AWESOME FIRST HEADER!");
} else {
console.log("Less Coolness");
}
In action:
http://jsbin.com/welcome/61883/edit
Edit: Added the cross-browser way of getting innerText/textContent. See Quirksmode for full reference on the matter.
OK, let's mix some jQuery with plain DOM code (as jQuery is not capable of handling text nodes):
var $el = $("#test > h2:first-child");
if (!$el.length) return false;
var el = $el.get(0),
reg = /\S/; // no whitespace
for (var prev = el; prev = prev.previousSibling; )
if (prev.nodeType == 3 && reg.test(prev.data))
return false;
return el;
Demo at jsfiddle.net
Is there any jQuery function similar to closest() that will return elements outside of the parent chain, traversing sideways? For example, I want to call a function foo() on the div source that would return the div target. I know I could navigate using parent() and siblings(), but I need something generic that would go as many levels as needed, up, sideways and down?
var allsources = $('.source');
allsources.click(function()){
$(this).closest('.target').hide();
});
<div class="row">
<div>
<div class="target" ></div>
</div>
<div>
<div>
<div class="source"></div>
</div>
</div>
</div>
<div class="row">
<div>
<div class="target" ></div>
</div>
<div>
<div>
<div class="source"></div>
</div>
</div>
</div>
EDIT:
My definition of closest: you have an element source. Try to find it down. If find more than one, return one that is less node hoops down/next/prev. If not found, go one level up, and try to find again. Repeat until no parent.
If, by closest, you mean "travel up as little as possible, then anywhere downwards", then you can do
$("#source")
.closest(":has(.target)")
.find(".target:first") //make sure we only select one element in case of a tie
In your case, it would be better to specify the common parent directly:
$(this)
.closest(".row")
.find(".target") //there's no tie here, no need to arbitrate
This is a tricky one. As has been commented, how do you define closest in this context? Assuming you can decide on some rules; for example:
Traverse up: 3pt
Traverse down: 2pts
Move sideways: 1pts
And then consider the item with the lowest points to be "closest" then it would be easy enough to author a plugin, named something such as closestAll, which would do the recursive traversal of the whole dom tree to determine the closest item.
However, looking at your recent edit, one (of many!) right solutions to the problem stated is:
var allsources = $('.source');
allsources.click(function(){
$(this).parents('.row').find('.target').hide();
});
Live example: http://jsfiddle.net/zCvJM/ (Source A only hides Target A, Same for B)
If you know exactly the structure of the dom and level of nesting, have you consider to use the eq() method
$(this).parents().eq(1).prev().children(".target")
I don't think there is a way to do this other than basically querying the whole DOM:
$('#target')
Because if you want to go up and across (never mind down as well) then the target element isn't related to the child element. If you also want to check for the presence of the child element you will have to do that separately.
-Edit:
After reading your comment on wanting to find the closest element regardless of whether it is a parent, I think you will have to write a custom function to crawl back up the dom one node at a time. I have tested the following and it works:
Markup
<div id="parent">
<div id="child1">
<div id="source"></div>
</div>
<div id="child2">
<div class="target" rel="right"></div>
</div>
<div id="child3">
<div>
<div class="target" rel="wrong"></div>
</div>
</div>
</div>
Script
$(document).ready(function () {
var tgt = findClosest($('#source'), '.target');
if (tgt != undefined) {
alert(tgt.attr('rel'));
}
});
function findClosest(source, targetSel) {
var crawledNodes = $();
var target = null;
// Go up
source.parents().each(function () {
console.log(crawledNodes.index($(this)));
if (crawledNodes.index($(this)) == -1 && target == null) {
crawledNodes.add($(this));
target = findTarget($(this), targetSel);
// Go across
$(this).siblings().each(function () {
console.log("Sibling");
if (crawledNodes.index($(this)) == -1 && target == null) {
crawledNodes.add($(this));
target = findTarget($(this), targetSel);
}
});
}
});
return target;
}
function findTarget(el, targetSel) {
console.log(targetSel);
var target = el.find(targetSel);
if (target.size() > 0) {
return target.eq(0);
}
else
{
return null;
}
}
If I understood the specification correctly you mean something like the function closest defined below:
var allsources = $(".source");
function closest($source,selector) {
if($source == null) return $([]);
var $matchingChildren = $source.find(selector);
if($matchingChildren.length != 0) return $($matchingChildren.get(0));
else return closest($source.parent(), selector)
}
allsources.click(closest($(this),'.target').hide();});
You can see it working at http://jsfiddle.net/y2wJV/1/
Your definition requires that when choosing among matching children the function must return one that is less node hoops down/next/prev. This requirement has not been met, but this function is quite flexible and seems to do what you want to do in the case of the example you provided.
I found this code that is simple but does not solve the tie issue (returns the first)...
(function ($) {
$.fn.findClosest = function (filter) {
var $found = $(),
$currentSet = this; // Current place
while ($currentSet.length) {
$found = $currentSet.find(filter);
if ($found.length) break; // At least one match: break loop
// Get all children of the current set
$currentSet = $currentSet.parent();
}
return $found.first(); // Return first match of the collection
};
})(jQuery);
I encountered a similar problem, i had a table i needed to find the next element which may be outside the current td, so i made a jquery function:
$.fn.nextAllLevels = function(sel) {
if ($(this).nextAll(sel).length != 0) {
return $(this).nextAll(sel).eq(0);
} else if ($(this).nextAll(':has(' + sel + ')').length != 0) {
return $(this).nextAll(':has(' + sel + ')').find(sel).eq(0);
} else {
return $(this).parent().nextAllLevels(sel);
}
So to use this you simply call
$('#current').nextAllLevels('.target');
To give you the element closest in the foward direction, regardsless of whether in is in the current parent or not.
I want to apply a class 'solo' to a link in a paragraph where that link is the ONLY element in the paragraph.
So this would get the 'solo' class:
<p><a>I am alone</a></p>
But this would not:
<p><a>I am not alone</a> because there is more text!</p>
You could do:
$('p').filter(function() {
var $childNodes = $(this).contents();
return $childNodes
.not($childNodes.filter('a').first())
.not(function() {
return this.nodeType === 3 && $.trim(this.nodeValue) === '';
}).length === 0;
});
This gets all child nodes (including text nodes) and removes the first a element it finds and all text nodes only containing whitespaces from that set. If the resulting set is empty, the link was the only child (alone).
Reference: filter, not, contents, Node.nodeType, trim
Update: Maybe this could be done easier, but I wanted it to work also for the case your HTML contains line breaks, which would result in text nodes containing only whitespaces:
<p>
<a>I am alone</a>
</p>
<p>
<a>I am not alone</a> because there is more text!
</p>
DEMO
Try this
$('p').filter(function(){
var $childrens = $(this).children();
return ($childrens.length == 1
&& $childrens.is('a')
&& $(this).text() == $childrens.text());
}).addClass('solo');
Demo
There is 1 more way to do this:
if($("p").find('a').length === 1){
//perform operation
}
Not very performant, but this is the most concise and nice (and working) solution I came out with!
$('p').filter( function() {
return !$('<p>'+$.trim($(this).html())+'</p>').contents().not('a:first').length;
}).addClass('solo');
http://jsfiddle.net/DYxgu/