I have a drop down menu where I need list items to be highlighted based on the mouse scroll increments. When the user hovers over the list and scrolls 1x up or down, the item immediately above or below should be highlighted. This involves getting the deltaY of the mousescroll event I believe. The problem is that this deltaY value is super sensitive. So when there's even a slight scroll up gesture, it logs it as something crazy like 7x/8x. How should I approach handling this functionality.
This is what I currently have (but feel free to suggest something completely different if it'll work better):
***(also, (event.target.id[event.target.id - 1]) this isn't working. what am I doing wrong?)
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
highlightListItem(event: any, scrollUp?: boolean, scrollDown?: boolean): void {
this.highlightTimer = setTimeout(() => {
this.someService.highlightListItem(event.target.id);
}, 450);
if (scrollUp) {
this.highlightListItem = setTimeout(() => {
this.someService.highlightListItem(event.target.id[event.target.id - 1]);
}, 450)
} else if (scrollDown) {
this.highlightTimer = setTimeout(() => {
this.someService.highlightTimer(event.target.id[event.target.id - 1]);
}, 450);
}
scrollEvent(event: any): void {
console.log(event.deltaY * 100);
if (event.deltaY < 0) {
this.highlightListItem(event, false, true);
} else if (event.deltaY > 0) {
this.highlightListItem(event, true, false);
}
}
HTML:
<mat-card class="some-card" *ngFor="let property of properties; let i = index;" id="segment{{i}}"
(mousewheel)="scrollEvent($event)">
<mat-card-content class="some-table">
<div class="index">{{i + 1 }}</div>
<div class="someClass">
<div>{{property.field}}</div>
</div>
</mat-card-content>
</mat-card>
Related
Note: Similar question is here but it doesn't seem to be answered in an obvious way I can apply.
Problem: I have a 'go to top' button whose function is to scroll the user to the top of the page. It should not be visible when the user is actually at the top.
Its visibility is presently handled via an animated opacity attribute, but it is able to be interacted with due to display: none not being applied. So I must find a way to have display: none apply when the opacity is 0.
The first implementation, in Angular, is to do something like this:
#HostListener('window:scroll', ['$event'])
scrolled(_event: Event) {
if (window.pageYOffset === 0) {
setTimeout(() => (this.noClick = true), 1000);
} else {
this.noClick = false;
}
this.show = window.pageYOffset > 0;
}
Where noClick and show are boolean values, and noClick is applied to a class which sets display:none and show is applied to the animated opacity.
There is a race condition however, where the user can scroll down after scrolling to 0 and the delay can still set noClick to true.
I would like to consider an Observable to solve this issue.
If I have two subjects:
displayNone = new Subject<boolean>(); // only contains true values
displaySome = new Subject<boolean>(); // only contains false values
and some event code:
#HostListener('window:scroll', ['$event'])
scrolled(_event: Event) {
if (window.pageYOffset === 0) {
this.displayNone.next(true);
} else {
this.displaySome.next(false);
}
this.show = window.pageYOffset > 0;
}
I need to construct an observable for the displayNone functionality where if displayNone is delayed 1000ms (animation time) and in that time displaySome emits, it must ignore displayNone.
The closest that I can come presently is something like this:
race(this.displaySome, this.displayNone.pipe(delay(1000)))
But my understanding is that it will take only evaluate it once and make a decision which one to pass through. I need it to be constantly evaluated.
You don't need two Observables to show/hide a button with animations. The best solution to your problem is different from the answer to your question. I'm trying to answer both.
Only emit if another Observable hasn't emitted in the last X milliseconds
const X = 1000;
const obs$ = source$.pipe(
// emit values on a new inner Observable when the otherObservable$ emits
window(otherObservable$),
// skip all values on this inner Observable until a given time has passed
// (but not for the first inner Observable created before otherObservable$ emits)
mergeMap((w, i) => i == 0 ? w : w.pipe(skipUntil(timer(X))))
);
Show/hide a Button depending on scroll events
Create an Observable from scroll events and map to a boolean value indicating whether to show or hide the button.
hasScrolledDown$: Observable<boolean>;
ngOnInit() {
this.hasScrolledDown$ = fromEvent(window, 'scroll').pipe(
throttleTime(20),
map(() => window.pageYOffset > 50)
)
}
Use ngIf to show / hide the button depending on the value from the Observable.
<button *ngIf="hasScrolledDown$ | async">
(Optional) Add animations for enter and leave state changes.
<button *ngIf="hasScrolledDown$ | async" [#fadeAnimation]>
export const fadeOutAnimation = animation([
style({ opacity: 1 }),
animate('200ms', style({ opacity: 0 }))
])
export const fadeInAnimation = animation([
style({ opacity: 0 }),
animate('200ms', style({ opacity: 1 }))
])
#Component({
selector: 'button-overview-example',
templateUrl: 'button-overview-example.html',
styleUrls: ['button-overview-example.css'],
animations: [
trigger('fadeAnimation', [
transition(':enter', [useAnimation(fadeInAnimation)]),
transition(':leave', [useAnimation(fadeOutAnimation)])
])
],
})
Demo
I made a tooltip which appears when I hover on an element, and shows the full name of the product, productName.
<div
className="product-select-info"
onMouseEnter={e => productNameHandleHover(e)}
onMouseLeave={productNameHandleNoHover}
>
{productName}
<div
className="tooltip"
style={{
display: isTooltipShown ? "block" : "none",
top: mouseLocation.y,
left: mouseLocation.x,
}}
>
{productName}
</div>
</div>
And here are my handlers:
const productNameHandleHover = (event: any): void => {
setmouseLocation({
x: event.pageX,
y: event.pageY,
});
setisTooltipShown(true);
};
const productNameHandleNoHover = (): void => {
setisTooltipShown(false);
};
My problem is, I want to only show the tooltip after like 0.5 seconds. Currently, the tooltip appears as soon as the mouse goes over the div. How do I achieve this? I tried using setTimeout but I was just running into issues with that.
It is good to use css transitions as ritaj has mentioned in the comments.
But if you absolutely want javascript implementation, Whenever you are hovering over your element, set a class variable to be true.
const productNameHandleHover = (event: any): void => {
this.hovering = true;
...
}
and set it false whenever it is not.
const productNameHandleNoHover = (): void => {
this.hovering = false;
setisTooltipShown(false);
};
And when you actually set your tooltip check if your class variable is set or not.
const productNameHandleHover = (event: any): void => {
this.hovering = true;
setmouseLocation({
x: event.pageX,
y: event.pageY,
});
setTimeout(() => {
if (this.hovering) {
setisTooltipShown(true);
}
}, 500)
};
Here is a codesandbox that does what you need.
But you can already see the amount of effort you have to put in. So coming back to the original point. Using css transitions is a better option
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!
Am currently using framework7 and I have this problem wherein I need to get a button floating once the user pass scrolling a specific element.
But for some reason am not able to make the scroll event work. Even used a native event listener but still no luck.
Here is my code. In my component:
export default {
methods: {
handleScroll(event) {
alert('should work')
}
},
created() {
window.addEventListener('scroll', this.handleScroll);
},
destroyed() {
window.removeEventListener('scroll', this.handleScroll);
},
mounted() {
window.addEventListener('scroll', this.handleScroll)
this.handleScroll;
var element = document.querySelector(".similar-adventures");
var top = element.offsetTop;
window.scrollTo(0, top);
}
}
And here is my native event listener code:
window.addEventListener(‘scroll’, function(e){
// Get the new Value
newValue = window.pageYOffset;
//Subtract the two and conclude
if(oldValue - newValue < 0){
console.log(“Up”);
} else if(oldValue - newValue > 0){
console.log(“Down”);
}
// Update the old value
oldValue = newValue;
});
I know this is old now but i will answer for future reference, so i think the problem here is that the window is not actually scrolling as framework7 uses pages/views.
In vue the renders to 2 divs like so..
<f7-page>
<div slot="fixed">Fixed element</div>
<p>Page content goes here</p>
</f7-page>
<!-- Renders to: -->
<div class="page">
<div>Fixed element</div>
<div class="page-content">
<p>Page content goes here</p>
</div>
</div>
i found that its the page-content class that you want to put the eventListenter on best way to do this is Dom7 like so...
let page = $$('.page-content')
page.on('scroll', () => {
console.log(page.scrollTop()) // will show page top position
page.scrollTop(0) // will scroll to top
})
//if you have multiple pages
let page = $$('.page-content')
let home = $$(page[0])
let about = $$(page[1])
page.on('scroll', () => {
console.log(home.scrollTop()) //home page top position
console.log(about.scrollTop()) //about page top position
})
//more options
page.scrollTop(position, duration, callback)
page.scrollTo(left, top, duration, callback)
just remember to import $$ from 'Dom7'
This code retrieves all the pages from the f7 component in an array
let pages = document.querySelectorAll('.page-content');
Then to make a page scrollable, select the respective index and do:
pages[0].addEventListener('scroll', function () { console.log('is scrolling...') } );
For the same code but in a more beautiful way as we don't want to specify the page by index:
add an id to your f7-page tag
<f7-page name="whatever" id='myPage'>
then do this code for example in mounted:
let f7page = document.getElementById('myPage');
let scrollableDiv = f7page.querySelector('.page-content');
scrollableDiv.addEventListener('scroll', function () { console.log('is scrolling...') } );
special thanks to BiscuitmanZ's comment for finding the underlying issue
Hey Guys i need your help please,
is started to working on an Ionic 2 App. My Navigation is not that complicated. I have one menu if i click one item another menu opens with a submenu and if i click on an item in the submenu a third page should render above it and this works really fine. Now the third activity should be a very long scrolling site with a lot of section (the sections are on top of each other). And every section should have a toolbar with one back button to go back to the submenu and two arrow keys for the previous or next section.
Here a small picture
now my problems:
how can i achieve the magnetic part? I think it like so: the Bar sits on the top of the page and above the content. When i scroll the content goes underneath and i can scroll to the end. When iam at the end everything should stop and when i pull further the next Section Bar jumps to the top of my site.
I hope you can help me thank you ;)
Plunker Demo
To make this work you need to:
Create a function that scrolls your scroll-content element to the top
Track the scroll position of scroll-content
Use *ngIf on your scroll to top button to conditionally show after scroll-content has reached a certain threshold.
Scroll to top function
I adapted this SO answer to apply to the scroll-content element
scrollToTop(scrollDuration) {
let scrollStep = -this.ionScroll.scrollTop / (scrollDuration / 15);
let scrollInterval = setInterval( () => {
if ( this.ionScroll.scrollTop != 0 ) {
this.ionScroll.scrollTop = this.ionScroll.scrollTop + scrollStep;
} else {
clearInterval(scrollInterval);
}
}, 15);
Track scroll-content position
This example uses the window height as the threshold for showing the scroll to top button like this:
this.ionScroll.addEventListener("scroll", () => {
if (this.ionScroll.scrollTop > window.innerHeight) {
this.showButton = true;
} else {
this.showButton = false;
}
});
Button Html
<button *ngIf="showButton" (click)="scrollToTop(1000)">Scroll Top</button>
Full component Typescript
import { NavController } from 'ionic-angular/index';
import { Component, OnInit, ElementRef } from "#angular/core";
#Component({
templateUrl:"home.html"
})
export class HomePage implements OnInit {
public ionScroll;
public showButton = false;
public contentData = [];
constructor(public myElement: ElementRef) {}
ngOnInit() {
// Ionic scroll element
this.ionScroll = this.myElement.nativeElement.children[1].firstChild;
// On scroll function
this.ionScroll.addEventListener("scroll", () => {
if (this.ionScroll.scrollTop > window.innerHeight) {
this.showButton = true;
} else {
this.showButton = false;
}
});
// Content data
for (let i = 0; i < 301; i++) {
this.contentData.push(i);
}
}
// Scroll to top function
// Adapted from https://stackoverflow.com/a/24559613/5357459
scrollToTop(scrollDuration) {
let scrollStep = -this.ionScroll.scrollTop / (scrollDuration / 15);
let scrollInterval = setInterval( () => {
if ( this.ionScroll.scrollTop != 0 ) {
this.ionScroll.scrollTop = this.ionScroll.scrollTop + scrollStep;
} else {
clearInterval(scrollInterval);
}
}, 15);
}
}
Full component Html
<ion-navbar primary *navbar>
<ion-title>
Ionic 2
</ion-title>
<button *ngIf="showButton" (click)="scrollToTop(1000)">Scroll Top</button>
</ion-navbar>
<ion-content class="has-header" #testElement>
<div padding style="text-align: center;">
<h1>Ionic 2 Test</h1>
<div *ngFor="let item of contentData">
test content-{{item}}
</div>
</div>
</ion-content>