JS scroll and click - javascript

I have an element that displays additional products. Looks like this:
<span class="load-products btn btn-default unveil-button">Další produkty</span>
I need to press this element when I reach the bottom window. Similar to infinity Scroll. I have a function that indicates window movement. I can't figure out a function that presses the span element.
https://446042.myshoptet.com/blahoprani/

document.getElementsByClassName('pagination-loader').click();
Solved

You can use the IntersectionObserver
Also, performance tip: avoid using classes for single elements like load-products
// options
const options = {
rootMargin: '0px',
threshold: 1.0
}
// define the observed object
const target = document.querySelector('.load-products');
// what to do when the object intersects the viewport
const callback = (entries) => {
// only listen to `intersection` events
if(entries[0].isIntersecting){
document.getElementsByClassName('load-products')[0].click()
}
}
const observer = new IntersectionObserver(callback, options);
observer.observe(target);

Related

Simulating a click on dynamically rendered React element

I am creating a geography game where you are supposed to click on a specific country on a world map - if you click on the right one, the country changes color and the game presents a new country to be clicked at. If the player doesn't know, he can click on a button which will show him the correct answer. For this, I want to simulate a click event, so that the same onClick() function is called as if you clicked on the correct country.
I am using D3, and the world map is made up of svg paths. Below is the code I thought would work, using the HTMLElement.click() method:
function simulateClick() {
// for each index in the nodelist,
// if the properties are equal to the properties of currentTargetUnit,
// simulate a click on the path of that node
let nodelist = d3.selectAll(".unit")
for (let i = 0; i < nodelist._groups[0].length; i++) {
if (nodelist._groups[0].item(i).__data__.properties.filename === currentTargetUnit.properties.filename) {
console.log(nodelist._groups[0][i])
// logs the correct svg path element
nodelist._groups[0][i].click()
// logs TypeError: nodelist._groups[0][i].click is not a function
}
}
}
I then looked at some tutorials which say that, for some reason I don't fully understand, you rather need to use React.useRef for this - but in all their examples, they put a "ref" value on an element which is returned from the beginning in the React component, like so:
import React, { useRef } from "react";
const CustomTextInput = () => {
const textInput = useRef();
focusTextInput = () => textInput.current.focus();
return (
<>
<input type="text" ref={textInput} />
<button onClick={focusTextInput}>Focus the text input</button>
</>
);
}
This obviously doesn't work because my svg path elements aren't returned initially. So my question is - how can I achieve this, whether using useRef or not?
Below are some previous questions I looked at which also did not help.
Simulate click event on react element
React Test Renderer Simulating Clicks on Elements
Simulating click on react element
I finally solved it - instead of calling the onClick() which was set inside the node I created a new clickevent with the help of the following code:
function simulateClick() {
let nodelist = d3.selectAll(".unit")
for (let i = 0; i < nodelist._groups[0].length; i++) {
if (nodelist._groups[0].item(i).__data__.properties.filename === currentTargetUnit.properties.filename) {
var event = document.createEvent("SVGEvents");
event.initEvent("click",true,true);
nodelist._groups[0].item(i).dispatchEvent(event);
}
}
}

How to track and save the last scroll position of a virtual scroll list in javascript

I have a virtual scrolling list built with JS in a Cordova app and I want to save the exact node that was at the top of the viewport after each scroll. The complication with virtual scroll is that using scrollTop is not reliable because nodes are being removed from the top and bottom of the list, which changes scroll position. Is there a reliable and performant way to do this in Javascript? The use case is this: when the user closes and reopens the app, I want to place the user at the same scroll position and ensure that the element at that position is the element that the user last saw right before closing the app. I'm considering this strategy:
get the element at the top of the list on each scroll, something like this:
scrollableList.addEventListener('scroll', e => {
const topMostElement = document.elementFromPoint(150, 150); // <-- 150px due to some positioning in the app
});
saving an identifier for that element so that I can then preload the list on app init such that the element is always rendered in that position
I'd like to know if there are other strategies for dealing with this issue in a JS-based virtual scroller. I've seen tutorials for building a virtual scroller in Javascript but haven't seen one that provides implementation details on how to get around this problem. Thanks in advance!
Update 1
The virtual scroll list has a lot of nodes, can be more than 100, each with its own child nodes. This causes its own set of problems so it's important that (where possible) the solution doesn't require iterating through the list of nodes or performing expensive operations on each node, to avoid blocking the main thread.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style >
.scroll-item {
height: 400px;
background: red;
}
.scroll-item:nth-of-type(2n) {
background: #000;
}
</style>
</head>
<body>
<div id="scrollable-list">
<div id="item-1" class="scroll-item"></div>
<div id="item-2" class="scroll-item"></div>
<div id="item-3" class="scroll-item"></div>
<div id="item-4" class="scroll-item"></div>
<div id="item-5" class="scroll-item"></div>
</div>
<script>
const lastItem = localStorage.getItem('lastItem');
if (lastItem) {
const itemElement = document.getElementById(lastItem);
if (itemElement) {
itemElement.scrollIntoView();
}
}
let options = {
rootMargin: '0px',
threshold: 1.0
}
const target = document.querySelectorAll('.scroll-item');
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
console.log(entry.target);
localStorage.setItem('lastItem', entry.target.id);
})
}, options);
target.forEach(item => {
observer.observe(item);
});
</script>
</body>
</html>
You can use IntesectionObserver and save the current element inside the localstorage
example:
https://jsfiddle.net/6bu0ynkp/
first, we are going to select all "scroll-item" items
then we are going to create a new IntersectionObserver and loop over
the entries to check if the item is intersecting
if the item inside the viewport we are going to save its id to localStorage
on the next page reload we are checking if we have "lastItem" and scroll the item into view
My guess is you may save row index, not position. This seems more semantically. Say, your datasource has items from MIN to MAX indexes. By persisting START index, you may initialize the view along with the simplest calculation of the initial position as follows:
await renderVirtualScrollUI();
viewportElement.scrollTop = (START - MIN) * ITEM_SIZE;
You may read START value from the API, Local Storage, query string, whatever. Now how it can be determined. As one of the possible approaches I would traverse viewport's inner elements and look for the first visible one using getBoundingClientRect:
const getTopRowIndex = () => {
const rows = viewportElement.children; // depends on the template
const viewportTop = viewportElement.getBoundingClientRect().top;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const rowTop = row.getBoundingClientRect().top;
if (rowTop > viewportTop) {
// calculate topmost visible index based on position
const index = Math.floor(viewportElement.scrollTop / ITEM_SIZE) + MIN;
persistTopIndex(index);
return;
}
}
}
Run getTopRowIndex() method when you want to save the value. For example, in response to scroll event throttled by 500ms (via lodash.throttle):
viewportElement.addEventListener('scroll', _.throttle(getTopRowIndex, 500));

Animation module stops working properly if animation class is not unique on the page

I've written a simple module to apply css classes from animate.css library.Classes are defined in html element data-attribute, but for some reason all animations work only if they are unique on the page, otherwise animation is implemented only to the last element with it's class.
Upd. Debugger in devtools shows that module iterates through each node, but still applies non-unique animations only to the last node.
Example:
<h2 id="1" class="module_animate-box" data-animate-trigger="scroll" data-animate-script='{"class":"zoomIn","position":"700"}'>Hello</h2>
<h2 id="2" class="module_animate-box" data-animate-trigger="scroll" data-animate-script='{"class":"zoomIn","position":"1000"}'>World</h2>
if you use class "zoomIn" twice on the page, only id="2" will work.
import "./animate.css";
//check full list of availiable classes here: https://github.com/daneden/animate.css/blob/master/README.md
export default class animationModule {
constructor({
selector
}) {
this.selector = selector;
this.nodes = Array.from(document.querySelectorAll(selector));
this.init();
}
getCoords(e) {//get coords of an element
let coords = e.getBoundingClientRect();
return coords;
}
getTriggerEvent(e){//get the appropriate function by it's trigger type
switch(e.dataset.animateTrigger){
case "scroll":
return this.onScroll
break;
case 'hover':
return this.onHover
break;
case 'moved':
return this.onMouseMove//sort of parallax
default:
return "loaded"
}
}
onScroll(e,repeats,animationTriggerYOffset,isOutOfViewport,animationType){//if trigger === scroll
e.style.animationIterationCount = repeats;//set iteration limits of animation
window.onscroll= function(){
(window.pageYOffset >= animationTriggerYOffset && !(window.pageYOffset > isOutOfViewport.bottom)) ?//check if target el is in the trigger position and not out of the viewport
e.classList.add('animated', 'infinite', animationType.class) ://toggles on classes if everything is ok
e.classList.remove('animated', 'infinite', animationType.class)// toggles off classes if lower the defined trigger position or out of viewport
repeats = e.dataset.animateRepeat;//reset iteration for animation
}
}
onHover(e, repeats, animationTriggerYOffset, isOutOfViewport, animationType){//if trigger === hover
e.style.animationIterationCount = repeats;//set iteration for animation
e.addEventListener('mousemove', ()=> {
e.classList.add('animated', 'infinite', animationType.class);
})
e.addEventListener('animationend', function () {//resets animation iteration
e.classList.remove('animated', 'infinite', animationType.class)
})
}
onMouseMove(e, repeats, animationTriggerYOffset, isOutOfViewport, animationType){
//in data-animate-script set values{"pageX":"#","pageY": "#"} the less the number the bigger the amplitude. negative numbers reverse the movement
window.addEventListener('mousemove',(m) => {
e.style.transform = `translate(${m.pageX * -1 / animationType.pageX}px, ${m.pageY * -1 / animationType.pageY}px)`
})
}
init() {
this.nodes.forEach(e => {
console.log(e.dataset)
let animationType = JSON.parse(e.dataset.animateScript);//define class name of animation needed to apply
let repeats = e.dataset.animateRepeat || 'infinite';//define number of iterations for animation (INFINITE by default)
let animationTriggerYOffset = animationType.position || 0;//defines YOffset to trigger animation(0 by default)
let isOutOfViewport = this.getCoords(e);//sets coords of an element from getCoords function
let action = this.getTriggerEvent(e);//get appropriate function depending on data-animate-trigger value
action(e, repeats, animationTriggerYOffset, isOutOfViewport, animationType);//call appropriate function per each node
})
}
}
// Module data and attributes
// .module_animate-box - class that defines animation target
// data-animate-trigger - animation trigger(possible values: "scroll", "hover", "moved"(watches mouse movement))
// data-animate-repeat - number of repetitions (infinite by default)
// data-animate-script - JSON description of animation. example: '{"class":"wobble","position":"300"}' - will add class wobble when pageYoffset===300
I'm having a little bit of trouble following, but I'd bet it's something object specific being written in window.onscroll
Good luck!

Access element whose parent is hidden - cypress.io

The question is as given in the title, ie, to access element whose parent is hidden. The problem is that, as per the cypress.io docs :
An element is considered hidden if:
Its width or height is 0.
Its CSS property (or ancestors) is visibility: hidden.
Its CSS property (or ancestors) is display: none.
Its CSS property is position: fixed and it’s offscreen or covered up.
But the code that I am working with requires me to click on an element whose parent is hidden, while the element itself is visible.
So each time I try to click on the element, it throws up an error reading :
CypressError: Timed out retrying: expected
'< mdc-select-item#mdc-select-item-4.mdc-list-item>' to be 'visible'
This element '< mdc-select-item#mdc-select-item-4.mdc-list-item>' is
not visible because its parent
'< mdc-select-menu.mdc-simple-menu.mdc-select__menu>' has CSS property:
'display: none'
The element I am working with is a dropdown item, which is written in pug. The element is a component defined in angular-mdc-web, which uses the mdc-select for the dropdown menu and mdc-select-item for its elements (items) which is what I have to access.
A sample code of similar structure :
//pug
mdc-select(placeholder="installation type"
'[closeOnScroll]'="true")
mdc-select-item(value="false") ITEM1
mdc-select-item(value="true") ITEM2
In the above, ITEM1 is the element I have to access. This I do in cypress.io as follows :
//cypress.io
// click on the dropdown menu to show the dropdown (items)
cy.get("mdc-select").contains("installation type").click();
// try to access ITEM1
cy.get('mdc-select-item').contains("ITEM1").should('be.visible').click();
Have tried with {force:true} to force the item click, but no luck. Have tried to select the items using {enter} keypress on the parent mdc-select, but again no luck as it throws :
CypressError: cy.type() can only be called on textarea or :text. Your
subject is a: < mdc-select-label
class="mdc-select__selected-text">Select ...< /mdc-select-label>
Also tried using the select command, but its not possible because the Cypress engine is not able to identify the element as a select element (because its not, inner workings are different). It throws :
CypressError: cy.select() can only be called on a . Your
subject is a: < mdc-select-label
class="mdc-select__selected-text">Select ...< /mdc-select-label>
The problem is that the mdc-select-menu that is the parent for the mdc-select-item has a property of display:none by some internal computations upon opening of the drop-down items.
This property is overwritten to display:flex, but this does not help.
All out of ideas. This works in Selenium, but does not with cypress.io. Any clue what might be a possible hack for the situation other than shifting to other frameworks, or changing the UI code?
After much nashing-of-teeth, I think I have an answer.
I think the root cause is that mdc-select-item has display:flex, which allows it to exceed the bounds of it's parents (strictly speaking, this feels like the wrong application of display flex, if I remember the tutorial correctly, however...).
Cypress does a lot of parent checking when determining visibilty, see visibility.coffee,
## WARNING:
## developer beware. visibility is a sink hole
## that leads to sheer madness. you should
## avoid this file before its too late.
...
when $parent = parentHasDisplayNone($el.parent())
parentNode = $elements.stringify($parent, "short")
"This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'display: none'"
...
when $parent = parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent())
parentNode = $elements.stringify($parent, "short")
width = elOffsetWidth($parent)
height = elOffsetHeight($parent)
"This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'overflow: hidden' and an effective width and height of: '#{width} x #{height}' pixels."
But, when using .should('be.visible'), we are stuck with parent properties failing child visibility check, even though we can actually see the child.
We need an alternate test.
The work-around
Ref jquery.js, this is one definition for visibility of the element itself (ignoring parent properties).
jQuery.expr.pseudos.visible = function( elem ) {
return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
}
so we might use that as the basis for an alternative.
describe('Testing select options', function() {
// Change this function if other criteria are required.
const isVisible = (elem) => !!(
elem.offsetWidth ||
elem.offsetHeight ||
elem.getClientRects().length
)
it('checks select option is visible', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
//cy.get('mdc-select-item').contains("ITEM1").should('be.visible') //this will fail
cy.get('mdc-select-item').contains("ITEM1").then (item1 => {
expect(isVisible(item1[0])).to.be.true
});
});
it('checks select option is not visible', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
cy.document().then(function(document) {
const item1 = document.querySelectorAll('mdc-select-item')[0]
item1.style.display = 'none'
cy.get('mdc-select-item').contains("ITEM1").then (item => {
expect(isVisible(item[0])).to.be.false
})
})
});
it('checks select option is clickable', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
//cy.get('mdc-select-item').contains("ITEM1").click() // this will fail
cy.get('mdc-select-item').contains("ITEM1").then (item1 => {
cy.get('mdc-select-item').contains("ITEM2").then (item2 => {
expect(isVisible(item2[0])).to.be.true //visible when list is first dropped
});
item1.click();
cy.wait(500)
cy.get('mdc-select-item').contains("ITEM2").then (item2 => {
expect(isVisible(item2[0])).to.be.false // not visible after item1 selected
});
});
})
Footnote - Use of 'then' (or 'each')
The way you normally use assertion in cypress is via command chains, which basically wraps the elements being tested and handles things like retry and waiting for DOM changes.
However, in this case we have a contradiction between the standard visibility assertion .should('be.visible') and the framework used to build the page, so we use then(fn) (ref) to get access to the unwrapped DOM. We can then apply our own version of the visibility test using stand jasmine expect syntax.
It turns out you can also use a function with .should(fn), this works as well
it('checks select option is visible - 2', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
cy.get('mdc-select-item').contains("ITEM1").should(item1 => {
expect(isVisible(item1[0])).to.be.true
});
});
Using should instead of then makes no difference in the visibility test, but note the should version can retry the function multiple times, so it can't be used with click test (for example).
From the docs,
What’s the difference between .then() and .should()/.and()?
Using .then() simply allows you to use the yielded subject in a callback function and should be used when you need to manipulate some values or do some actions.
When using a callback function with .should() or .and(), on the other hand, there is special logic to rerun the callback function until no assertions throw within it. You should be careful of side affects in a .should() or .and() callback function that you would not want performed multiple times.
You can also solve the problem by extending chai assertions, but the documentation for this isn't extensive, so potentially it's more work.
For convenience and reusability I had to mix the answer of Richard Matsen and Josef Biehler.
Define the command
// Access element whose parent is hidden
Cypress.Commands.add('isVisible', {
prevSubject: true
}, (subject) => {
const isVisible = (elem) => !!(
elem.offsetWidth ||
elem.offsetHeight ||
elem.getClientRects().length
)
expect(isVisible(subject[0])).to.be.true
})
You can now chain it from contains
describe('Testing select options', function() {
it('checks select option is visible', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
//cy.get('mdc-select-item').contains("ITEM1").should('be.visible') // this will fail
cy.get('mdc-select-item').contains("ITEM1").isVisible()
});
});
I came across this topic but was not able to run your example. So I tried a bit and my final solution is this. maybe someone other also needs this. Please note that I use typescript.
First: Define a custom command
Cypress.Commands.add("isVisible", { prevSubject: true}, (p1: string) => {
cy.get(p1).should((jq: JQuery<HTMLElement>) => {
if (!jq || jq.length === 0) {
//assert.fail(); seems that we must not assetr.fail() otherwise cypress will exit immediately
return;
}
const elem: HTMLElement = jq[0];
const doc: HTMLElement = document.documentElement;
const pageLeft: number = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
const pageTop: number = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
let elementLeft: number;
let elementTop: number;
let elementHeight: number;
let elementWidth: number;
const length: number = elem.getClientRects().length;
if (length > 0) {
// TODO: select correct border box!!
elementLeft = elem.getClientRects()[length - 1].left;
elementTop = elem.getClientRects()[length - 1].top;
elementWidth = elem.getClientRects()[length - 1].width;
elementHeight = elem.getClientRects()[length - 1].height;
}
const val: boolean = !!(
elementHeight > 0 &&
elementWidth > 0 &&
elem.getClientRects().length > 0 &&
elementLeft >= pageLeft &&
elementLeft <= window.outerWidth &&
elementTop >= pageTop &&
elementTop <= window.outerHeight
);
assert.isTrue(val);
});
});
Please note the TODO. In my case I was targeting a button which has two border boxes. The first with height and width 0. So i must select the second one. Please adjust this to your needs.
Second: Use it
cy.wrap("#some_id_or_other_locator").isVisible();
I could solve it by calling scrollIntoView after getting an element. See this answer.
A related problem:
Cypress was unable to find a tab element because it had a style of display: none (even though it was visible on the page)
My workaround:
Cypress could target the tab by matching text and clicking
cy.get("[data-cy=parent-element]").contains("target text").click();
To expand a bit the answer of BTL, if anyone faced an error - Property 'isVisible' does not exist on type 'Chainable<JQuery<HTMLElement>> in Typescript, following is what I added at the top of commands.ts in cypress to get away with it -
declare global {
namespace Cypress {
interface Chainable {
isVisible;
}
}
}
And may be replacing expect(isVisible(subject[0])).to.be.true with assert.True(isVisible(subject[0])); if you see any chai assertion error with expect and don't want to import it - as in Josef Biehler answer..
I was facing the same error that parent is hidden so Cypress is unable to click the child element, I handled this by handling the visibility of parent from hidden to visible by this code
cy.get('div.MuiDrawer-root.MuiDrawer-docked').invoke('css', 'overflow-x', 'visible').should('have.css', 'overflow-x', 'visible')
Note: You can apply any css you want in the invoke function like I have
Remove the flex and try. If it is solved then use the flex standard way

Mutation Observer or DOMNodeInserted

I have a script where I´ve use on the first slide of Adobe Captivate, to automate the task ok creating, courses, the script create the UX, navigation elements, intro/end motions, a game, insert spritesheets with characters, etc...
I´ve used DOMNodeInserted until know to check the modifications on the slide, when the user go to the next slide, the elements are added to the DOM and the page content is changed I´ve used this timer until now to call the function:
function detectChange(){
var slideName = document.getElementById('div_Slide')
slideName.addEventListener("DOMNodeInserted", detectChange, false);
updateSlideElements();
setTimeout(updateSlideElements, 100);
}
So I´m trying to use mutation Observer now:
var observer = new MutationObserver(function(mutations, observer) {
updateSlideElements();
});
observer.observe(document.getElementById('div_Slide').firstChild, {
attributes: true,
childList:true
});
But this is what´s happening, before with setTimeout I could reach the following element:
var motionText2 = document.querySelectorAll('div[id*=motion][class=cp-accessibility]');
This element is the firstChild of:
And the element can be found:
But now with mutationObserver the console returns empty:
I´ve just use a setTimeout inside the observer and watch the parent container not the firstChild:
var observer = new MutationObserver(function(mutations, observer) {
setTimeout(updateSlideElements, 100);
});
observer.observe(document.getElementById('div_Slide'), {
attributes: true,
childList:true
//subtree:true
});

Categories