react-native-js-only-modal
Version:
An enhanced, animated, customizable Modal for React Native. This is a javascript only Modal
281 lines • 9.84 kB
JavaScript
import * as React from 'react';
import { useState, useEffect, useRef, useContext } from 'react';
import * as Animatable from 'react-native-animatable';
import { View, StyleSheet, Dimensions, BackHandler } from 'react-native';
import uuid from 'react-uuid';
const ContextProvider = React.createContext({});
const screenSize = (dimensions) => {
const size = {
width: Dimensions.get(dimensions ?? 'window').width,
height: Dimensions.get(dimensions ?? 'window').height,
};
return size;
};
const Provider = ({ children, zIndex, }) => {
const data = {
items: new Map(),
update: () => { },
find: (id) => {
return data.items.get(id);
},
has: (id) => data.items.has(id),
remove: (id) => {
data.items.delete(id);
},
updateProps: (props, id) => {
const item = data.find(id);
if (item)
item.component.props = props;
},
push: (item) => {
data.items.set(item.id, item);
},
zIndex: zIndex || 90000,
};
useEffect(() => {
return () => {
data.items.clear();
data.update();
};
}, []);
return (<ContextProvider.Provider value={data}>
{children}
<ModalContainer />
</ContextProvider.Provider>);
};
const ModalContainer = () => {
const [_, setUpdater] = useState(0);
const [items] = useState(new Map());
const context = useContext(ContextProvider);
context.update = () => {
setUpdater((x) => (x > 1000 ? 0 : x) + 1);
};
context.items = items;
const rItem = [];
items.forEach((x) => {
rItem.push(x);
});
return (<>
{rItem.map((x, index) => (<InternalModal key={index} item={x} zIndex={context.zIndex} index={index}/>))}
</>);
};
const InternalModal = ({ item, index, zIndex, }) => {
const [size, setSize] = useState(screenSize(item.component.props.dimensions));
const backDropRef = useRef();
const getBounds = (pos, native) => {
return {
x: pos.x,
x2: pos.x + pos.width,
y: pos.y,
y2: pos.y + pos.height,
pageX: native.nativeEvent.pageX,
pageY: native.nativeEvent.pageY,
};
};
item.component.onHide = async () => {
try {
if (backDropRef.current)
await backDropRef.current.animate('fadeOut', item.component.props.duration);
}
catch (e) {
console.log(e);
}
};
useEffect(() => {
const d = Dimensions.addEventListener('change', () => {
setSize(screenSize(item.component.props.dimensions));
});
const onBackPressed = BackHandler.addEventListener('hardwareBackPress', () => {
if (item.component.props.disableBackHandler !== true)
item.component.props.onCloseRequest?.('BackButton');
return true;
});
return () => {
d.remove();
onBackPressed.remove();
};
}, []);
const procent = ($this, value) => {
return ($this / 100) * value;
};
const styleSizeProps = {};
const stringSizeToNumber = (s, pValue) => {
if (!s)
return s;
if (typeof s === 'string' && s.indexOf('%') !== -1)
return procent(pValue, parseFloat(s.replace(/%/g, '').trim()));
return s;
};
const styleSize = (style) => {
if (!style)
return;
if (Array.isArray(style))
style.forEach((x) => styleSize(x));
else {
const keys = Object.keys(style);
const assignValue = (k, value) => {
if (keys.find((x) => x === k))
styleSizeProps[k] = value;
};
assignValue('width', stringSizeToNumber(style.width, size.width));
assignValue('minWidth', stringSizeToNumber(style.minWidth, size.width));
assignValue('minHeight', stringSizeToNumber(style.minHeight, size.height));
assignValue('height', stringSizeToNumber(style.height, size.height));
}
};
const viewStyle = () => {
styleSize(item.component.props.style);
let dstyle = Array.isArray(item.component.props.style)
? item.component.props.style
: [item.component.props.style];
dstyle = [...dstyle, { zIndex: 2, ...styleSizeProps }];
return dstyle;
};
const isOutSide = (e) => {
if (item.component.layoutData) {
const bounds = getBounds(item.component.layoutData, e);
if (bounds.pageX < bounds.x ||
bounds.pageX > bounds.x2 ||
bounds.pageY < bounds.y ||
bounds.pageY > bounds.y2) {
return true;
}
}
return false;
};
const onLayout = (e) => {
item.component.layoutData = e.nativeEvent.layout;
};
return (<>
{item.component.props.hideBackDrop != true ? (<Animatable.View ref={(c) => (backDropRef.current = c)} pointerEvents="box-none" duration={item.component.props.duration} animation={'fadeIn'} style={{
...styles.modal,
...size,
zIndex: (zIndex - 1) * (index + 1),
}}>
<View pointerEvents="box-none" style={[
styles.backDrop,
{
minHeight: size.height,
minWidth: size.width,
},
item.component.props.backDropStyle,
{
opacity: (item.component.props.backDropStyle?.opacity ??
styles.backDrop.opacity) +
(index + 1) / 100,
},
]}/>
</Animatable.View>) : null}
<Animatable.View ref={(c) => (item.component.ref = c)} easing={item.component.props.easing} direction={item.component.props.direction} duration={item.component.props.duration} animation={item.component.props.animationIn} onAnimationBegin={item.component.props.onAnimationBegin} onAnimationEnd={item.component.props.onAnimationEnd} style={[
styles.modal,
{
zIndex: zIndex * (index + 1),
...size,
},
item.component.props.containerStyle,
]} onResponderRelease={(e) => {
if (isOutSide(e))
item.component.props.onCloseRequest?.('BackDrop');
}} onStartShouldSetResponder={(e) => {
return isOutSide(e);
}}>
<View onLayout={onLayout} style={{
flexDirection: 'column',
...styleSizeProps,
backgroundColor: 'transparent',
}}>
<View {...item.component.props} style={viewStyle()}/>
</View>
</Animatable.View>
</>);
};
const Modal = ({ visible, children, onCloseRequest, style, hideBackDrop, animationIn, animationOut, duration, direction, containerStyle, backDropStyle, useNativeDriver, easing, dimensions, disableBackHandler, }) => {
const rId = useRef(uuid());
const context = useContext(ContextProvider);
if (!animationIn)
animationIn = 'slideInDown';
if (!animationOut)
animationOut = 'slideOutUp';
if (duration === undefined)
duration = 100;
useEffect(() => {
const id = new String(rId.current).toString();
if (visible) {
const props = {
visible,
children,
onCloseRequest,
style,
hideBackDrop,
animationIn,
animationOut,
duration,
direction,
containerStyle,
backDropStyle,
useNativeDriver,
easing,
dimensions,
disableBackHandler,
};
if (!context.has(id)) {
context.push({
id: id,
component: {
props: props,
},
});
}
else
context.updateProps(props, id);
context.update();
}
else {
const item = context.find(id);
rId.current = uuid(); // forget the current element
if (item && item.component.ref) {
item.component.ref.animate(animationOut, duration).then(async () => {
if (item.component.onHide)
await item.component.onHide?.();
context.remove(id);
context.update();
});
}
else {
context.remove(id);
context.update();
}
}
});
useEffect(() => {
return () => {
context.remove(rId.current);
context.update();
};
}, []);
return null;
};
const styles = StyleSheet.create({
backDrop: {
backgroundColor: '#000',
opacity: 0.7,
left: 0,
top: 0,
bottom: 0,
width: '100%',
height: '100%',
position: 'absolute',
},
modal: {
position: 'absolute',
left: 0,
top: 0,
backgroundColor: 'transparent',
justifyContent: 'center',
alignItems: 'center',
maxHeight: '100%',
minHeight: '100%',
},
});
export { Modal, Provider };
//# sourceMappingURL=index.js.map