Last active
July 9, 2020 14:44
-
-
Save embarq/d74379a5a78b3956c7f9a77eea610040 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @ts-check | |
import { Subject, from, fromEvent, timer } from 'rxjs' | |
import { filter, throttle, map, switchMap, tap, first, takeUntil } from 'rxjs/operators' | |
import gsap from 'gsap' | |
import './style.css' | |
console.clear() | |
class Slider { | |
constructor(container, childSelector, freeScrollClassName = 'freeScroll') { | |
if (container == null || childSelector == null) { | |
throw new Error('Invalid arguments') | |
} | |
this.isAtive = false | |
this.freeScroll = false | |
this.freeScrollClassName = freeScrollClassName | |
this.container = container | |
this.childSelector = childSelector | |
this.index = 0 | |
this.updateSlidesParams() | |
} | |
updateSlidesParams() { | |
const items = Array.from(this.container.querySelectorAll(this.childSelector)) | |
this.items = items.length | |
this.coords = items.map(el => el.offsetTop) | |
this.sizes = items.map(el => el.clientHeight) | |
} | |
animateContent(target) { | |
} | |
isOverscroll(target) { | |
return ( | |
target.scrollTop >= this.sizes[this.index] - this.container.clientHeight || | |
target.scrollTop <= 0 | |
) | |
} | |
/** update slider state. toggle freeScroll for oversized sections */ | |
afterSlide(next = () => {}) { | |
// in case current section height is larger than viewport it should be scrollable | |
this.freeScroll = this.sizes[this.index] > this.container.clientHeight | |
const currentItem = this.container.children.item(this.index) | |
if (this.freeScroll) { | |
// make an oversized section to be scrollable area | |
// by default it will add the following styles: { overflow-y: auto; height: 100% } | |
currentItem.classList.add(this.freeScrollClassName) | |
// determine when user attempts to scroll-out through the top or bottom boundary. | |
// emits when scrollTop reach a top or bottom scroll boundary. | |
const oversroll$ = fromEvent(currentItem.parentElement, 'mousewheel').pipe( | |
filter((e) => e.deltaY < -4 || e.deltaY > 4), | |
// detect wheel direction | |
map(event => { | |
const delta = event.wheelDelta / 120 | |
return [event, delta > 0 ? -1 : delta < 0 ? 1 : 0] | |
}), | |
// test if scrollTop is close to top or bottom boundary | |
filter(([e, direction]) => { | |
const {scrollTop} = e.target.parentElement | |
const maxScrollTop = this.sizes[this.index] - this.container.clientHeight | |
return ( | |
(direction > 0 && scrollTop >= maxScrollTop) || | |
(direction < 0 && scrollTop <= 0) | |
) | |
}) | |
) | |
// stores specific direction computed by overscroll$ | |
let nextDirection = NaN | |
fromEvent(currentItem, 'scroll') | |
.pipe( | |
// gate to the observable cancelation(takeUntil) allows actual scroll | |
filter(e => this.isOverscroll(e.target)), | |
takeUntil( | |
oversroll$.pipe( | |
first(), | |
tap(([e, direction]) => nextDirection = direction) | |
) | |
) | |
) | |
.subscribe( | |
() => { | |
this.freeScroll = false | |
currentItem.classList.remove(this.freeScrollClassName) | |
}, | |
null, | |
() => { | |
this.freeScroll = false | |
currentItem.classList.remove(this.freeScrollClassName) | |
if (!Number.isNaN(nextDirection)) { | |
this.slide(nextDirection) | |
} else if (currentItem.scrollTop < 1) { | |
this.slide(-1) | |
} else { | |
this.slide(1) | |
} | |
} | |
) | |
} else { | |
currentItem.classList.remove(this.freeScrollClassName) | |
} | |
this.isActive = false | |
next() | |
} | |
/** play sliding animation. run Slider.afterSlide() after animation complete */ | |
slideTo(direction) { | |
this.isActive = true | |
return new Promise(resolve => | |
gsap.to( | |
this.container, | |
{ | |
y: -(this.coords[this.index]), | |
duration: 1, | |
ease: "power1.inOut", | |
onComplete: () => this.afterSlide(resolve) | |
} | |
) | |
) | |
} | |
/** | |
* @param {number} dir - sliding direction. -1 for slide-down and 1 for slide-up | |
*/ | |
async slide(dir) { | |
if (this.isActive || this.freeScroll) return; | |
const index = this.index + dir | |
if ( | |
// keep index changes in range | |
this.items <= index || index < 0 || | |
this.index === index | |
) { | |
return; | |
} | |
this.index = index | |
this.animateContent(index, dir) | |
return this.slideTo(dir) | |
} | |
init() { | |
fromEvent(el, 'mousewheel') | |
.pipe( | |
filter((e) => ( | |
// too small changes in mousewheel delta | |
(e.deltaY < -4 || e.deltaY > 4) && | |
// a slide transition is in progress | |
!slider.isActive | |
)), | |
// detect wheel direction | |
map(event => { | |
const delta = event.wheelDelta / 120 | |
return delta > 0 ? -1 : delta < 0 ? 1 : 0 | |
}), | |
// don't action until animation end | |
throttle((dir) => | |
from(slider.slide(dir)).pipe( | |
switchMap(() => timer(100)) | |
) | |
) | |
) | |
.subscribe() | |
} | |
} | |
const el = document.querySelector('.scrollable-content') | |
const slider = new Slider(el, '.box') | |
slider.animateContent = function(index, direction) { | |
gsap.fromTo( | |
`.box-${(index + direction) + 1} h1`, | |
{ | |
y: -200, | |
opacity: 0, | |
rotation: 0, | |
}, | |
{ | |
y: 0, | |
opacity: 1, | |
rotation: 360, | |
delay: 0, | |
duration: 1, | |
overwrite: 'auto' | |
} | |
) | |
} | |
slider.init() | |
fromEvent(window, 'resize').subscribe(() => { | |
slider.updateSlidesParams() | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="scrollable"> | |
<div class="scrollable-content"> | |
<div class="box box-1"> | |
<h1>1</h1> | |
</div> | |
<div class="content"> | |
<div class="box box-2"> | |
<h1>2</h1> | |
</div> | |
</div> | |
<div class="box box-3"> | |
<h1>3</h1> | |
</div> | |
<div class="box box-4"> | |
<h1>4</h1> | |
</div> | |
<div class="box box-5"> | |
<h1>5</h1> | |
</div> | |
</div> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
html { | |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; | |
} | |
html, body { | |
margin: 0; | |
padding: 0; | |
} | |
h1 { | |
font-size: 10rem; | |
} | |
.scrollable { | |
height: 100vh; | |
width: 100%; | |
overflow: hidden; | |
} | |
.scrollable-content { | |
width: 100%; | |
height: 100%; | |
} | |
.box { | |
display: grid; | |
place-items: center; | |
padding: 0 10%; | |
height: 100vh; | |
color: #fff; | |
/* outline: 4px dashed white; */ | |
} | |
.box-1 { | |
background-image: linear-gradient(#3c8f97, #3c61c7); | |
} | |
.box-2 { | |
background-image: linear-gradient(#976cd8, #3c8f97); | |
height: 200vh; | |
} | |
.box-3 { | |
background-image: linear-gradient(#7c85c7, #3ca5c7); | |
} | |
.box-4 { | |
background-image: linear-gradient(#3c65c7, #7c85c7); | |
height: 70vh; | |
} | |
.box-5 { | |
background-image: linear-gradient(#3c8f97, #3c65c7); | |
} | |
.content { | |
height: 100%; | |
overflow: hidden | |
} | |
.freeScroll { | |
overflow-y: auto; | |
height: 100%; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment