Created
November 28, 2018 22:12
-
-
Save shobhitsharma/8bc0a7f0ffcd485c5bcbcc94c02c6682 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
import * as React from "react"; | |
const styles = { | |
wrapper: { | |
width: "100%", | |
height: "100%", | |
position: "relative" | |
}, | |
frame: { | |
width: "100%", | |
height: "100%", | |
position: "absolute" | |
} | |
}; | |
type CarouselProps = { | |
axis: "x" | "y"; | |
auto: boolean; | |
loop: boolean; | |
interval: number; | |
duration: number; | |
widgets?: Array<Function>; | |
frames?: Array<any>; | |
children?: any; | |
style?: object; | |
minMove: number; | |
onTransitionEnd?: Function; | |
}; | |
type CarouselState = { | |
frames: Array<any>; | |
total?: any; | |
current: number; | |
movingFrames?: any; | |
frameWidth: number; | |
frameHeight: number; | |
slider?: any; | |
startX: number; | |
startY: number; | |
deltaX: number; | |
deltaY: number; | |
}; | |
class Carousel extends React.Component<CarouselProps, CarouselState> { | |
public static defaultProps: CarouselProps = { | |
axis: "x", | |
auto: false, | |
loop: false, | |
interval: 5000, | |
duration: 300, | |
minMove: 42 | |
}; | |
private mounted: boolean; | |
constructor(props: CarouselProps) { | |
super(props); | |
this.state = { | |
frames: [].concat(props.frames || props.children || []), | |
frameWidth: 0, | |
frameHeight: 0, | |
startX: 0, | |
startY: 0, | |
current: 0, | |
deltaX: 0, | |
deltaY: 0 | |
}; | |
this.mounted = false; | |
this.onTouchStart = this.onTouchStart.bind(this); | |
this.onTouchMove = this.onTouchMove.bind(this); | |
this.onTouchEnd = this.onTouchEnd.bind(this); | |
this.autoSlide = this.autoSlide.bind(this); | |
this.prev = this.prev.bind(this); | |
this.next = this.next.bind(this); | |
if (props.loop === false && props.auto) { | |
console.warn("[re-carousel] Auto-slide only works in loop mode."); | |
} | |
} | |
public componentDidMount(): void { | |
this.mounted = true; | |
this.prepareAutoSlide(); | |
// Hide all frames | |
for (let i = 1; i < this.state.frames.length; i++) { | |
(this.refs["f" + i] as any).style.opacity = 0; | |
} | |
(this.refs.wrapper as any).addEventListener("touchmove", this.onTouchMove, { capture: true }); | |
(this.refs.wrapper as any).addEventListener("touchend", this.onTouchEnd, { capture: true }); | |
} | |
public componentWillUnmount(): void { | |
this.mounted = false; | |
this.clearAutoTimeout(); | |
(this.refs.wrapper as any).removeEventListener("touchmove", this.onTouchMove, { capture: true }); | |
(this.refs.wrapper as any).removeEventListener("touchend", this.onTouchEnd, { capture: true }); | |
} | |
public componentWillReceiveProps(nextProps: CarouselProps): void { | |
const frames = [].concat(nextProps.frames || nextProps.children || []); | |
this.setState({ frames }); | |
} | |
public onTouchStart(e: any): void { | |
if (this.state.total < 2) { | |
return; | |
} | |
// e.preventDefault() | |
this.clearAutoTimeout(); | |
this.updateFrameSize(() => { | |
console.log("updateFrameSize"); | |
}); | |
this.prepareSiblingFrames(); | |
const { pageX, pageY } = (e.touches && e.touches[0]) || e; | |
this.setState({ | |
startX: pageX, | |
startY: pageY, | |
deltaX: 0, | |
deltaY: 0 | |
}); | |
(this.refs.wrapper as any).addEventListener("mousemove", this.onTouchMove, { capture: true }); | |
(this.refs.wrapper as any).addEventListener("mouseup", this.onTouchEnd, { capture: true }); | |
(this.refs.wrapper as any).addEventListener("mouseleave", this.onTouchEnd, { capture: true }); | |
} | |
public onTouchMove(e: TouchEvent): void { | |
if (e.touches && e.touches.length > 1) { | |
return; | |
} | |
this.clearAutoTimeout(); | |
const { pageX, pageY } = (e.touches && e.touches[0]) || e; | |
let deltaX = pageX - this.state.startX; | |
let deltaY = pageY - this.state.startY; | |
this.setState({ | |
deltaX: deltaX, | |
deltaY: deltaY | |
}); | |
if (this.props.axis === "x" && Math.abs(deltaX) > Math.abs(deltaY)) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
} | |
if (this.props.axis === "y" && Math.abs(deltaY) > Math.abs(deltaX)) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
} | |
// when reach frames edge in non-loop mode, reduce drag effect. | |
if (!this.props.loop) { | |
if (this.state.current === this.state.frames.length - 1) { | |
if (deltaX < 0) { | |
deltaX /= 3; | |
} | |
if (deltaY < 0) { | |
deltaY /= 3; | |
} | |
} | |
if (this.state.current === 0) { | |
if (deltaX > 0) { | |
deltaX /= 3; | |
} | |
if (deltaY > 0) { | |
deltaY /= 3; | |
} | |
} | |
} | |
this.moveFramesBy(deltaX, deltaY); | |
} | |
public onTouchEnd(): void { | |
const direction = this.decideEndPosition(); | |
if (direction) { | |
this.transitFramesTowards(direction); | |
} | |
// cleanup | |
(this.refs.wrapper as any).removeEventListener("mousemove", this.onTouchMove, { capture: true }); | |
(this.refs.wrapper as any).removeEventListener("mouseup", this.onTouchEnd, { capture: true }); | |
(this.refs.wrapper as any).removeEventListener("mouseleave", this.onTouchEnd, { capture: true }); | |
setTimeout(() => this.prepareAutoSlide(), this.props.duration); | |
} | |
public decideEndPosition(): string | null { | |
const { deltaX = 0, deltaY = 0, current, frames } = this.state; | |
const { axis, loop, minMove } = this.props; | |
switch (axis) { | |
case "x": | |
if (loop === false) { | |
if (current === 0 && deltaX > 0) { | |
return "origin"; | |
} | |
if (current === frames.length - 1 && deltaX < 0) { | |
return "origin"; | |
} | |
} | |
if (Math.abs(deltaX) < minMove) { | |
return "origin"; | |
} | |
return deltaX > 0 ? "right" : "left"; | |
case "y": | |
if (loop === false) { | |
if (current === 0 && deltaY > 0) { | |
return "origin"; | |
} | |
if (current === frames.length - 1 && deltaY < 0) { | |
return "origin"; | |
} | |
} | |
if (Math.abs(deltaY) < minMove) { | |
return "origin"; | |
} | |
return deltaY > 0 ? "down" : "up"; | |
default: | |
return null; | |
} | |
} | |
public moveFramesBy(deltaX: number, deltaY: number): void { | |
const { prev, current, next } = this.state.movingFrames; | |
const { frameWidth, frameHeight } = this.state; | |
switch (this.props.axis) { | |
case "x": | |
translateXY(current, deltaX, 0); | |
if (deltaX < 0) { | |
translateXY(next, deltaX + frameWidth, 0); | |
} else { | |
translateXY(prev, deltaX - frameWidth, 0); | |
} | |
break; | |
case "y": | |
translateXY(current, 0, deltaY); | |
if (deltaY < 0) { | |
translateXY(next, 0, deltaY + frameHeight); | |
} else { | |
translateXY(prev, 0, deltaY - frameHeight); | |
} | |
break; | |
default: | |
} | |
} | |
public prepareAutoSlide(): void { | |
if (this.state.frames.length < 2) { | |
return; | |
} | |
this.clearAutoTimeout(); | |
this.updateFrameSize(() => { | |
this.prepareSiblingFrames(); | |
}); | |
// auto slide only avalible in loop mode | |
if (this.mounted && this.props.loop && this.props.auto) { | |
const slideTimeoutID = setTimeout(this.autoSlide, this.props.interval); | |
this.setState({ slider: slideTimeoutID }); | |
} | |
} | |
// auto slide to 'next' or 'prev' | |
public autoSlide(direction: string): void { | |
this.clearAutoTimeout(); | |
switch (direction) { | |
case "prev": | |
this.transitFramesTowards(this.props.axis === "x" ? "right" : "down"); | |
break; | |
case "next": | |
default: | |
this.transitFramesTowards(this.props.axis === "x" ? "left" : "up"); | |
} | |
// prepare next move after animation | |
setTimeout(() => this.prepareAutoSlide(), this.props.duration); | |
} | |
public next(): boolean | undefined { | |
const { current, frames } = this.state; | |
if (!this.props.loop && current === frames.length - 1) { | |
return false; | |
} | |
this.autoSlide("next"); | |
} | |
public prev(): boolean | undefined { | |
if (!this.props.loop && this.state.current === 0) { | |
return false; | |
} | |
const { prev, next } = this.state.movingFrames; | |
if (prev === next) { | |
// Reprepare start position of prev frame | |
// (it was positioned as "next" frame) | |
if (this.props.axis === "x") { | |
translateXY(prev, -this.state.frameWidth, 0, 0); | |
} else { | |
translateXY(prev, 0, -this.state.frameHeight, 0); | |
} | |
prev.getClientRects(); // trigger layout | |
} | |
this.autoSlide("prev"); | |
} | |
public clearAutoTimeout(): void { | |
clearTimeout(this.state.slider); | |
} | |
public updateFrameSize(cb: any): any { | |
const { width, height } = window.getComputedStyle(this.refs.wrapper as any); | |
this.setState( | |
{ | |
frameWidth: parseFloat((width || "0px").split("px")[0]), | |
frameHeight: parseFloat((height || "0px").split("px")[0]) | |
}, | |
cb | |
); | |
} | |
public getSiblingFrames(): any { | |
return { | |
current: this.refs["f" + this.getFrameId("current")], | |
prev: this.refs["f" + this.getFrameId("prev")], | |
next: this.refs["f" + this.getFrameId("next")] | |
}; | |
} | |
public prepareSiblingFrames(): void { | |
const siblings = this.getSiblingFrames(); | |
if (!this.props.loop) { | |
if (this.state.current === 0) { | |
siblings.prev = undefined; | |
} | |
if (this.state.current === this.state.frames.length - 1) { | |
siblings.next = undefined; | |
} | |
} | |
this.setState({ movingFrames: siblings }); | |
// Prepare frames position | |
translateXY(siblings.current, 0, 0); | |
if (this.props.axis === "x") { | |
translateXY(siblings.prev, -this.state.frameWidth, 0); | |
translateXY(siblings.next, this.state.frameWidth, 0); | |
} else { | |
translateXY(siblings.prev, 0, -this.state.frameHeight); | |
translateXY(siblings.next, 0, this.state.frameHeight); | |
} | |
return siblings; | |
} | |
public getFrameId(position: string): number { | |
const { frames, current } = this.state; | |
const total = frames.length; | |
switch (position) { | |
case "prev": | |
return (current - 1 + total) % total; | |
case "next": | |
return (current + 1) % total; | |
default: | |
return current; | |
} | |
} | |
public transitFramesTowards(direction: string): void { | |
const { prev, current, next } = this.state.movingFrames; | |
const { duration, axis, onTransitionEnd } = this.props; | |
let newCurrentId = this.state.current; | |
switch (direction) { | |
case "up": | |
translateXY(current, 0, -this.state.frameHeight, duration); | |
translateXY(next, 0, 0, duration); | |
newCurrentId = this.getFrameId("next"); | |
break; | |
case "down": | |
translateXY(current, 0, this.state.frameHeight, duration); | |
translateXY(prev, 0, 0, duration); | |
newCurrentId = this.getFrameId("prev"); | |
break; | |
case "left": | |
translateXY(current, -this.state.frameWidth, 0, duration); | |
translateXY(next, 0, 0, duration); | |
newCurrentId = this.getFrameId("next"); | |
break; | |
case "right": | |
translateXY(current, this.state.frameWidth, 0, duration); | |
translateXY(prev, 0, 0, duration); | |
newCurrentId = this.getFrameId("prev"); | |
break; | |
default: | |
// back to origin | |
translateXY(current, 0, 0, duration); | |
if (axis === "x") { | |
translateXY(prev, -this.state.frameWidth, 0, duration); | |
translateXY(next, this.state.frameWidth, 0, duration); | |
} else if (axis === "y") { | |
translateXY(prev, 0, -this.state.frameHeight, duration); | |
translateXY(next, 0, this.state.frameHeight, duration); | |
} | |
} | |
if (onTransitionEnd) { | |
setTimeout(() => onTransitionEnd(this.getSiblingFrames()), duration); | |
} | |
this.setState({ current: newCurrentId }); | |
} | |
// debugFrames () { | |
// console.log('>>> DEBUG-FRAMES: current', this.state.current) | |
// const len = this.state.frames.length | |
// for (let i = 0; i < len; ++i) { | |
// const ref = this.refs['f' + i] | |
// console.info(ref.innerText.trim(), ref.style.transform) | |
// } | |
// } | |
public render(): JSX.Element { | |
const { frames, current } = this.state; | |
const { widgets, axis, loop, auto, interval } = this.props; | |
const wrapperStyle = Object.assign({}, styles.wrapper, this.props.style); | |
return ( | |
<div style={wrapperStyle}> | |
<div | |
ref="wrapper" | |
style={Object.assign({}, { overflow: "hidden" }, wrapperStyle)} | |
onTouchStart={this.onTouchStart} | |
// className={this.props.className} | |
onMouseDown={this.onTouchStart} | |
> | |
{frames.map((frame: HTMLElement, i: number) => { | |
const frameStyle = Object.assign({}, { zIndex: 99 - i }, styles.frame); | |
return ( | |
<div ref={"f" + i} key={i} style={frameStyle}> | |
{frame} | |
</div> | |
); | |
})} | |
{this.props.frames && this.props.children} | |
</div> | |
{widgets && | |
[] | |
.concat(widgets as any) | |
.map((Widget: any, i: number) => ( | |
<Widget | |
key={i} | |
index={current} | |
total={frames.length} | |
prevHandler={this.prev} | |
nextHandler={this.next} | |
axis={axis} | |
loop={loop} | |
auto={auto} | |
interval={interval} | |
/> | |
))} | |
</div> | |
); | |
} | |
} | |
function translateXY(el: HTMLElement, x: number, y: number, duration: number = 0): void { | |
if (!el) { | |
return; | |
} | |
el.style.opacity = "1"; | |
el.style.transitionDuration = duration + "ms"; | |
el.style.webkitTransitionDuration = duration + "ms"; | |
el.style.transform = `translate(${x}px, ${y}px)`; | |
el.style.webkitTransform = `translate(${x}px, ${y}px) translateZ(0)`; | |
} | |
export default Carousel; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment