Skip to content

Instantly share code, notes, and snippets.

@shobhitsharma
Created December 3, 2018 19:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shobhitsharma/17bd1361e33e7d41947c7b40f15ad91c to your computer and use it in GitHub Desktop.
Save shobhitsharma/17bd1361e33e7d41947c7b40f15ad91c to your computer and use it in GitHub Desktop.
import * as React from "react";
import styled from "styled-components";
const CarouselWrapper = styled.div<any>`
display: flex;
height: 100%;
`;
const CarouselFrame = styled.div<any>`
position: relative;
`;
const ButtonNavigator = styled.div`
position: fixed;
top: 0;
z-index: 101;
`;
type CarouselProps = {
axis: "x" | "y";
duration: number;
startAt: number;
frames?: React.Component<{}>[];
children?: any;
style?: object;
minMove: number;
onTransitionEnd?: Function;
};
type CarouselState = {
frames: React.Component<{}>[];
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",
startAt: 0,
duration: 300,
minMove: 42
};
private mounted: boolean;
constructor(props: CarouselProps) {
super(props);
this.state = {
frames: [].concat(props.frames || props.children || []),
frameWidth: 580,
frameHeight: 480,
startX: 0,
startY: 0,
current: props.startAt,
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.onKeydown = this.onKeydown.bind(this);
this.prev = this.prev.bind(this);
this.next = this.next.bind(this);
}
public componentWillReceiveProps(nextProps: CarouselProps): void {
const frames = [].concat(nextProps.frames || nextProps.children || []);
this.setState({ frames });
}
public componentDidMount(): void {
this.mounted = true;
window.addEventListener("resize", this.onResize.bind(this), true);
(this.refs.wrapper as any).addEventListener("keydown", this.onKeydown.bind(this), true);
(this.refs.wrapper as any).addEventListener("touchmove", this.onTouchMove, { capture: true });
(this.refs.wrapper as any).addEventListener("touchend", this.onTouchEnd, { capture: true });
}
public render(): JSX.Element {
console.log("==== CAROUSEL LIBRARY RENDERED", this.props, this.state);
const { frames, current } = this.state;
const { axis } = this.props;
return (
<React.Fragment>
<ButtonNavigator>
<button onClick={this.prev}>Prev</button>
<button onClick={this.next}>Next</button>
</ButtonNavigator>
<CarouselWrapper
ref={"wrapper"}
tabIndex={0}
onTouchStart={this.onTouchStart}
onMouseDown={this.onTouchStart}
>
{frames.map((frame: React.Component, i: number) => {
return (
<CarouselFrame ref={"f" + i} key={i}>
{frame}
</CarouselFrame>
);
})}
{this.props.frames && this.props.children}
</CarouselWrapper>
</React.Fragment>
);
}
public componentWillUnmount(): void {
this.mounted = false;
window.removeEventListener("resize", this.onResize.bind(this), true);
(this.refs.wrapper as any).removeEventListener("keydown", this.onKeydown.bind(this), true);
(this.refs.wrapper as any).removeEventListener("touchmove", this.onTouchMove, { capture: true });
(this.refs.wrapper as any).removeEventListener("touchend", this.onTouchEnd, { capture: true });
}
public onResize(e: Event): void {
console.log("====== RESIZE", e);
}
public onTouchStart(e: React.TouchEvent | React.MouseEvent): void {
if (this.state.total < 2) {
return;
}
this.updateFrameSize(() => {
console.log("updateFrameSize", this.state, this.props);
});
this.prepareSiblingFrames();
const { pageX, pageY } = ((e as React.TouchEvent).touches && (e as React.TouchEvent).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: React.TouchEvent): void {
if (e.touches && e.touches.length > 1) {
return;
}
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.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);
}
(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 });
}
public onKeydown(event: React.MouseEvent): void {
const key = (event as any).keyCode || (event as any).which;
this.updateFrameSize(() => {
console.log("updateFrameSize", this.state, this.props);
});
this.prepareSiblingFrames();
switch (key) {
case 37:
case 38:
this.prev();
break;
case 39:
case 40:
this.next();
break;
default:
break;
}
}
public next(): boolean | undefined {
const { current, frames } = this.state;
if (current === frames.length - 1) {
return false;
}
this.transitFramesTowards(this.props.axis === "x" ? "left" : "up");
}
public prev(): boolean | undefined {
if (this.state.current === 0) {
return false;
}
let prev, current, next;
if (!this.state.movingFrames) {
const frames = this.getSiblingFrames();
prev = frames.prev;
current = frames.current;
next = frames.next;
} else {
const movingFrames = this.state.movingFrames;
prev = movingFrames.prev;
current = movingFrames.current;
next = movingFrames.next;
}
if (prev === next) {
if (this.props.axis === "x") {
translateXY(prev, -this.state.frameWidth, 0, 0);
} else {
translateXY(prev, 0, -this.state.frameHeight, 0);
}
prev.getClientRects();
}
this.transitFramesTowards(this.props.axis === "x" ? "right" : "down");
}
private 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;
}
}
private getSiblingFrames(): any {
return {
current: this.refs[`f${this.getFrameId("current")}`],
prev: this.refs[`f${this.getFrameId("prev")}`],
next: this.refs[`f${this.getFrameId("next")}`]
};
}
private decideEndPosition(): string | null {
const { deltaX = 0, deltaY = 0, current, frames } = this.state;
const { axis, minMove } = this.props;
switch (axis) {
case "x":
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 (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;
}
}
private 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:
}
}
private 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
);
}
private prepareSiblingFrames(): void {
const siblings = this.getSiblingFrames();
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;
}
private transitFramesTowards(direction: string): void {
let prev, current, next;
if (!this.state.movingFrames) {
const frames = this.getSiblingFrames();
prev = frames.prev;
current = frames.current;
next = frames.next;
} else {
const movingFrames = this.state.movingFrames;
prev = movingFrames.prev;
current = movingFrames.current;
next = movingFrames.next;
}
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:
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 });
}
}
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