react-native-flip-book
Version:
447 lines (445 loc) • 12.7 kB
JavaScript
"use strict";
import usePrevious from './hooks/usePrevious';
import useSetState from './hooks/useSetState';
import React, { useEffect, useState, useCallback, useMemo, createElement as _createElement } from 'react';
import { useRef } from 'react';
import { StyleSheet, View } from 'react-native';
import { BookPage } from './BookPage';
import { BookPagePortrait } from './portrait/BookPagePortrait';
import { createPages } from './utils/utils';
import { BookPageBackground } from './BookPageBackground';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const debug = true;
const logger = msg => {
if (debug) {
console.log(msg);
}
};
const PageFlipper = /*#__PURE__*/React.forwardRef(({
data,
enabled = true,
pressable = true,
singleImageMode = true,
renderLastPage,
portrait = false,
onFlippedEnd,
onFlipStart,
onPageDrag,
onPageDragEnd,
onPageDragStart,
onEndReached,
onInitialized,
renderContainer,
renderPage,
pageSize = {
height: 600,
width: 400
},
contentContainerStyle
}, ref) => {
const [{
width,
height
}, setLayout] = useState({
height: 0,
width: 0
});
const [state, setState] = useSetState({
pageIndex: 0,
pages: [],
isAnimating: false,
initialized: false,
// pageSize: { width: 0, height: 0 },
prev: {
left: '',
right: ''
},
current: {
left: '',
right: ''
},
next: {
left: '',
right: ''
},
nextPageIndex: undefined,
isPortrait: portrait
});
const isAnimatingRef = useRef(false);
const prevBookPage = useRef(null);
const nextBookPage = useRef(null);
const portraitBookPage = useRef(null);
const previousPortrait = usePrevious(portrait);
const containerSize = useMemo(() => {
if (!state.initialized) {
return {
width: 0,
height: 0
};
}
let size = {
height: pageSize.height,
width: singleImageMode && !state.isPortrait ? pageSize.width * 2 : pageSize.width
};
if (!singleImageMode && state.isPortrait) {
size = {
height: pageSize.height,
width: pageSize.width / 2
};
}
let finalSize;
// corrections
if (size.height > size.width) {
const ratio = size.height / size.width;
finalSize = {
height: width * ratio,
width
};
if (finalSize.height > height) {
const diff = finalSize.height / height;
finalSize.height = height;
finalSize.width = finalSize.width / diff;
}
} else {
const ratio = size.width / size.height;
finalSize = {
height,
width: height * ratio
};
if (finalSize.width > width) {
const diff = finalSize.width / width;
finalSize.width = width;
finalSize.height = finalSize.height / diff;
}
}
return finalSize;
}, [height, singleImageMode, width, state.initialized, state.isPortrait, pageSize.height, pageSize.width]);
useEffect(() => {
const initialize = async () => {
try {
const allPages = createPages({
portrait,
singleImageMode,
data
});
let adjustedIndex = getAdjustedIndex(allPages);
setState({
initialized: true,
pages: allPages,
prev: allPages[adjustedIndex - 1],
current: allPages[adjustedIndex],
next: allPages[adjustedIndex + 1],
pageIndex: adjustedIndex,
isPortrait: portrait
});
if (onInitialized) {
onInitialized({
pages: allPages,
index: adjustedIndex
});
}
} catch (error) {
console.error('error', error);
}
};
initialize();
// eslint-disable-next-line
}, [data, portrait, singleImageMode]);
useEffect(() => {
if (state.nextPageIndex !== undefined) {
if (!state.isPortrait) {
if (state.nextPageIndex > state.pageIndex) {
nextBookPage.current?.turnPage();
} else {
prevBookPage.current?.turnPage();
}
} else {
portraitBookPage.current?.turnPage(state.nextPageIndex > state.pageIndex ? 1 : -1);
}
}
// eslint-disable-next-line
}, [state.nextPageIndex]);
const goToPage = useCallback(index => {
if (index === undefined || index === null) {
logger('index cannot be undefined or null');
return;
}
if (typeof index !== 'number' || isNaN(index)) {
logger('index must be a number');
return;
}
if (index < 0 || index > state.pages.length - 1) {
logger('invalid page');
return;
}
if (isAnimatingRef.current) {
logger('is already animating');
return;
}
if (index === state.pageIndex) {
logger('same page');
return;
}
if (index > state.pageIndex) {
setState({
next: state.pages[index],
nextPageIndex: index
});
} else {
setState({
prev: state.pages[index],
nextPageIndex: index
});
}
}, [setState, state.pageIndex, state.pages]);
const previousPage = useCallback(() => {
const newIndex = state.pageIndex - 1;
goToPage(newIndex);
}, [goToPage, state.pageIndex]);
const nextPage = useCallback(() => {
const newIndex = state.pageIndex + 1;
goToPage(newIndex);
}, [goToPage, state.pageIndex]);
React.useImperativeHandle(ref, () => ({
goToPage,
nextPage,
previousPage
}), [goToPage, nextPage, previousPage]);
const getAdjustedIndex = allPages => {
// THIS NEEDS REWORKING
let adjustedIndex = state.pageIndex;
if (previousPortrait !== undefined && previousPortrait !== portrait && singleImageMode) {
if (portrait) {
adjustedIndex *= 2;
} else {
adjustedIndex = Math.floor(adjustedIndex % 2 === 0 ? adjustedIndex / 2 : (adjustedIndex - 1) / 2);
}
}
if (adjustedIndex < 0 || adjustedIndex > allPages.length - 1) {
// invalid index, reset to 0
adjustedIndex = 0;
}
return adjustedIndex;
};
const onLayout = e => {
setLayout({
height: e.nativeEvent.layout.height,
width: e.nativeEvent.layout.width
});
};
const onPageFlipped = useCallback(index => {
const newIndex = state.nextPageIndex !== undefined ? state.nextPageIndex : state.pageIndex + index;
if (newIndex < 0 || newIndex > state.pages.length - 1) {
// this if condition theoretically should never occur in the first place, so it could be removed but it's here just in case
logger('invalid page');
setState({
isAnimating: false,
nextPageIndex: undefined
});
isAnimatingRef.current = false;
return;
}
const prev = state.pages[newIndex - 1];
const current = state.pages[newIndex];
const next = state.pages[newIndex + 1];
setState({
pageIndex: newIndex,
isAnimating: false,
prev,
current,
next,
nextPageIndex: undefined
});
isAnimatingRef.current = false;
if (onFlippedEnd && typeof onFlippedEnd === 'function') {
onFlippedEnd(newIndex);
}
if (newIndex === state.pages.length - 1 && onEndReached) {
onEndReached();
}
}, [onEndReached, onFlippedEnd, setState, state.nextPageIndex, state.pageIndex, state.pages]);
const setIsAnimating = useCallback(val => {
setState({
isAnimating: val
});
isAnimatingRef.current = val;
}, [setState]);
const getPageStyle = (right, front) => {
if (!singleImageMode && isPortrait) {
const pageStyle = {
height: containerSize.height,
width: containerSize.width * 2,
position: 'absolute'
};
const isEvenPage = pageIndex % 2 === 0;
if (front) {
if (isEvenPage) {
pageStyle.left = right ? 0 : -containerSize.width;
} else {
pageStyle.left = right ? -containerSize.width : 0;
}
} else {
if (isEvenPage) {
pageStyle.left = right ? -containerSize.width : 0;
} else {
pageStyle.left = right ? 0 : -containerSize.width;
}
}
return pageStyle;
}
const pageStyle = {
height: containerSize.height,
width: singleImageMode && !isPortrait ? containerSize.width / 2 : containerSize.width,
position: 'absolute'
};
const offset = singleImageMode ? 0 : -containerSize.width / 2;
if (isPortrait && front) {
pageStyle.left = 0;
} else if (front && right || !front && right) {
// front right or back right
pageStyle.left = offset;
} else if (front && !right) {
// front left
pageStyle.right = offset;
}
return pageStyle;
};
if (!state.initialized) {
return null;
}
const {
current,
pageIndex,
pages,
next,
prev,
isPortrait,
isAnimating
} = state;
const isFirstPage = pageIndex === 0;
const isLastPage = pageIndex === pages.length - 1;
const isSecondToLastPage = pageIndex === pages.length - 2;
const shouldRenderLastPage = (isSecondToLastPage || isLastPage) && singleImageMode && data.length % 2 !== 0;
const bookPageProps = {
containerSize: containerSize,
isAnimating: isAnimating,
enabled,
setIsAnimating: setIsAnimating,
isAnimatingRef: isAnimatingRef,
onPageFlip: onPageFlipped,
getPageStyle,
single: singleImageMode,
onFlipStart,
onPageDrag,
onPageDragEnd,
onPageDragStart,
isPressable: pressable,
renderPage
};
const ContentWrapper = renderContainer ? renderContainer : Wrapper;
return /*#__PURE__*/_jsx(View, {
style: styles.container,
onLayout: onLayout,
children: /*#__PURE__*/_jsx(View, {
style: [styles.contentContainer, {
height: containerSize.height,
width: containerSize.width
}, contentContainerStyle],
children: /*#__PURE__*/_jsx(ContentWrapper, {
children: !isPortrait ? /*#__PURE__*/_jsxs(View, {
style: styles.content,
children: [!prev ? /*#__PURE__*/_jsx(Empty, {}) : /*#__PURE__*/_jsx(BookPage, {
ref: prevBookPage,
right: false,
front: current,
back: prev,
...bookPageProps
}, `left${pageIndex}`), !next ? /*#__PURE__*/_jsx(Empty, {}) : /*#__PURE__*/_jsx(BookPage, {
ref: nextBookPage,
right: true,
front: current,
back: next,
...bookPageProps
}, `right${pageIndex}`), /*#__PURE__*/_jsx(BookPageBackground, {
left: !prev ? current.left : prev.left,
right: !next ? current.right : next.right,
isFirstPage: isFirstPage,
isLastPage: isLastPage,
getPageStyle: getPageStyle,
containerSize: containerSize,
renderPage: renderPage,
renderLastPage: renderLastPage,
shouldRenderLastPage: shouldRenderLastPage
})]
}) : /*#__PURE__*/_jsxs(View, {
style: styles.portraitContent,
children: [/*#__PURE__*/_jsx(View, {
style: {
...StyleSheet.absoluteFillObject
},
children: /*#__PURE__*/_createElement(BookPagePortrait, {
...bookPageProps,
current: current,
prev: prev,
next: next,
onPageFlip: onPageFlipped,
key: `right${pageIndex}`,
ref: portraitBookPage
})
}), next && /*#__PURE__*/_jsx(View, {
style: {
...StyleSheet.absoluteFillObject,
zIndex: -5,
overflow: 'hidden'
},
children: renderPage && /*#__PURE__*/_jsx(View, {
style: getPageStyle(true, false),
children: renderPage(next.right)
})
})]
})
})
})
});
});
export default /*#__PURE__*/React.memo(PageFlipper);
const Wrapper = props => /*#__PURE__*/_jsx(View, {
style: styles.wrap,
...props
});
const Empty = () => /*#__PURE__*/_jsx(View, {
style: styles.container,
pointerEvents: "none"
});
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
wrap: {
flex: 1
},
contentContainer: {
flexDirection: 'row',
// shadowColor: '#000',
// shadowOffset: {
// width: 0,
// height: 2,
// },
// shadowOpacity: 0.25,
// shadowRadius: 3.84,
// elevation: 5,
backgroundColor: 'white'
},
content: {
flex: 1,
flexDirection: 'row',
overflow: 'hidden'
},
portraitContent: {
flex: 1,
overflow: 'hidden'
}
});
//# sourceMappingURL=index.js.map