hypertoxin
Version:
A themeable ReactNative component library for developing native apps
975 lines (922 loc) • 35.4 kB
JavaScript
/**
* Copyright 2016-present Tuan Le.
*
* Licensed under the MIT License.
* You may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/mit-license.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*------------------------------------------------------------------------
*
* @module BodyScreen
* @description - Body screen component.
*
* @author Tuan Le (tuan.t.lei@gmail.com)
*
* @flow
*/
; // eslint-disable-line
import React from 'react';
import ReactNative from 'react-native'; // eslint-disable-line
import PropTypes from 'prop-types';
import { View as AnimatedView } from 'react-native-animatable';
import {
DefaultTheme,
DefaultThemeContext
} from '../../themes/default-theme';
const {
Dimensions,
KeyboardAvoidingView,
ImageBackground,
View,
ScrollView,
PanResponder
} = ReactNative;
const DEVICE_WIDTH = Dimensions.get(`window`).width;
const DEVICE_HEIGHT = Dimensions.get(`window`).height;
const DEFAULT_ANIMATION_DURATION_MS = 300;
const DEFAULT_BODY_SCREEN_STYLE = {
container: {
flex: 1,
flexDirection: `column`,
justifyContent: `flex-start`,
alignItems: `flex-start`,
width: DEVICE_WIDTH,
height: DEVICE_HEIGHT
},
contentTopRoom: {
flexDirection: `column`,
backgroundColor: `transparent`
},
contentMiddleRoom: {
flexDirection: `column`,
backgroundColor: `transparent`
},
contentBottomRoom: {
flexDirection: `column`,
backgroundColor: `transparent`
}
};
const readjustStyle = (newStyle = {
shade: `themed`,
overlay: `themed`,
color: `themed`,
padding: `themed`,
roomAlignment: `start`,
contentTopRoomAlignment: `none`,
contentMiddleRoomAlignment: `none`,
contentBottomRoomAlignment: `none`
}, prevAdjustedStyle = DEFAULT_BODY_SCREEN_STYLE, Theme = DefaultTheme) => {
const {
shade,
overlay,
color,
padding,
roomAlignment,
contentTopRoomAlignment,
contentMiddleRoomAlignment,
contentBottomRoomAlignment,
style
} = newStyle;
const themedShade = shade === `themed` ? Theme.screen.body.shade : shade;
const themedOverlay = overlay === `themed` ? Theme.screen.body.overlay : overlay;
const nullPadding = {
padding: null,
paddingTop: null,
paddingBottom: null,
paddingLeft: null,
paddingRight: null,
paddingHorizontal: null,
paddingVertical: null
};
let themedColor;
let themedPadding;
let themedRoomAlignment = {};
let themedContentTopRoomAlignment = {};
let themedContentMiddleRoomAlignment = {};
let themedContentBottomRoomAlignment = {};
if (color === `themed`) {
if (Theme.screen.color.body.hasOwnProperty(Theme.screen.body.color)) {
themedColor = Theme.screen.color.body[Theme.screen.body.color][themedShade];
} else {
themedColor = Theme.screen.body.color;
}
} else if (Theme.screen.color.body.hasOwnProperty(color)) {
themedColor = Theme.screen.color.body[color][themedShade];
} else {
themedColor = color;
}
if (typeof padding === `string`) {
if (padding === `themed`) {
if (typeof Theme.screen.body.padding === `string` && Theme.screen.padding.bodyhasOwnProperty(Theme.screen.body.padding)) {
themedPadding = Theme.screen.padding.body[Theme.screen.body.padding];
} else {
themedPadding = Theme.screen.body.padding;
}
} else if (Theme.screen.padding.bodyhasOwnProperty(padding)) {
themedPadding = Theme.screen.padding.body[padding];
} else {
themedPadding = 0;
}
} else {
themedPadding = padding;
}
if (typeof themedPadding === `number`) {
themedPadding = {
...nullPadding,
padding: themedPadding
};
} else if (typeof themedPadding === `object`) {
themedPadding = Object.entries(themedPadding).reduce((_themedPadding, [ key, value ]) => {
let _padding = nullPadding;
_padding[`padding${key.charAt(0).toUpperCase()}${key.slice(1)}`] = value;
_themedPadding = {
..._themedPadding,
..._padding
};
return _themedPadding;
}, nullPadding);
}
switch (themedOverlay) { // eslint-disable-line
case `opaque`:
break;
case `translucent`:
themedColor = `${themedColor}${Theme.screen.color.body.opacity}`;
break;
case `transparent`:
themedColor = `transparent`;
break;
}
switch (roomAlignment) { // eslint-disable-line
case `start`:
themedRoomAlignment = {
justifyContent: `flex-start`
};
break;
case `center`:
themedRoomAlignment = {
justifyContent: `center`
};
break;
case `end`:
themedRoomAlignment = {
justifyContent: `flex-end`
};
break;
case `stretch`:
themedRoomAlignment = {
justifyContent: `space-between`
};
break;
}
switch (contentTopRoomAlignment) { // eslint-disable-line
case `start`:
themedContentTopRoomAlignment = {
flexShrink: 1,
justifyContent: `flex-start`,
alignSelf: `flex-start`,
alignItems: `flex-start`
};
break;
case `center`:
themedContentTopRoomAlignment = {
flexShrink: 1,
justifyContent: `center`,
alignSelf: `center`,
alignItems: `center`
};
break;
case `end`:
themedContentTopRoomAlignment = {
flexShrink: 1,
justifyContent: `flex-end`,
alignSelf: `flex-end`,
alignItems: `flex-end`
};
break;
case `stretch`:
themedContentTopRoomAlignment = {
flexShrink: 1,
justifyContent: `space-between`,
alignSelf: `stretch`,
alignItems: `stretch`
};
break;
case `none`:
break;
}
switch (contentMiddleRoomAlignment) { // eslint-disable-line
case `start`:
themedContentMiddleRoomAlignment = {
flexShrink: 1,
justifyContent: `flex-start`,
alignSelf: `flex-start`,
alignItems: `flex-start`
};
break;
case `center`:
themedContentMiddleRoomAlignment = {
flexShrink: 1,
justifyContent: `center`,
alignSelf: `center`,
alignItems: `center`
};
break;
case `end`:
themedContentMiddleRoomAlignment = {
flexShrink: 1,
justifyContent: `flex-end`,
alignSelf: `flex-end`,
alignItems: `flex-end`
};
break;
case `stretch`:
themedContentMiddleRoomAlignment = {
flexShrink: 1,
justifyContent: `space-between`,
alignSelf: `stretch`,
alignItems: `stretch`
};
break;
case `none`:
break;
}
switch (contentBottomRoomAlignment) { // eslint-disable-line
case `start`:
themedContentBottomRoomAlignment = {
flexShrink: 1,
justifyContent: `flex-start`,
alignSelf: `flex-start`,
alignItems: `flex-start`
};
break;
case `center`:
themedContentBottomRoomAlignment = {
flexShrink: 1,
justifyContent: `center`,
alignSelf: `center`,
alignItems: `center`
};
break;
case `end`:
themedContentBottomRoomAlignment = {
flexShrink: 1,
justifyContent: `flex-end`,
alignSelf: `flex-end`,
alignItems: `flex-end`
};
break;
case `stretch`:
themedContentBottomRoomAlignment = {
flexShrink: 1,
justifyContent: `space-between`,
alignSelf: `stretch`,
alignItems: `stretch`
};
break;
case `none`:
break;
}
return {
container: {
...prevAdjustedStyle.container,
...themedRoomAlignment,
...themedPadding,
backgroundColor: themedColor,
...(typeof style === `object` && style.hasOwnProperty(`container`) && typeof style.container === `object` ? style.container : {})
},
contentTopRoom: {
...prevAdjustedStyle.contentTopRoom,
...themedContentTopRoomAlignment,
...(typeof style === `object` && style.hasOwnProperty(`contentTopRoom`) && typeof style.contentTopRoom === `object` ? style.contentTopRoom : {})
},
contentMiddleRoom: {
...prevAdjustedStyle.contentMiddleRoom,
...themedContentMiddleRoomAlignment,
...(typeof style === `object` && style.hasOwnProperty(`contentMiddleRoom`) && typeof style.contentMiddleRoom === `object` ? style.contentMiddleRoom : {})
},
contentBottomRoom: {
...prevAdjustedStyle.contentBottomRoom,
...themedContentBottomRoomAlignment,
...(typeof style === `object` && style.hasOwnProperty(`contentBottomRoom`) && typeof style.contentBottomRoom === `object` ? style.contentBottomRoom : {})
}
};
};
const ImageURISourcePropType = PropTypes.shape({
uri: PropTypes.string,
bundle: PropTypes.string,
method: PropTypes.string,
headers: PropTypes.objectOf(PropTypes.string),
body: PropTypes.string,
cache: PropTypes.oneOf([
`default`,
`reload`,
`force-cache`,
`only-if-cached`
]),
width: PropTypes.number,
height: PropTypes.number,
scale: PropTypes.number
});
export default class BodyScreen extends React.Component {
static contextType = DefaultThemeContext
static propTypes = {
shade: PropTypes.oneOf([ `themed`, `light`, `dark` ]),
overlay: PropTypes.oneOf([ `themed`, `opaque`, `translucent`, `transparent` ]),
color: PropTypes.string,
pading: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.shape({
top: PropTypes.number,
bottom: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
horizontal: PropTypes.number,
vertical: PropTypes.number
})
]),
coverImageSource: PropTypes.oneOfType([
() => null,
PropTypes.string,
PropTypes.number,
ImageURISourcePropType,
PropTypes.arrayOf(ImageURISourcePropType)
]),
roomAlignment: PropTypes.oneOf([ `start`, `center`, `end`, `stretch` ]),
contentTopRoomAlignment: PropTypes.oneOf([ `none`, `start`, `center`, `end`, `stretch` ]),
contentMiddleRoomAlignment: PropTypes.oneOf([ `none`, `start`, `center`, `end`, `stretch` ]),
contentBottomRoomAlignment: PropTypes.oneOf([ `none`, `start`, `center`, `end`, `stretch` ]),
scrollable: PropTypes.bool,
keyboardAvoiding: PropTypes.bool,
onScroll: PropTypes.func
}
static defaultProps = {
shade: `themed`,
overlay: `themed`,
color: `themed`,
pading: `themed`,
coverImageSource: null,
roomAlignment: `start`,
contentTopRoomAlignment: `none`,
contentMiddleRoomAlignment: `none`,
contentBottomRoomAlignment: `none`,
scrollable: false,
keyboardAvoiding: false,
onScroll: () => null
}
static getDerivedStateFromProps (props, state) {
const {
shade,
overlay,
color,
pading,
roomAlignment,
contentTopRoomAlignment,
contentMiddleRoomAlignment,
contentBottomRoomAlignment,
style
} = props;
const {
Theme
} = state.context;
return {
adjustedStyle: readjustStyle({
shade,
overlay,
color,
pading,
roomAlignment,
contentTopRoomAlignment,
contentMiddleRoomAlignment,
contentBottomRoomAlignment,
style
}, state.adjustedStyle, Theme)
};
}
constructor (props) {
super(props);
const component = this;
component.refCache = {};
component.panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => false,
onMoveShouldSetPanResponderCapture: () => false,
onPanResponderMove: (event, gestureState) => {
let scrollDirection = 0;
if (gestureState.vy > 0) {
scrollDirection = 1;
} else if (gestureState.vy < 0) {
scrollDirection = -1;
}
component.setState(() => {
return {
scrollEvent: {
direction: scrollDirection
// pageX: event.nativeEvent.pageX,
// pageY: event.nativeEvent.pageY,
// locationX: event.nativeEvent.locationX,
// locationY: event.nativeEvent.locationY
}
};
});
}
});
component.state = {
context: {
Theme: DefaultTheme
},
adjustedStyle: DEFAULT_BODY_SCREEN_STYLE,
scrollEvent: 0
};
}
getScrollEvent = () => {
const component = this;
const {
scrollEvent
} = component.state;
return scrollEvent;
}
scrollTo = (destination = {
x: 0,
y: 0,
animated: true
}) => {
const component = this;
const {
scrollable
} = component.props;
const scrollView = component.refCache[`scroll-view`];
if (scrollable && scrollView !== undefined) {
const {
x,
y,
animated
} = destination;
scrollView.scrollTo({
x,
y,
animated
});
}
}
animate (animation = {
onTransitionBegin: () => null,
onTransitionEnd: () => null,
onAnimationBegin: () => null,
onAnimationEnd: () => null
}) {
const component = this;
const {
Theme
} = component.context;
if (typeof animation === `string` && animation !== `none`) {
const animationName = animation.replace(/-([a-z])/g, (match, word) => word.toUpperCase());
if (Theme.screen.animation.body.hasOwnProperty(animationName)) {
animation = Theme.screen.animation.body[animationName];
}
}
if (typeof animation === `object`) {
const {
refName,
transitions,
onTransitionBegin,
onTransitionEnd,
onAnimationBegin,
onAnimationEnd
} = animation;
const componentRef = component.refCache[refName];
if (componentRef !== undefined && Array.isArray(transitions)) {
let transitionDuration = 0;
const transitionPromises = transitions.map((transition, transitionIndex) => {
let transitionBeginPromise;
let transitionEndPromise;
if (typeof transition === `object`) {
let transitionType;
let componentRefTransition = {
from: {},
to: {}
};
let componentRefTransitionOption = {
duration: DEFAULT_ANIMATION_DURATION_MS,
delay: 0,
easing: `linear`
};
if (transition.hasOwnProperty(`from`)) {
let from = typeof transition.from === `function` ? transition.from(component.props, component.state, component.context) : transition.from;
componentRefTransition.from = typeof from === `object` ? from : {};
transitionType = `from`;
}
if (transition.hasOwnProperty(`to`)) {
let to = typeof transition.to === `function` ? transition.to(component.props, component.state, component.context) : transition.to;
componentRefTransition.to = typeof to === `object` ? to : {};
transitionType = transitionType === `from` ? `from-to` : `to`;
}
if (transition.hasOwnProperty(`option`) && typeof transition.option === `object`) {
componentRefTransitionOption = {
...componentRefTransitionOption,
...transition.option
};
}
transitionBeginPromise = new Promise((resolve) => {
setTimeout(() => {
if (transitionType === `to`) {
componentRef.transitionTo(
componentRefTransition.to,
componentRefTransitionOption.duration,
componentRefTransitionOption.easing,
componentRefTransitionOption.delay
);
} else if (transitionType === `from-to`) {
setTimeout(() => {
componentRef.transition(
componentRefTransition.from,
componentRefTransition.to,
componentRefTransitionOption.duration,
componentRefTransitionOption.easing
);
}, componentRefTransitionOption.delay);
}
(typeof onTransitionBegin === `function` ? onTransitionBegin : () => null)(transitionIndex);
resolve((_onTransitionBegin) => (typeof _onTransitionBegin === `function` ? _onTransitionBegin : () => null)(_onTransitionBegin));
}, transitionDuration + 5);
});
transitionDuration += componentRefTransitionOption.duration + componentRefTransitionOption.delay;
transitionEndPromise = new Promise((resolve) => {
setTimeout(() => {
(typeof onTransitionEnd === `function` ? onTransitionEnd : () => null)(transitionIndex);
resolve((_onTransitionEnd) => (typeof _onTransitionEnd === `function` ? _onTransitionEnd : () => null)(transitionIndex));
}, transitionDuration);
});
}
return [ transitionBeginPromise, transitionEndPromise ];
});
const animationBeginPromise = new Promise((resolve) => {
(typeof onAnimationBegin === `function` ? onAnimationBegin : () => null)();
resolve((_onAnimationBegin) => (typeof _onAnimationBegin === `function` ? _onAnimationBegin : () => null)());
});
const animationEndPromise = new Promise((resolve) => {
setTimeout(() => {
(typeof onAnimationEnd === `function` ? onAnimationEnd : () => null)();
resolve((_onAnimationEnd) => (typeof _onAnimationEnd === `function` ? _onAnimationEnd : () => null)());
}, transitionDuration + 5);
});
return Promise.all([
animationBeginPromise,
...transitionPromises.flat(),
animationEndPromise
].filter((animationPromise) => animationPromise !== undefined));
}
}
}
componentDidMount () {
const component = this;
const {
shade,
overlay,
color,
pading,
roomAlignment,
contentTopRoomAlignment,
contentMiddleRoomAlignment,
contentBottomRoomAlignment,
style
} = component.props;
const {
Theme
} = component.context;
component.setState((prevState) => {
return {
context: {
Theme
},
adjustedStyle: readjustStyle({
shade,
overlay,
color,
pading,
roomAlignment,
contentTopRoomAlignment,
contentMiddleRoomAlignment,
contentBottomRoomAlignment,
style
}, prevState.adjustedStyle, Theme)
};
});
}
componentWillUnMount () {
const component = this;
component.panResponder = null;
component.refCache = {};
}
renderContentChildren (contentTopChildren = null, contentMiddleChildren = null, contentBottomChildren = null) {
const component = this;
const {
adjustedStyle
} = component.state;
return ([
<AnimatedView
key = 'animated-content-top-room-view'
ref = {(componentRef) => {
component.refCache[`animated-content-top-room-view`] = componentRef;
}}
useNativeDriver = { true }
style = { adjustedStyle.contentTopRoom }
>
{
contentTopChildren
}
</AnimatedView>,
<AnimatedView
key = 'animated-content-middle-room-view'
ref = {(componentRef) => {
component.refCache[`animated-content-middle-room-view`] = componentRef;
}}
useNativeDriver = { true }
style = { adjustedStyle.contentMiddleRoom }
>
{
contentMiddleChildren
}
</AnimatedView>,
<AnimatedView
key = 'animated-content-bottom-room-view'
ref = {(componentRef) => {
component.refCache[`animated-content-bottom-room-view`] = componentRef;
}}
useNativeDriver = { true }
style = { adjustedStyle.contentBottomRoom }
>
{
contentBottomChildren
}
</AnimatedView>
]);
}
render () {
const component = this;
const {
shade,
coverImageSource,
scrollable,
keyboardAvoiding,
onScroll,
children
} = component.props;
const {
adjustedStyle,
scrollEvent
} = component.state;
let contentTopChildren = null;
let contentMiddleChildren = null;
let contentBottomChildren = null;
if (React.Children.count(children) > 0) {
let fragments = React.Children.toArray(React.Children.map(children, (child) => {
if (child !== null) {
const {
exclusions,
room
} = child.props;
if (typeof room === `string` && (room === `content-top` || room === `content-middle` || room === `content-bottom`)) {
const inheritedProps = Array.isArray(exclusions) ? Object.entries({
shade
}).filter(([
propName,
prop // eslint-disable-line
]) => exclusions.every((exclusion) => propName !== exclusion)).reduce((_inheritedProps, [ propName, prop ]) => {
_inheritedProps[propName] = prop;
return _inheritedProps;
}, {}) : {
shade
};
return React.cloneElement(child, inheritedProps);
}
}
return child;
}));
contentTopChildren = fragments.filter((child) => {
if (child !== null) {
const {
room
} = child.props;
return room === `content-top`;
}
return false;
});
contentTopChildren = contentTopChildren.length > 0 ? contentTopChildren : null;
contentMiddleChildren = fragments.filter((child) => {
if (child !== null) {
const {
room
} = child.props;
return room === `content-middle`;
}
return false;
});
contentMiddleChildren = contentMiddleChildren.length > 0 ? contentMiddleChildren : null;
contentBottomChildren = fragments.filter((child) => {
if (child !== null) {
const {
room
} = child.props;
return room === `content-bottom`;
}
return false;
});
contentBottomChildren = contentBottomChildren.length > 0 ? contentBottomChildren : null;
}
if (keyboardAvoiding) {
if (scrollable) {
if (coverImageSource !== null) {
return (
<ImageBackground
style = { adjustedStyle.container }
source = {
typeof coverImageSource === `string` ? {
uri: coverImageSource,
cache: `only-if-cached`
} : coverImageSource
}
resizeMode = 'cover'
>
<KeyboardAvoidingView
style = {{
backgroundColor: `transparent`
}}
behavior = 'padding'
keyboardVerticalOffset = { 0 }
>
<ScrollView
ref = {(componentRef) => {
component.refCache[`scroll-view`] = componentRef;
}}
style = {{
backgroundColor: `transparent`
}}
directionalLockEnabled = { true }
scrollEventThrottle = { 16 }
onScroll = {() => {
onScroll(scrollEvent);
}}
{ ...component.panResponder.panHandlers }
>
{
component.renderContentChildren(contentTopChildren, contentMiddleChildren, contentBottomChildren)
}
</ScrollView>
</KeyboardAvoidingView>
</ImageBackground>
);
}
return (
<KeyboardAvoidingView
style = { adjustedStyle.container }
behavior = 'padding'
keyboardVerticalOffset = { 0 }
>
<ScrollView
ref = {(componentRef) => {
component.refCache[`scroll-view`] = componentRef;
}}
directionalLockEnabled = { true }
scrollEventThrottle = { 16 }
onScroll = {() => {
onScroll(scrollEvent);
}}
{ ...component.panResponder.panHandlers }
>
{
component.renderContentChildren(contentTopChildren, contentMiddleChildren, contentBottomChildren)
}
</ScrollView>
</KeyboardAvoidingView>
);
}
if (coverImageSource !== null) {
return (
<ImageBackground
style = { adjustedStyle.container }
source = {
typeof coverImageSource === `string` ? {
uri: coverImageSource,
cache: `only-if-cached`
} : coverImageSource
}
resizeMode = 'cover'
>
<KeyboardAvoidingView
style = {{
backgroundColor: `transparent`
}}
behavior = 'padding'
keyboardVerticalOffset = { 0 }
>
{
component.renderContentChildren(contentTopChildren, contentMiddleChildren, contentBottomChildren)
}
</KeyboardAvoidingView>
</ImageBackground>
);
}
return (
<KeyboardAvoidingView
style = { adjustedStyle.container }
behavior = 'padding'
keyboardVerticalOffset = { 0 }
>
{
component.renderContentChildren(contentTopChildren, contentMiddleChildren, contentBottomChildren)
}
</KeyboardAvoidingView>
);
}
if (scrollable) {
if (coverImageSource !== null) {
return (
<ImageBackground
style = { adjustedStyle.container }
source = {
typeof coverImageSource === `string` ? {
uri: coverImageSource,
cache: `only-if-cached`
} : coverImageSource
}
resizeMode = 'cover'
>
<ScrollView
ref = {(componentRef) => {
component.refCache[`scroll-view`] = componentRef;
}}
style = {{
backgroundColor: `transparent`
}}
directionalLockEnabled = { true }
scrollEventThrottle = { 16 }
onScroll = {() => {
onScroll(scrollEvent);
}}
{ ...component.panResponder.panHandlers }
>
{
component.renderContentChildren(contentTopChildren, contentMiddleChildren, contentBottomChildren)
}
</ScrollView>
</ImageBackground>
);
}
return (
<View style = { adjustedStyle.container }>
<ScrollView
ref = {(componentRef) => {
component.refCache[`scroll-view`] = componentRef;
}}
style = {{
backgroundColor: `transparent`
}}
directionalLockEnabled = { true }
scrollEventThrottle = { 16 }
onScroll = {() => {
onScroll(scrollEvent);
}}
{ ...component.panResponder.panHandlers }
>
{
component.renderContentChildren(contentTopChildren, contentMiddleChildren, contentBottomChildren)
}
</ScrollView>
</View>
);
}
if (coverImageSource !== null) {
return (
<ImageBackground
style = { adjustedStyle.container }
source = {
typeof coverImageSource === `string` ? {
uri: coverImageSource,
cache: `only-if-cached`
} : coverImageSource
}
resizeMode = 'cover'
>
{
component.renderContentChildren(contentTopChildren, contentMiddleChildren, contentBottomChildren)
}
</ImageBackground>
);
}
return (
<View style = { adjustedStyle.container }>
{
component.renderContentChildren(contentTopChildren, contentMiddleChildren, contentBottomChildren)
}
</View>
);
}
}