Is there a way to select elements appearing after a given element using CSS selectors.
Suppose I have a DOM like this one
<body>
<span class="next">A next span</span>
<div>
<span class="next target">the target next span</span>
</div>
<div>
<span class="next">this is the one I want to select</span>
</div>
</body>
I want to select the spans of class next but only the ones that appear after span.next.target. What make this tricky is that they need not to be siblings or under the same parent node but can appear anywhere in the DOM.
Is this even possible, or am I doomed to stick with a for loop?
You can use plain javascript for this:
'use strict';
console.clear();
const spanNextNodes = document.querySelectorAll('div span.next');
let find = function*(nodes) {
let isNextTargetFound = false,
arrayOfNodes = Array.prototype.slice.call(nodes); // untill spread operator is supported [...nodes], we have to use slice method
for (let n of arrayOfNodes) {
if (isNextTargetFound) {
yield n;
} else if (n.classList.contains('target')) {
isNextTargetFound = true;
}
}
}
for (let item of find(spanNextNodes)) {
console.log(item);
}
Here is an example
It's based on ES2015 standards using function generators (yield), but if you would like to use ES5 you can add items to array and return array instead.
Related
I'm having a for loop to get a certain task done.Now in that for loop,in the last iteration, I need to add the green-color class to all the elements which has the class checkMarks.
This is my code, currently it adds the green-color class only to the first element. Is there a way to do this without having to use another for loop inside the current for loop?
const studentLength = 24;
for(let i=0; i<studentLength; i++){
//something happens here
if(i===studentLength ){ //if last iteration
document.querySelectorAll(".checkMarks").classList.add("green-color");
}
}
You need to iterate the result of querySelectorAll and apply the class to each element:
document.querySelectorAll(".checkMarks").forEach(e => e.classList.add("green-color"));
.green-color {
color: green;
}
<div class="checkMarks">TEST</div>
<div class="checkMarks">TEST2</div>
<div class="checkMarks">TEST3</div>
<div class="checkMarks">TEST4</div>
<div class="checkMarks">TEST5</div>
<div class="checkMarks">TEST6</div>
<div class="checkMarks">TEST7</div>
You need to loop since the All in querySelectorAll returns a nodelist, but you should use forEach on the students too
I wrap in a spread [...] to handle early EDGE browsers
students.forEach(student => { /*something happens here */});
[...document.querySelectorAll(".checkMarks")].forEach(chk => chk.classList.add("green-color"));
If there is only ONE checkMark you can do document.querySelector(".checkMarks").classList.add("green-color") without the all, but vanilla JS does not support adding to a list in one go like the equivalent jQuery $(".checkMarks").addClass("green-color") would
Personally, I would use a for...of loop (ES6):
const divs = document.querySelectorAll(".checkMarks");
for (const div of divs) {
div.classList.add("green-color");
}
.green-color {
color: green;
}
<div class="checkMarks">DIV1</div>
<div class="checkMarks">DIV2</div>
<div class="checkMarks">DIV3</div>
<div class="checkMarks">DIV4</div>
<div class="checkMarks">DIV5</div>
<div class="checkMarks">DIV6</div>
<div class="checkMarks">DIV7</div>
I have many of the below 'k-top' div elements, with the same inner div structure, except different unique text in two places, in 'k-in' and in my checkbox id.
<div class="k-top">
<span class="k-icon k-i-expand"></span><-------------- trigger click on this if below text is found
<span class="k-checkbox-wrapper" role="presentation">
<input type="checkbox" tabindex="-1" id="unique TEXT99" class="k-checkbox">
<span class="k-checkbox-label checkbox-span"></span>
</span>
<span class="k-in">unique TEXT99</span></div><- if this text is found in k-in trigger click on elem above
I want to iterate through all my span.k-ins until I find the innerText to match contains of 'unique' for instance, then once unique is found, I want to .click(); on it's sibling element '.k-i-expand' as seen in the mark-up above. I do not want to trigger a .click(); on all .k-i-expand just the specific one that has same parent as where my 'unique text' is found.
Thus far I have tried .closest, I have also tried sibling.parent.. both return null or undefined.. Note, I am not using jQuery.
The below works successfully to click all .k-i-expand - but I need to .click() only the one where k-in innerText contains 'unique'. Ideally I'd use starts with, or contains, but I'd specify the whole word if needed i.e. unique TEXT99
let exp = document.querySelectorAll('.k-i-expand');
let i;
for (i = 0; i < exp.length; ++i) {
exp[i].click();
};
More previous attempts can be seen here: how to run a .click on elems parent sibling selector?
I created a recursive function which checks all it's Siblings until it finds one with the specified innerHTML. If it does not find one, it does nothing:
function checkSibling(node) {
if (node.innerHTML == "unique TEXT99") {
return true;
} else if (node.nextSibling) {
return checkSibling(node.nextSibling);
} else {
return false;
}
}
async function clickOnNode() {
let exp = document.querySelectorAll(".k-i-expand");
for await (const node of exp) {
const hasText = await checkSibling(node);
if (hasText) {
console.log("Result: ", hasText);
node.click();
}
}
}
clickOnNode();
I also created a codepen with the code for you to play around. I guess the innerHTML check could be improved via a Regex.
Have you tried iterating over the .k-top elements and looking into each one to find your .k-in?
const expandItemsContaining = (text) => {
// Let's get all the .k-top divs
const kTops = document.querySelectorAll('.k-top');
// And peek into each and every one of them
kTops.forEach(kTop => {
// First we check whether there is a .k-in containing your text
const kIn = kTop.querySelector('.k-in');
const shouldClick = kIn && kIn.innerText && kIn.innerText.indexOf(text) !== -1;
// And if there is one we find the .k-i-expand and click it
if (shouldClick) {
const kExpand = kTop.querySelector('.k-i-expand');
if (kExpand) {
kExpand.click();
}
}
})
}
I am trying to create an extension for VSTS using their extension kit (https://learn.microsoft.com/en-us/vsts/extend/overview?view=vsts).
<script type="text/javascript">
VSS.init();
var items = {}
// Get data service and display
VSS.getService(VSS.ServiceIds.ExtensionData).then((dataService) => {
dataService.getDocuments('MyCollection2').then((docs) => {
// keep a reference to the element instead of searching for it in each loop.
const itemsDiv = document.getElementById('items');
const contents = [];
for (let i = 0; i < docs.length; i++) {
// using template strings here to show you another way of working with strings in es6
var name = docs[i].name
contents.push(
`<div
class="listItem"
onClick="console.log(docs[i])"
onmouseover="this.style.background='#D5DBDB';"
onmouseout="this.style.background='white';">
${docs[i].name}
</div>`
)
}
// finally update the target element one time with your contents.
// The new line character isn't required, can just use '',
// but this might be easier to read for you
itemsDiv.innerHTML = contents.join('');
});
});
</script>
So what my javascript part does is I try to fetch objects from VSTS`s internal data storage (I named it MyCollection2) and display the objects as a list
HTML part
<section>
<nav>
<div class="create_button">+ Create KPI</div>
<div id="items"></div>
</nav>
<article>
<h2>Create KPI</h2>
<br>
<form action="" id="form" onsubmit="sConsole(event)">
KPI Name<br>
<input type="data" id="name">
<br><br>
Actual Value<br>
<input type="data" id="actual">
<br><br>
Potential Value<br>
<input type="data" id="potential">
<br><br>
Goal %<br>
<input type="data" id="goal">
<br><br>
<button type="submit">Create</button><span>Cancel</span>
</form>
</article>
</section>
So all the objects are rendered in the div with the id items.
Everything is fine up to this point.
The problem is the onClick="console.log(docs[i]) part in my javascript part.
My intention was to console.log the document object whenever each item in the list was clicked.
However, this doesn't print the object as I intended.
It just prints externalContentHost10 and I don't know what that is.
What can I do to make this work?
docs is defined in your function; the scope of the onclick attribute (note: should be all lowercase) is not the same. In general, you should avoid inline event handlers as they’re not very flexible or maintainable. You should instead use addEventListener, which means ditching innerHTML and working with proper element nodes. A few other changes I would make are:
Flattening the promises (removing the nesting) by returning them
Using for...of for iteration
Using const (and let, but in this case const is enough) instead of var so that your variables have the right scope
This gives us:
VSS.init();
const items = {};
// Get data service and display
VSS.getService(VSS.ServiceIds.ExtensionData)
// the callback on the next line returns a promise, which the JavaScript engine will follow, so you don't need to nest the next `then`
.then((dataService) => dataService.getDocuments('MyCollection2'))
.then((docs) => {
// keep a reference to the element instead of searching for it in each loop.
const container = document.getElementById('items');
// this loop will remove any existing children
while (container.firstChild !== null) {
container.removeChild(container.firstChild);
}
// `for...of` is a simpler way to iterate over a collection
for (const doc of docs) {
// create a `div` element
const div = document.createElement("div");
// add a text node to it
div.appendChild(document.createTextNode(doc.name));
// add event listeners to change its background
div.addEventListener("mouseover", e => { div.style.background = "#D5DBDB"; });
div.addEventListener("mouseout", e => { div.style.background = "white"; });
// add a `click` listener
div.addEventListener("click", e => { console.log(doc); });
// add the new div to the container
container.appendChild(div);
}
});
If you wanted to use classes instead to manage the styling—which is the recommended method—then you could implement the event listeners using classList:
div.addEventListener("mouseover", e => div.classList.add("hover-class"));
div.addEventListener("mouseout", e => div.classList.remove("hover-class"));
(classList has toggle and replace methods, but they aren’t supported by IE at all, and Edge only seems to support toggle, so whether to use them depends on your minimum supported version.)
But you would probably be better off defining a CSS :hover class rather than doing all this, if styling is all you want to change.
I am trying to select one ore more elements that are NOT descendants of another specific element.
<html>
<body>
<div>
<p>
<b>
<i> don't select me </i>
</b>
</p>
</div>
<div>
<i>don't select me either </i>
</div>
<i> select me </i>
<b>
<i> select me too </i>
</b>
</body>
</html>
In the example above I want to select all 'i' elements, that are not inside div elements.
The other way around would be easy, with ('div i'), but using this in :not() is not possible.
How can I select all i elements outside of div elements?
Often it is suggested the use of jQuery, which would be like:
nondiv_i = all_i.not(all_div.find("i"))
I can't use jQuery, but could use jqLite - jqLite does not have a not()-function. A jqLite solution is welcome too!
Is it possible to do this without repeated iterations and comparisons?
Edit: To clarify, i don't want to have any div-ancestors for my i-elements, not only no direct div-parents.
A comparable XPath would look like this:
//i[not(ancestor::div)]
function isDescendant(parent, child) {
var all = parent.getElementsByTagName(child.tagName);
for (var i = -1, l = all.length; ++i < l;) {
if(all[i]==child){
return true;
}
}
return false;
}
for the specific case of yours;
is = document.getElementsByTagName("i");
divs = document.getElementsByTagName("div");
nondivs = [];
var contains;
for(i=0;i<is.length;i++){
contains = false;
for(j=0;j<divs.length;j++){
if(isDescendant(divs[j],is[i])){
contains = true;
j = divs.length;
}
}
if(!contains){
nondivs.push(is[i]);
}
}
Add a class to all of the <i> tags ("itag", for example). From there, you can fetch them by calling getElementsByClassName:
var r = document.getElementsByClassName("itag");
console.log("length" + r.length);
You can then get them by index:
console.log(r[0]);
console.log(r[1].innerHTML); // get text of i tag
so basically here is my script:
http://jsfiddle.net/JJFap/42/
Code -
jQuery(document).ready(function() {
var rel = new Array();
var count = 0;
jQuery(".setting").each(function() {
rel[count] = [];
if(jQuery("span").attr("rel")) {
rel[count].push(jQuery("span").attr("rel"));
}
console.log(count);
count++;
});
jQuery("body").text(rel);
console.log(rel);
});
and
<div class="setting">
<span rel="Variable">Variable</span>
<span rel="Item">Item</span>
<span rel="Something">Something</span>
</div>
<div>
<span rel="Smth">Smth</span>
<span>Sec</span>
</div>
<div class="setting">
<span>Second</span>
<span rel="first">First</span>
<span rel="Third">Third</span>
</div>
my question, is why does it display Variable, variable?
I would like it to display Variable, First, but I'm not able to do.
Basically what I would like to achieve is create new array, in which insert each div.setting span elements with rel attribute array.
So basically in this example it should output -
Array (
Array[0] => "Variable","Item","Something";
Array[1] => "first","Third";
)
Hope you understood what I meant :)
EDIT:
In my other example I tried to add jQuery("span").each(function() ... inside first each function, but it outputted two full arrays of all span elements with rel. I can't have different classes / ids for each div element, since all will have same class.
jQuery('span') is going to find ALL spans in your page, and then pull out the rel attribute of the first one. Since you don't provide a context for that span search, you'll always get the same #1 span in the document.
You should be using this:
jQuery('span',this).each(function() {
rel[count] = [];
if (jQuery(this).attr("rel")) {
rel[count].push(jQuery(this).attr("rel"));
}
console.log(count);
count++;
})
instead of this:
rel[count] = [];
if(jQuery("span").attr("rel")) {
rel[count].push(jQuery("span").attr("rel"));
}
console.log(count);
count++;
http://jsfiddle.net/mblase75/JJFap/52/
The trick is to use a second .each to loop over all the span tags inside each <div class="setting"> -- your original code was using jQuery("span"), which would just grab the first span tag in the document every time.
In addition to what has been said, you can also get rid of the count and one push() when using jQuery.fn.map() as well as getting rid of the if when adding [rel] to the selector:
jQuery(document).ready(function() {
var rel = [];
jQuery(".setting").each(function() {
rel.push(jQuery(this).find('span[rel]').map(function() {
return this.getAttribute('rel');
}).get());
});
jQuery("body").text(rel);
console.log(rel);
});
Within the .each() method, you have this code a couple times: jQuery("span").attr("rel"). That code simply looks for ALL span tags on the page. When you stick it inside the .push() method, it's just going to push the value for the rel attribute of the first jQuery object in the collection. Instead, you want to do something like $(this).find('span'). This will cause it to look for any span tags that are descendants of the current .setting element that the .each() method is iterating over.