click on specific sibling after matching text found - javascript

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();
}
}
})
}

Related

Trying to click through list of links in table - Cypress

I am trying to itirate through a list of links on a table and ensure the next page has ht ecorrect url but running into issues. One problem is that there are no good class names to work with so I have been using cy.xpath.
//Loop through each element (This is a dynamic amount of elements)
cy.xpath('//span[text()="Id"]//following::a[contains(#href,"maps")]'.each($el) => {
cy.get($el).then(($btn) => {
let id_text = $btn.text()
//Check that the element is visible and click on it
cy.get($el)
.should('be.visible')
.click()
//Check that the url contains the text value of the element that was clicked on
cy.url()
.should('contain', id_text)
})
})
It works one time through and then gets tripped up saying the DOM element became detached
When you see DOM element became detached it means an action has made the page refresh and a previous query is no longer pointing at a valid element.
In you case, the action is the .click() and the list of elements selected by cy.xpath('//span[text()="Id"]//following::a[contains(#href,"maps")]') has been refreshed, so the list that Cypress is iterating over is no longer valid.
One approach to solving this is to separate the test into two loops.
const links = []; // save link info here
const selector = '//span[text()="Id"]//following::a[contains(#href,"maps")]';
cy.xpath(selector)
.each(($el, index) => {
const id_text = $el.text()
links.push(id_text)
cy.xpath(selector).eq(index)
.as(`maps${index}`) // save a unique alias for this link
})
cy.then(function() {
links.forEach((link, index) => {
// Check that the element is visible and click on it
cy.get(`#maps${index}`) // get the element from the alias
.should('be.visible')
.click()
//Check that the url contains the text value of the element that was clicked on
cy.url().should('contain', link)
cy.go('back') // return to start page
})
})
You can shorten your code like this:
cy.get('[href*="maps"]').each(($el) => {
let id_text = $el.text().trim()
//Check that the element is visible and click on it
cy.wrap($el).should('be.visible').click()
//Check that the url contains the text value of the element that was clicked on
cy.url().should('contain', id_text)
//Wait half a sec
cy.wait(500)
})
Found a solution, it may (probably isn't) the best way but hey it works! This loops through all numbers in the column I needed, goes into the next page, checks that the url contains the number it clicked on, then goes back.
//Get length of table
cy.get('.MuiTableBody-root')
.find('tr')
.then((row) => {
let num_rows = row.length
for(let i=1; i < num_rows; i++){
cy.get(`:nth-child(${i}) > .column-id > a`).then(($btn) => {
let id_txt = $btn.text()
//Click on ID Number
cy.get(`:nth-child(${i}) > .column-id > a`)
.click()
//Check that the url has the id_txt
cy.url()
.and('contain',id_txt)
cy.go(-1) //Go back
}) //Ends the Numbers of IDs loop
} //Ends for loop
}) //Ends the then((row)) loop

jQuery/Cheerio: How to recursively get certain elements by name/tag?

I'm trying to make a bot that scrapes links from a website. I am running in to some weird error that I cannot figure out. My guess is that the second if statement is failing to check and also my unfamiliarity with jQuery is not helping.
Error:
element.each is not a function
const $ = load(html);
const html = $("#id");
const temp = [];
function recursive(element) {
if (element.name === "a") {
temp.push(element);
}
if (!element || element.children().length > 0 === false) {
return "DID NOT FIND IT OR NO CHILDREN FOUND";
}
return element.each((_, item) => recursive(item));
}
recursive(html);
return temp;
I've tried to create simple snippet demonstrating what you seem to accomplished with JQuery.
Firstly, your check if for the Tag of an element doesn't seemed to be working properly. I had to use the .prop('tagName') to get the Tag of the element. And it gets returned in all capital letters.
Your second IF-Statement should work fine, but the .each() Method didnt work as expected. You want to iterate through all children and start the recursive function. And the way you provided the child element didnt end up working.
The .each() Method want a callback function which provides two parameters as you have uses correctly. but the Item is a normal HTML Node and you had to select it with the JQuery Constant $ like $(item). This gives you the desired JQuery Element you can work with.
const html = $("#test");
const temp = [];
function recursive(element) {
if (element.prop("tagName") === "A") {
temp.push(element);
}
if (!element || element.children().length > 0 === false) {
return "DID NOT FIND IT OR NO CHILDREN FOUND";
}
return element.children().each((i, item) => recursive($(item)));
}
recursive(html);
console.log(temp)
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="test">
<div class="headings">
<h1>Heading</h1>
Main Page
</div>
<div class="test-cls">
<button>Hello</button>
Test Page
</div>
</div>

How to add all containers in a selection to an array

I am trying to get all the containers in a selection and add them into an array. So far, I have been able to get only the first container using the following code:
function getSelectedNode()
{
var containers = [];//need to add containers here so we can later loop on it and do the transformations
if (document.selection)
return document.selection.createRange().parentElement();
else
{
var selection = window.getSelection();
if (selection.rangeCount > 0)
return selection.getRangeAt(0).startContainer.parentNode;
}
}
So if I had:
<p>
<b>Here's some more content</b>.
<span style="background-color: #ffcccc">Highlight some</span>
and press the button. Press the other button to remove all highlights
</p>
and I selected this part of the text:
"Here's some more content Highlight"
Once I use the container returned by getSelectedNode() and do some transformation on it only "Here's some more content" gets affected correctly and not "Highlight". So is there a way to make it get all containers and not just the first one?
Note: I was also previously looking at this link:
How can I get the DOM element which contains the current selection?
and someone even commented:
"This solution doesn't work for all cases. If you try to select more than one tag, all the subsequent tags except the first one will be ignored."
Use Range.commonAncestorContainer and Selection.containsNode:
function getSelectedNode()
{
var containers = [];//need to add containers here so we can later loop on it and do the transformations
if (document.selection)
return document.selection.createRange().parentElement();
else
{
var selection = window.getSelection();
if (selection.rangeCount > 0) {
var range = selection.getRangeAt(0);
if (range.startContainer === range.endContainer) {
containers.push(range.startContainer);
} else {
var children = range.commonAncestorContainer.children;
containers = Array.from(children || []).filter(node => selection.containsNode(node, true));
}
}
}
return containers;
}
In your case, all possible "containers" are siblings that have no children, and we are selecting using a mouse or keyboard. In this case, we only have to consider two possibilities: you've selected a single node, or you've selected sibling nodes.
However, if your HTML were more complicated and you considered the possibility of scripts creating multiple selections, we'd have need a different solution. You would have to go through each node in the DOM, looking for ones that were part of something selection.
Maybee i am blondie and old school but if i have to fill a array i use a for next loop and something called push to fill the array. That might not be cool but usually works. I can not see any loop or pushing. So there will be only one element.`
other code
...
if (selection.rangeCount > 0)
for (var i;i<selection.rangeCount;i++){
var x= selection.getRangeAt(i).startContainer.parentNode ; //make a var
containers.push(x);//push var to array
}
return containers ;
}`
It seems that you wan't to unhighlight the selected text, it seems easer to go through the highlighted portions and see if they are part of the selection, here is an example:
document.addEventListener('mouseup', event => {
const sel = document.getSelection();
if (!sel.isCollapsed) {
const elms = [...document.querySelectorAll('.highlighted')];
const selectedElms = elms.filter(e => sel.containsNode(e, true));
if (selectedElms.length) {
selectedElms.forEach(e => {
let prev = e.nextSibling;
[...e.childNodes].forEach(child => e.parentElement.insertBefore(child, e));
e.remove();
});
sel.empty();
}
}
});
.highlighted {
background-color: #ffcccc
}
<p>
<b>Here's <span class="highlighted">Highlight <b>some</b></span> some more content</b>.
<span class="highlighted">Highlight some</span>
and press the button. Press the <span class="highlighted">Highlight some</span> other button to remove all highlights
</p>
Because I've used true as the second parameter to containsNode(...), this example will unhighlight the elements that are only partially selected.

JavaScript issue on filtering text in html element

I've been struggling with the same piece of code for a few days by now...
So for the html part I have this :
<input type="text" id="search_immobilier_ville" name="search_immobilier[ville]">
<div class="collection" id="search-ville-collection"></div>
I have an input where I have to type any city name, then I want to filter the matching cities names into an existing array of cities like this :
let api_results = [['Le Moule',97152],['Lamentin',97189],...]
let ville_input = document.getElementById('search_immobilier_ville');
Then display the matching cities as a list of elements in the div #search-ville-collection.
On each keyup event, I need to perform this action, and update the visual list in real time.
My issue is that my filtering system is messed up, if I search "lam" for example, I can get a city called "lamentin" (pass the test) and another with just the "la" matching like "capesterre-de-marie-galante"
So far, I've done this :
// Previously filled by an API
let api_results = [[name,postalCode],[name,postalCode],...];
ville_input.addEventListener('keyup', () => {
let value = ville_input.value.toUpperCase().trim();
// User input
let input_val = ville_input.value.toUpperCase().trim();
// Filtering through var ville_input
api_results.forEach(el => {
if (el[0].toUpperCase().startsWith(input_val) || input_val.length >= 2 && el[0].toUpperCase().includes(input_val)) {
result_list.style.display = 'block';
// if city not present in the html list, add it
if (document.getElementById(el[1]) === null) {
$(result_list).append(`<a class="collection-item search-ville-results" id="${el[1]}"> ${el[0]} - ${el[1]} </a>`);
}
}
}); // End forEach
/* Looping through the collection child nodes to check
if there are cities that don't match the user input */
for (let child of result_list.children) {
console.log(child)
// if the user input doesn't match with an existing city in the node, delete the node
if (!child.text.toUpperCase().includes(input_val)) {
result_list.removeChild(child);
}
}
// Highlight first element of the list
result_list.firstElementChild.classList.add('active');
// empty results div if user input is empty
if (input_val == '') {
result_list.style.display = 'none';
result_list.innerHTML = '';
}
});
This code works PARTIALLY. For example, if I type "lam", I'm supposed to get only one result based on my result set, but check out this scenario :
Typing "l":
Typing "la":
Typing "lam":
(Here you begin to see the issue)
Typing "lame":
I'm sure there's something wrong in my code, but I can't figure out what.
Your problem is with the loop you are using to remove invalid items:
for (let child of result_list.children) {
console.log(child)
// if the user input doesn't match with an existing city in the node, delete the node
if (!child.text.toUpperCase().includes(input_val)) {
result_list.removeChild(child);
}
}
children returns a live HTMLCollection, meaning that if you modify it (eg, by removing items) it will update, which will cause issues with your loop. You need to go through the items in a way that will not be affected if the collection changes.
Wrong way:
This is an example of how the loop behaves currently. The button should remove all the items that contain "th", but note how it doesn't get them all and requires multiple clicks:
document.querySelector('#removeAll').addEventListener('click', () => {
let list = document.querySelector('#list')
for (let item of list.children) {
if (item.textContent.toLowerCase().includes('th')) {
list.removeChild(item)
}
}
})
<button type="button" id="removeAll">Remove All</button>
<ul id="list">
<li>Stuff</li>
<li>Things</li>
<li>Others</li>
<li>More</li>
</ul>
(A) correct way:
One way to loop through the collection in a way that is not affected by items being removed is to start at the last index and go backwards:
document.querySelector('#removeAll').addEventListener('click', () => {
let list = document.querySelector('#list')
let index = list.children.length
while (index--) {
let item = list.children[index]
if (item.textContent.toLowerCase().includes('th')) {
list.removeChild(item)
}
}
})
<button type="button" id="removeAll">Remove All</button>
<ul id="list">
<li>Stuff</li>
<li>Things</li>
<li>Others</li>
<li>More</li>
</ul>
Better way
As an additional note, you might be better off just clearing the list entirely and using filter to get the matching results and then update the list that way. As it is currently, you are doing a lot of checking to see if the list already contains the item, checking the current list for invalid items, etc. That will affect performance of your UI, especially on lower end devices.
Try to clear out your result_list as the first thing you do inside your keyup event.
result_list.innerHTML = '';
After that, make sure to filter your api_results.
const filteredResults = api_results.filter(result => result[0].toUpperCase().includes(input_val));
console.log(filteredResults); // Sanity check.
filteredResults.forEach(result => /* your old function. */);

get second element by class name if it exists

I'm trying to get the ID of an element by class name like this
var prod_id2 = document.getElementsByClassName('select-selected')[1].id
document.getElementById('hidden-input-2').value = prod_id2;
This works fine, but my issue is that if there's only one element with that class it breaks the functionality, so I need some sort of if statement to only define this var if there is a second div with that class.
Any ideas?
Try this:
const elements = document.querySelectorAll('.test');
if (elements[1]) {
elements[1].innerText = 'Hithere';
}
<div class="test">hi</div>
<div class="test">hi</div>
<div class="test">hi</div>
document.querySelectorAll('.test'); selects all elements with the class test and returns a nodelist.
Then we can access the second element via of the nodelist with elements[1].
Here is how to check for the second element.
You can also set another fallback , different to null:
document.addEventListener("DOMContentLoaded", function(event) {
var selectedElements = document.querySelectorAll('.selected-selected'),
prod_id2 = selectedElements[1] || null;
alert(prod_id2)
});
<div id="test" class="selected-selected"></div>
You can also check that value then:
if (prod_id2) { // do some other stuff with the set value }
It breaks the functionality I think because you are grabbing the 2nd element specifically. You can do:
const prod_id2 = document.querySelectorAll('.select-selected');
and loop over the elements and grab the ID
prod_id2.forEach((prod, index) => {
if(index === 2) {
document.getElementById('hidden-input-2').value = prod.id;
}
})

Categories