Grabbed dndbeyond's source code
``` ~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js ```
This commit is contained in:
@@ -0,0 +1,570 @@
|
||||
import {
|
||||
spring,
|
||||
TransitionMotion,
|
||||
TransitionStyle,
|
||||
} from "@serprex/react-motion";
|
||||
import { uniqueId, throttle } from "lodash";
|
||||
import React from "react";
|
||||
import { DOWN, SwipeEventData, UP } from "react-swipeable";
|
||||
|
||||
import { Swipeable } from "~/components/Swipeable";
|
||||
|
||||
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
|
||||
import { ComponentCarouselItem } from "./ComponentCarouselItem";
|
||||
|
||||
interface CarouselWindowItem {
|
||||
offset: number;
|
||||
idx: number;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
activeItemKey: string;
|
||||
onItemChange?: (newKey: string | null, oldKey: string | null) => void;
|
||||
overscanAmount: number;
|
||||
changingWaitTime: number;
|
||||
changeStartDelta: number;
|
||||
resizeThrottleTime: number;
|
||||
resizeBlockWaitTime: number;
|
||||
envDimensions: AppEnvDimensionsState;
|
||||
children:
|
||||
| Array<React.ReactElement<ComponentCarouselItem>>
|
||||
| React.ReactElement<ComponentCarouselItem>;
|
||||
}
|
||||
|
||||
interface WindowsItemState {
|
||||
windowItems: Array<CarouselWindowItem>;
|
||||
currentIdx: number;
|
||||
swipedAmount: number;
|
||||
}
|
||||
interface State extends WindowsItemState {
|
||||
activeIdx: number;
|
||||
placeholderWidth: number;
|
||||
filteredChildren: Array<ComponentCarouselItem>;
|
||||
childByIdxLookup: Record<number, ComponentCarouselItem>;
|
||||
childrenCount: number;
|
||||
isChanging: boolean;
|
||||
isResizeBlocked: boolean;
|
||||
}
|
||||
export default class ComponentCarousel extends React.PureComponent<
|
||||
Props,
|
||||
State
|
||||
> {
|
||||
static defaultProps = {
|
||||
overscanAmount: 2,
|
||||
changingWaitTime: 550,
|
||||
changeStartDelta: 75,
|
||||
resizeThrottleTime: 300,
|
||||
resizeBlockWaitTime: 2000,
|
||||
};
|
||||
|
||||
placeholderRef = React.createRef<HTMLDivElement>();
|
||||
changingRafId: number | null = null;
|
||||
changingStartTime: number | null = null;
|
||||
resizeBlockRafId: number | null = null;
|
||||
resizeBlockStartTime: number | null = null;
|
||||
orientationHandler: () => void;
|
||||
resizeHandler: () => void;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeIdx: 0,
|
||||
currentIdx: 0,
|
||||
swipedAmount: 0,
|
||||
placeholderWidth: 0,
|
||||
childByIdxLookup: {},
|
||||
childrenCount: 0,
|
||||
windowItems: [],
|
||||
filteredChildren: [],
|
||||
isChanging: false,
|
||||
isResizeBlocked: false,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Props, state: State): State {
|
||||
const { children, activeItemKey } = props;
|
||||
|
||||
let activeIdx: number = 0;
|
||||
|
||||
//TODO need to refactor to better handle this and not require any
|
||||
let filteredChildren: Array<any> = [];
|
||||
if (children) {
|
||||
filteredChildren = React.Children.toArray(children).filter(
|
||||
(child: any) => !!child && child.props.isEnabled
|
||||
);
|
||||
}
|
||||
React.Children.forEach(filteredChildren, (child, idx) => {
|
||||
if (!child) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { itemKey, isEnabled } = child.props;
|
||||
|
||||
if (isEnabled && itemKey === activeItemKey) {
|
||||
activeIdx = idx;
|
||||
}
|
||||
});
|
||||
|
||||
let childByIdxLookup: Record<number, ComponentCarouselItem> =
|
||||
filteredChildren.reduce(
|
||||
(acc, child, idx) => ({
|
||||
...acc,
|
||||
[idx]: child,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
let newState: State = {
|
||||
...state,
|
||||
activeIdx,
|
||||
currentIdx: activeIdx,
|
||||
filteredChildren,
|
||||
childrenCount: filteredChildren.length,
|
||||
childByIdxLookup,
|
||||
};
|
||||
|
||||
let windowItemsState: WindowsItemState = state.isChanging
|
||||
? state
|
||||
: ComponentCarousel.deriveWindowItemsState(props, newState);
|
||||
|
||||
return {
|
||||
...newState,
|
||||
...windowItemsState,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initResizeHandlers();
|
||||
this.updatePlaceholderDimensions();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.destroyResizeHandlers();
|
||||
this.stopChangeCheckLoop();
|
||||
this.stopResizeBlockCheckLoop();
|
||||
}
|
||||
|
||||
destroyResizeHandlers = (): void => {
|
||||
window.removeEventListener("resize", this.resizeHandler);
|
||||
window.removeEventListener("orientationchange", this.orientationHandler);
|
||||
};
|
||||
|
||||
initResizeHandlers = (): void => {
|
||||
const { resizeThrottleTime, envDimensions } = this.props;
|
||||
|
||||
this.resizeHandler = throttle(() => {
|
||||
if (!this.state.isResizeBlocked) {
|
||||
this.updatePlaceholderDimensions();
|
||||
}
|
||||
}, resizeThrottleTime);
|
||||
|
||||
this.orientationHandler = throttle(() => {
|
||||
let oldSize: number = envDimensions ? envDimensions.window.width : 0;
|
||||
let count: number = 0;
|
||||
let orientationIntervalId: number | undefined;
|
||||
orientationIntervalId = window.setInterval(() => {
|
||||
let newEnvWidth = envDimensions ? envDimensions.window.width : oldSize;
|
||||
if (oldSize !== newEnvWidth || count > 15) {
|
||||
window.clearInterval(orientationIntervalId);
|
||||
return;
|
||||
}
|
||||
if (oldSize !== window.innerWidth) {
|
||||
this.updatePlaceholderDimensions();
|
||||
}
|
||||
count++;
|
||||
}, 100);
|
||||
}, resizeThrottleTime);
|
||||
|
||||
window.addEventListener("resize", this.resizeHandler);
|
||||
window.addEventListener("orientationchange", this.orientationHandler);
|
||||
};
|
||||
|
||||
updatePlaceholderDimensions = (): void => {
|
||||
if (this.placeholderRef.current) {
|
||||
let placeholderWidth = this.placeholderRef.current.offsetWidth;
|
||||
this.setState(
|
||||
{
|
||||
placeholderWidth,
|
||||
},
|
||||
this.updateWindowItems
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getItemKeyByIdx = (itemIdx: number): string | null => {
|
||||
const { filteredChildren } = this.state;
|
||||
|
||||
let itemKey: string | null = null;
|
||||
|
||||
React.Children.forEach(filteredChildren, (child, idx) => {
|
||||
if (itemIdx === idx) {
|
||||
itemKey = child.props.itemKey;
|
||||
}
|
||||
});
|
||||
|
||||
return itemKey;
|
||||
};
|
||||
|
||||
static getSwipedItemPosIdx = (
|
||||
activeIdx: number,
|
||||
placeholderWidth: number,
|
||||
swipedAmount: number
|
||||
): number => {
|
||||
if (placeholderWidth === 0) {
|
||||
return 0;
|
||||
}
|
||||
let swipedSlideCount = Math.round(swipedAmount / placeholderWidth);
|
||||
return activeIdx - swipedSlideCount; // subtract because of swipe direction makes it negative going right
|
||||
};
|
||||
|
||||
// Need to be able to get what array index to use based on a position index that is both positive, zero, and negative
|
||||
// example: [a, b, c, d, e, f, g, h] (length = 8)
|
||||
// position index: -11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10
|
||||
// array index: 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2
|
||||
//
|
||||
// position index "-9" gives letter "h"
|
||||
static getArrayIdxByPositionIdx = (
|
||||
posIndex: number,
|
||||
total: number
|
||||
): number => {
|
||||
if (posIndex >= 0) {
|
||||
return posIndex % total;
|
||||
} else {
|
||||
const indexMod = Math.abs(posIndex) % total;
|
||||
return (indexMod === 0 ? 0 : 1) * (total - indexMod);
|
||||
}
|
||||
};
|
||||
|
||||
runChangeCheck = (callback: () => void): void => {
|
||||
const { changingWaitTime } = this.props;
|
||||
|
||||
if (
|
||||
this.changingStartTime !== null &&
|
||||
Number(new Date()) - this.changingStartTime > changingWaitTime
|
||||
) {
|
||||
callback();
|
||||
} else {
|
||||
this.changingRafId = window.requestAnimationFrame(
|
||||
this.runChangeCheck.bind(this, callback)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
startChangeCheckLoop = (callback: () => void): void => {
|
||||
this.changingStartTime = Number(new Date());
|
||||
this.stopChangeCheckLoop();
|
||||
this.changingRafId = window.requestAnimationFrame(
|
||||
this.runChangeCheck.bind(this, callback)
|
||||
);
|
||||
};
|
||||
|
||||
stopChangeCheckLoop = (): void => {
|
||||
if (this.changingRafId !== null) {
|
||||
window.cancelAnimationFrame(this.changingRafId);
|
||||
}
|
||||
};
|
||||
|
||||
startResizeBlockCheckLoop = (callback: () => void): void => {
|
||||
this.resizeBlockStartTime = Number(new Date());
|
||||
this.stopResizeBlockCheckLoop();
|
||||
this.resizeBlockRafId = window.requestAnimationFrame(
|
||||
this.runResizeBlockCheck.bind(this, callback)
|
||||
);
|
||||
};
|
||||
|
||||
stopResizeBlockCheckLoop = (): void => {
|
||||
if (this.resizeBlockRafId !== null) {
|
||||
window.cancelAnimationFrame(this.resizeBlockRafId);
|
||||
}
|
||||
};
|
||||
|
||||
runResizeBlockCheck = (callback: () => void): void => {
|
||||
const { resizeBlockWaitTime } = this.props;
|
||||
|
||||
if (
|
||||
this.resizeBlockStartTime !== null &&
|
||||
Number(new Date()) - this.resizeBlockStartTime > resizeBlockWaitTime
|
||||
) {
|
||||
callback();
|
||||
} else {
|
||||
this.resizeBlockRafId = window.requestAnimationFrame(
|
||||
this.runResizeBlockCheck.bind(this, callback)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static deriveWindowItemsState = (
|
||||
props: Props,
|
||||
state: State
|
||||
): WindowsItemState => {
|
||||
const { overscanAmount } = props;
|
||||
const {
|
||||
windowItems,
|
||||
placeholderWidth,
|
||||
swipedAmount,
|
||||
childrenCount,
|
||||
currentIdx,
|
||||
} = state;
|
||||
|
||||
let currentPosIdx: number = ComponentCarousel.getSwipedItemPosIdx(
|
||||
currentIdx,
|
||||
placeholderWidth,
|
||||
swipedAmount
|
||||
);
|
||||
let startIdx: number = currentPosIdx - overscanAmount;
|
||||
let endIdx: number = currentPosIdx + overscanAmount;
|
||||
|
||||
let newWindowItems: Array<CarouselWindowItem> = [];
|
||||
if (childrenCount > 0) {
|
||||
let offset: number = -1 * overscanAmount;
|
||||
for (let i = startIdx; i <= endIdx; i++) {
|
||||
let itemIdx = ComponentCarousel.getArrayIdxByPositionIdx(
|
||||
i,
|
||||
childrenCount
|
||||
);
|
||||
let existingItem = windowItems.find(
|
||||
(windowItem) => windowItem.idx === itemIdx
|
||||
);
|
||||
|
||||
newWindowItems.push({
|
||||
offset,
|
||||
idx: itemIdx,
|
||||
key: existingItem ? existingItem.key : uniqueId(),
|
||||
});
|
||||
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
windowItems: newWindowItems,
|
||||
currentIdx: currentPosIdx,
|
||||
swipedAmount: 0,
|
||||
};
|
||||
};
|
||||
|
||||
updateWindowItems = (startDelayedChange: boolean = false): void => {
|
||||
const { childrenCount, activeIdx } = this.state;
|
||||
|
||||
let windowItemsState = ComponentCarousel.deriveWindowItemsState(
|
||||
this.props,
|
||||
this.state
|
||||
);
|
||||
|
||||
this.setState(windowItemsState);
|
||||
|
||||
if (startDelayedChange) {
|
||||
this.startChangeCheckLoop(() => {
|
||||
let newActiveIdx = ComponentCarousel.getArrayIdxByPositionIdx(
|
||||
windowItemsState.currentIdx,
|
||||
childrenCount
|
||||
);
|
||||
this.handleItemChange(newActiveIdx, activeIdx);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleSwiping = ({ deltaX, dir }: SwipeEventData): void => {
|
||||
const isVerticalChange = [UP, DOWN].includes(dir);
|
||||
|
||||
if (!isVerticalChange) {
|
||||
this.stopChangeCheckLoop();
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
swipedAmount: deltaX,
|
||||
isChanging: true,
|
||||
isResizeBlocked: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleSwipedHorizontal = ({
|
||||
event,
|
||||
deltaX,
|
||||
velocity,
|
||||
}: SwipeEventData): void => {
|
||||
const isFlick = velocity > 0.1;
|
||||
const { placeholderWidth } = this.state;
|
||||
const { changeStartDelta } = this.props;
|
||||
|
||||
let newSwipedAmount: number = 0;
|
||||
if (isFlick) {
|
||||
if (deltaX > 0) {
|
||||
newSwipedAmount = placeholderWidth;
|
||||
} else if (deltaX < 0) {
|
||||
newSwipedAmount = -1 * placeholderWidth;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(
|
||||
(prevState) => {
|
||||
let isChanging: boolean =
|
||||
isFlick || Math.abs(deltaX) <= changeStartDelta;
|
||||
return {
|
||||
swipedAmount: isFlick ? newSwipedAmount : prevState.swipedAmount,
|
||||
isChanging,
|
||||
isResizeBlocked: isChanging,
|
||||
};
|
||||
},
|
||||
() => {
|
||||
this.updateWindowItems(true);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
handleItemChange = (newItemIdx: number, oldItemIdx: number): void => {
|
||||
const { onItemChange } = this.props;
|
||||
|
||||
if (onItemChange) {
|
||||
onItemChange(
|
||||
this.getItemKeyByIdx(newItemIdx),
|
||||
this.getItemKeyByIdx(oldItemIdx)
|
||||
);
|
||||
this.stopChangeCheckLoop();
|
||||
this.setState({
|
||||
activeIdx: newItemIdx,
|
||||
isChanging: false,
|
||||
});
|
||||
|
||||
this.startResizeBlockCheckLoop(() => {
|
||||
this.setState({
|
||||
isResizeBlocked: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handlePlaceholderClick = (newIdx: number, evt: React.MouseEvent): void => {
|
||||
const { activeIdx } = this.state;
|
||||
const { onItemChange } = this.props;
|
||||
|
||||
if (onItemChange) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
this.handleItemChange(newIdx, activeIdx);
|
||||
}
|
||||
};
|
||||
|
||||
static getItemPositionX = (
|
||||
swipedAmount: number,
|
||||
offset: number,
|
||||
placeholderWidth: number
|
||||
): number => {
|
||||
let itemPositionX = swipedAmount + offset * placeholderWidth;
|
||||
|
||||
if (itemPositionX >= 0) {
|
||||
return Math.min(placeholderWidth * 2, itemPositionX);
|
||||
} else {
|
||||
return Math.max(-1 * placeholderWidth * 2, itemPositionX);
|
||||
}
|
||||
};
|
||||
|
||||
renderPlaceholders = (): React.ReactNode => {
|
||||
const {
|
||||
childByIdxLookup,
|
||||
windowItems,
|
||||
swipedAmount,
|
||||
placeholderWidth,
|
||||
isChanging,
|
||||
} = this.state;
|
||||
|
||||
let placeholderStyles: Array<TransitionStyle> = windowItems.map(
|
||||
(windowItem) => {
|
||||
const child = childByIdxLookup[windowItem.idx];
|
||||
|
||||
return {
|
||||
key: windowItem.key,
|
||||
style: {
|
||||
transform: spring(
|
||||
ComponentCarousel.getItemPositionX(
|
||||
swipedAmount,
|
||||
windowItem.offset,
|
||||
placeholderWidth
|
||||
),
|
||||
{ stiffness: 400, damping: 30 }
|
||||
),
|
||||
},
|
||||
data: {
|
||||
offset: windowItem.offset,
|
||||
itemIdx: windowItem.idx,
|
||||
child,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
let classNames: Array<string> = ["ct-component-carousel__placeholders"];
|
||||
if (isChanging) {
|
||||
classNames.push("ct-component-carousel__placeholders--changing");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")} ref={this.placeholderRef}>
|
||||
<TransitionMotion styles={placeholderStyles}>
|
||||
{(interpolatedStyles) => (
|
||||
<React.Fragment>
|
||||
{interpolatedStyles.map((config) => {
|
||||
const { PlaceholderComponent, placeholderProps, itemKey } =
|
||||
config.data.child.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ct-component-carousel__placeholder"
|
||||
key={config.key}
|
||||
onClick={this.handlePlaceholderClick.bind(
|
||||
this,
|
||||
config.data.itemIdx
|
||||
)}
|
||||
style={{
|
||||
transform: `translateX(${config.style.transform}px)`,
|
||||
}}
|
||||
>
|
||||
<PlaceholderComponent {...placeholderProps} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</TransitionMotion>
|
||||
</div>
|
||||
);
|
||||
//`
|
||||
};
|
||||
|
||||
renderActiveComponent = (): React.ReactNode => {
|
||||
const { activeIdx, childByIdxLookup, filteredChildren } = this.state;
|
||||
|
||||
if (activeIdx === null || !filteredChildren.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let child = childByIdxLookup[activeIdx];
|
||||
const { ContentComponent } = child.props;
|
||||
|
||||
return (
|
||||
<div className="ct-component-carousel__active">
|
||||
<ContentComponent key={child.props.itemKey} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Swipeable
|
||||
handlerFunctions={{
|
||||
onSwiping: this.handleSwiping,
|
||||
onSwipedLeft: this.handleSwipedHorizontal,
|
||||
onSwipedRight: this.handleSwipedHorizontal,
|
||||
}}
|
||||
>
|
||||
<div className="ct-component-carousel">
|
||||
{this.renderPlaceholders()}
|
||||
{this.renderActiveComponent()}
|
||||
</div>
|
||||
</Swipeable>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user