Skip to content

Instantly share code, notes, and snippets.

@embarq
Last active July 9, 2020 14:44
Show Gist options
  • Save embarq/d74379a5a78b3956c7f9a77eea610040 to your computer and use it in GitHub Desktop.
Save embarq/d74379a5a78b3956c7f9a77eea610040 to your computer and use it in GitHub Desktop.
// @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()
})
<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>
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