add a custom SideMenu to replace drawer navigator

This commit is contained in:
ammarahm-ed
2020-11-20 16:21:24 +05:00
parent f2cd8f69d9
commit 38d889a5d9
12 changed files with 544 additions and 110 deletions

View File

@@ -4,8 +4,9 @@ import ScrollableTabView from 'react-native-scrollable-tab-view';
import ContextMenu from './src/components/ContextMenu';
import {DialogManager} from './src/components/DialogManager';
import {DummyText} from './src/components/DummyText';
import {Menu} from './src/components/Menu';
import SideMenu from './src/components/SideMenu';
import {Toast} from './src/components/Toast';
import {NavigationStack} from './src/navigation/Drawer';
import {NavigatorStack} from './src/navigation/NavigatorStack';
import {useTracked} from './src/provider';
import {Actions} from './src/provider/Actions';
@@ -23,24 +24,30 @@ import {
eOpenFullscreenEditor,
eOpenSideMenu,
} from './src/utils/Events';
import {editorRef, tabBarRef} from './src/utils/Refs';
import {editorRef, sideMenuRef, tabBarRef} from './src/utils/Refs';
import {EditorWrapper} from './src/views/Editor/EditorWrapper';
import {getIntent, getNote, post} from './src/views/Editor/Functions';
let {width, height} = Dimensions.get('window');
let movedAway = true;
let layoutTimer = null;
const onChangeTab = async (obj) => {
console.log(obj.i,obj.from,'tab changed')
if (obj.i === 1) {
eSendEvent(eCloseSideMenu);
sideMenuRef.current?.setGestureEnabled(false);
if (getIntent()) return;
movedAway = false;
if (!editing.currentlyEditing || !getNote()) {
eSendEvent(eOnLoadNote, {type: 'new'});
editing.currentlyEditing = true;
}
} else {
if (obj.from === 1) {
movedAway = true;
editing.currentlyEditing = false;
post('blur');
eSendEvent(eOpenSideMenu);
}
sideMenuRef.current?.setGestureEnabled(true);
}
};
@@ -103,83 +110,127 @@ const AppStack = React.memo(
}, []);
const _onLayout = async (event) => {
console.log(editing.currentlyEditing);
if (editing.currentlyEditing) return;
let size = event?.nativeEvent?.layout;
if (!size) return;
setDimensions({
width: size.width,
height: size.height,
});
setWidthHeight(size);
DDS.setSize(size);
DDS.checkSmallTab(size.width > size.height ? 'LANDSCAPE' : 'PORTRAIT');
if (DDS.isLargeTablet()) {
setMode('tablet');
dispatch({type: Actions.FULLSCREEN, state: false});
editorRef.current?.setNativeProps({
style: {
position: 'relative',
width: size.width * 0.55,
zIndex: null,
paddingHorizontal: 0,
},
});
} else if (DDS.isSmallTab) {
setMode('smallTablet');
dispatch({type: Actions.FULLSCREEN, state: false});
editorRef.current?.setNativeProps({
style: {
position: 'relative',
width: size.width,
zIndex: null,
paddingHorizontal: 0,
},
});
if (editing.currentlyEditing) {
tabBarRef.current?.goToPage(1);
}
} else {
setMode('mobile');
dispatch({type: Actions.FULLSCREEN, state: false});
editorRef.current?.setNativeProps({
style: {
position: 'relative',
width: size.width,
zIndex: null,
paddingHorizontal: 0,
},
});
if (editing.currentlyEditing) {
tabBarRef.current?.goToPage(1);
}
if (layoutTimer) {
clearTimeout(layoutTimer);
layoutTimer = null;
}
let size = event?.nativeEvent?.layout;
if (!size || (size.width === dimensions.width && mode !== null)) {
return;
}
layoutTimer = setTimeout(async () => {
eSendEvent(eCloseSideMenu);
setDimensions({
width: size.width,
height: size.height,
});
setWidthHeight(size);
DDS.setSize(size);
console.log(size.width, size.height);
DDS.checkSmallTab(size.width > size.height ? 'LANDSCAPE' : 'PORTRAIT');
if (DDS.isLargeTablet()) {
setMode('tablet');
sideMenuRef.current?.setGestureEnabled(false);
dispatch({type: Actions.DEVICE_MODE, state: 'tablet'});
dispatch({type: Actions.FULLSCREEN, state: false});
editorRef.current?.setNativeProps({
style: {
position: 'relative',
width: size.width * 0.55,
zIndex: null,
paddingHorizontal: 0,
},
});
} else if (DDS.isSmallTab) {
setMode('smallTablet');
dispatch({type: Actions.DEVICE_MODE, state: 'smallTablet'});
dispatch({type: Actions.FULLSCREEN, state: false});
sideMenuRef.current?.setGestureEnabled(false);
editorRef.current?.setNativeProps({
style: {
position: 'relative',
width: size.width,
zIndex: null,
paddingHorizontal: 0,
},
});
if (editing.currentlyEditing && !movedAway) {
tabBarRef.current?.goToPage(1);
}
} else {
sideMenuRef.current?.setGestureEnabled(true);
setMode('mobile');
dispatch({type: Actions.DEVICE_MODE, state: 'mobile'});
dispatch({type: Actions.FULLSCREEN, state: false});
editorRef.current?.setNativeProps({
style: {
position: 'relative',
width: size.width,
zIndex: null,
paddingHorizontal: 0,
},
});
if (editing.currentlyEditing && !movedAway) {
tabBarRef.current?.goToPage(1);
}
}
eSendEvent(eOpenSideMenu);
}, 500);
};
return (
<>
<View
style={{
position: 'absolute',
width: '400%',
height: '400%',
backgroundColor: colors.bg,
}}
/>
<SideMenu
ref={sideMenuRef}
containerStyle={{
backgroundColor: colors.bg,
}}
menu={mode === 'mobile' ? <Menu /> : null}>
<ScrollableTabView
ref={tabBarRef}
style={{
zIndex:2
zIndex: 1,
}}
initialPage={0}
prerenderingSiblingsNumber={Infinity}
onChangeTab={onChangeTab}
renderTabBar={() => <></>}>
{ mode !== 'tablet' && (
<NavigationStack component={NavigatorStack} />
{mode && mode !== 'tablet' && (
<View
style={{
width: dimensions.width,
height: '100%',
borderRightColor: colors.nav,
borderRightWidth: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
}}>
{mode && mode === 'smallTablet' && (
<View
style={{
height: '100%',
width: dimensions.width * 0.35,
}}>
<Menu />
</View>
)}
<View
style={{
height: '100%',
width:
mode === 'mobile'
? dimensions.width
: dimensions.width * 0.65,
}}>
<NavigatorStack />
</View>
</View>
)}
<View
style={{
width: '100%',
@@ -195,16 +246,35 @@ const AppStack = React.memo(
height: '100%',
borderRightColor: colors.nav,
borderRightWidth: 1,
flexDirection: 'row',
alignItems: 'center',
}}>
<NavigationStack component={NavigatorStack} />
<View
style={{
height: '100%',
width: dimensions.width * 0.15,
}}>
<Menu />
</View>
<View
style={{
height: '100%',
width: dimensions.width * 0.3,
}}>
<NavigatorStack />
</View>
</View>
)}
<EditorWrapper dimensions={dimensions} />
{mode && <EditorWrapper dimensions={dimensions} />}
</View>
</ScrollableTabView>
</>
</SideMenu>
);
},
() => true,
);
/**
*
*/

View File

@@ -1,17 +1,18 @@
import React from 'react';
import { useTracked } from '../../provider';
import { DDS } from '../../services/DeviceDetection';
import {useTracked} from '../../provider';
import {DDS} from '../../services/DeviceDetection';
import NavigationService from '../../services/Navigation';
import { SIZE } from '../../utils/SizeUtils';
import { ActionIcon } from '../ActionIcon';
import {sideMenuRef} from '../../utils/Refs';
import {SIZE} from '../../utils/SizeUtils';
import {ActionIcon} from '../ActionIcon';
export const HeaderLeftMenu = () => {
const [state] = useTracked();
const {colors, headerMenuState, currentScreen} = state;
const {colors, headerMenuState, currentScreen, deviceMode} = state;
const onLeftButtonPress = () => {
if (headerMenuState) {
NavigationService.openDrawer();
sideMenuRef.current?.openMenu(true);
return;
}
NavigationService.goBack();
@@ -19,7 +20,7 @@ export const HeaderLeftMenu = () => {
return (
<>
{!DDS.isTab || currentScreen === 'search' ? (
{deviceMode === 'mobile' || currentScreen === 'search' ? (
<ActionIcon
testID="left_menu_button"
customStyle={{

View File

@@ -9,6 +9,7 @@ import { PressableButton } from '../PressableButton';
import {COLORS_NOTE} from "../../utils/Colors";
import {SIZE, WEIGHT} from "../../utils/SizeUtils";
import Paragraph from '../Typography/Paragraph';
import { sideMenuRef } from '../../utils/Refs';
export const ColorSection = ({noTextMode}) => {
const [state, dispatch] = useTracked();
@@ -45,7 +46,7 @@ export const ColorSection = ({noTextMode}) => {
NavigationService.navigate('NotesPage', params);
eSendEvent(refreshNotesPage, params);
NavigationService.closeDrawer();
sideMenuRef.current?.openMenu(false);
}
return (

View File

@@ -7,6 +7,7 @@ import {DDS} from '../../services/DeviceDetection';
import {eSendEvent} from '../../services/EventManager';
import NavigationService from '../../services/Navigation';
import {eClearSearch} from '../../utils/Events';
import { sideMenuRef } from '../../utils/Refs';
import {SIZE} from '../../utils/SizeUtils';
import {PressableButton} from '../PressableButton';
import Paragraph from '../Typography/Paragraph';
@@ -34,7 +35,7 @@ export const MenuListItem = ({item, index, noTextMode, ignore, testID}) => {
NavigationService.navigate(item.name);
}
if (item.close) {
NavigationService.closeDrawer();
sideMenuRef.current?.openMenu(false)
}
};

View File

@@ -5,6 +5,7 @@ import {Actions} from '../../provider/Actions';
import {eSendEvent} from '../../services/EventManager';
import NavigationService from '../../services/Navigation';
import {refreshNotesPage} from '../../utils/Events';
import { sideMenuRef } from '../../utils/Refs';
import {SIZE, WEIGHT} from '../../utils/SizeUtils';
import {PressableButton} from '../PressableButton';
import Paragraph from '../Typography/Paragraph';
@@ -36,7 +37,7 @@ export const TagsSection = () => {
});
NavigationService.navigate('NotesPage', params);
eSendEvent(refreshNotesPage, params);
NavigationService.closeDrawer();
sideMenuRef.current?.openMenu(false)
};
return (

View File

@@ -0,0 +1,326 @@
// @flow
import PropTypes from 'prop-types';
import React from 'react';
import {
Dimensions,
PanResponder,
TouchableWithoutFeedback,
View,
} from 'react-native';
import Animated, {Easing} from 'react-native-reanimated';
import styles from './styles';
const deviceScreen = Dimensions.get('window');
const barrierForward = deviceScreen.width / 5;
function shouldOpenMenu(dx) {
return dx > barrierForward;
}
export default class SideMenu extends React.Component {
constructor(props) {
super(props);
this.prevLeft = 0;
this.isOpen = !!props.isOpen;
this.isGestureEnabled = true;
this.overlay;
this.opacity = new Animated.Value(0);
const initialMenuPositionMultiplier =
props.menuPosition === 'right' ? -1 : 1;
const openOffsetMenuPercentage = props.openMenuOffset / deviceScreen.width;
const hiddenMenuOffsetPercentage =
props.hiddenMenuOffset / deviceScreen.width;
const left = new Animated.Value(
props.isOpen
? props.openMenuOffset * initialMenuPositionMultiplier
: props.hiddenMenuOffset,
);
this.onLayoutChange = this.onLayoutChange.bind(this);
this.onStartShouldSetResponderCapture = props.onStartShouldSetResponderCapture.bind(
this,
);
this.onMoveShouldSetPanResponder = this.handleMoveShouldSetPanResponder.bind(
this,
);
this.onPanResponderMove = this.handlePanResponderMove.bind(this);
this.onPanResponderRelease = this.handlePanResponderEnd.bind(this);
this.onPanResponderTerminate = this.handlePanResponderEnd.bind(this);
this.state = {
width: deviceScreen.width,
height: deviceScreen.height,
openOffsetMenuPercentage,
openMenuOffset: deviceScreen.width * openOffsetMenuPercentage,
hiddenMenuOffsetPercentage,
hiddenMenuOffset: deviceScreen.width * hiddenMenuOffsetPercentage,
left,
};
}
onLayoutChange(e) {
const {width, height} = e.nativeEvent.layout;
const openMenuOffset = width * this.state.openOffsetMenuPercentage;
const hiddenMenuOffset = width * this.state.hiddenMenuOffsetPercentage;
this.setState({width, height, openMenuOffset, hiddenMenuOffset});
}
/**
* Get content view. This view will be rendered over menu
* @return {React.Component}
*/
getContentView() {
//let overlay = null;
const {width, height} = this.state;
const ref = (sideMenu) => (this.sideMenu = sideMenu);
const style = [
styles.frontView,
{width, height},
this.props.animationStyle(this.state.left),
this.props.containerStyle
];
return (
<Animated.View style={style} ref={ref} {...this.responder.panHandlers}>
{this.props.children}
<TouchableWithoutFeedback onPress={() => this.openMenu(false)}>
<Animated.View
ref={(ref) => (this.overlay = ref)}
onTouchStart={() => {}}
style={{
display: 'none',
position: 'relative',
top: 0,
right: 0,
left: 0,
bottom: 0,
backgroundColor: 'black',
opacity: this.opacity,
zIndex: 1,
}}
/>
</TouchableWithoutFeedback>
</Animated.View>
);
}
changeOpacity(opacity) {
if (opacity === 0.5) {
this.overlay.setNativeProps({
style: {
display: 'flex',
position: 'absolute',
zIndex: 999,
},
});
}
Animated.timing(this.opacity, {
toValue: opacity,
duration: 200,
easing: Easing.inOut(Easing.ease),
}).start(() => {
if (opacity < 0.5) {
this.overlay.setNativeProps({
style: {
display: 'none',
position: 'relative',
zIndex: -1,
},
});
}
});
}
moveLeft(offset) {
const newOffset = this.menuPositionMultiplier() * offset;
Animated.timing(this.state.left, {
toValue: newOffset,
duration: 200,
easing: Easing.elastic(0.5),
}).start();
this.prevLeft = newOffset;
}
menuPositionMultiplier() {
return this.props.menuPosition === 'right' ? -1 : 1;
}
handlePanResponderMove(e, gestureState) {
if (this.state.left.__getValue() * this.menuPositionMultiplier() >= 0) {
let newLeft = this.prevLeft + gestureState.dx;
if (newLeft >= this.props.openMenuOffset || newLeft < 0) return;
if (
!this.props.bounceBackOnOverdraw &&
Math.abs(newLeft) > this.state.openMenuOffset
) {
newLeft = this.menuPositionMultiplier() * this.state.openMenuOffset;
}
this.state.left.setValue(newLeft);
this.props.onMove(newLeft);
let o = newLeft / this.props.openMenuOffset;
this.opacity.setValue(o * 0.5);
this.overlay.setNativeProps({
style: {
display: 'flex',
position: 'absolute',
zIndex: 999,
},
});
}
}
handlePanResponderEnd(e, gestureState) {
const offsetLeft =
this.menuPositionMultiplier() *
(this.state.left.__getValue() + gestureState.dx);
this.openMenu(shouldOpenMenu(offsetLeft));
}
handleMoveShouldSetPanResponder(e, gestureState) {
if (this.gesturesAreEnabled()) {
const x = Math.round(Math.abs(gestureState.dx));
const y = Math.round(Math.abs(gestureState.dy));
const touchMoved = x > this.props.toleranceX && y < this.props.toleranceY;
if (this.isOpen) {
return touchMoved;
}
const withinEdgeHitWidth =
this.props.menuPosition === 'right'
? gestureState.moveX > deviceScreen.width - this.props.edgeHitWidth
: gestureState.moveX < this.props.edgeHitWidth;
const swipingToOpen = this.menuPositionMultiplier() * gestureState.dx > 0;
return withinEdgeHitWidth && touchMoved && swipingToOpen;
}
return false;
}
openMenu(isOpen) {
const {hiddenMenuOffset, openMenuOffset} = this.state;
this.changeOpacity(isOpen ? 0.5 : 0);
this.moveLeft(isOpen ? openMenuOffset : hiddenMenuOffset);
this.isOpen = isOpen;
this.forceUpdate();
this.props.onChange(isOpen);
}
setGestureEnabled(enabled) {
this.isGestureEnabled = enabled;
}
gesturesAreEnabled() {
return this.isGestureEnabled;
}
UNSAFE_componentWillMount() {
this.responder = PanResponder.create({
onStartShouldSetResponderCapture: this.onStartShouldSetResponderCapture,
onMoveShouldSetPanResponder: this.onMoveShouldSetPanResponder,
onPanResponderMove: this.onPanResponderMove,
onPanResponderRelease: this.onPanResponderRelease,
onPanResponderTerminate: this.onPanResponderTerminate,
});
}
componentDidMount() {
//eSubscribeEvent(eSendSideMenuOverlayRef, this._getOverlayViewRef);
}
componentWillUnmount() {
//eUnSubscribeEvent(eSendSideMenuOverlayRef, this._getOverlayViewRef);
}
_getOverlayViewRef = (data) => {
//this.overlayViewRef = data.ref;
};
render() {
const boundryStyle =
this.props.menuPosition === 'right'
? {left: this.state.width - this.state.openMenuOffset}
: {right: this.state.width - this.state.openMenuOffset};
const menu = (
<View style={[styles.menu, boundryStyle]}>
{this.props.isFullscreen ? null : this.props.menu}
</View>
);
return (
<View style={styles.container} onLayout={this.onLayoutChange}>
{menu}
{this.getContentView()}
</View>
);
}
}
SideMenu.propTypes = {
edgeHitWidth: PropTypes.number,
toleranceX: PropTypes.number,
toleranceY: PropTypes.number,
menuPosition: PropTypes.oneOf(['left', 'right']),
onChange: PropTypes.func,
onMove: PropTypes.func,
children: PropTypes.node,
menu: PropTypes.node,
openMenuOffset: PropTypes.number,
hiddenMenuOffset: PropTypes.number,
animationStyle: PropTypes.func,
disableGestures: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
animationFunction: PropTypes.func,
onStartShouldSetResponderCapture: PropTypes.func,
isOpen: PropTypes.bool,
bounceBackOnOverdraw: PropTypes.bool,
autoClosing: PropTypes.bool,
};
SideMenu.defaultProps = {
toleranceY: 10,
toleranceX: 10,
edgeHitWidth: 60,
children: null,
menu: null,
openMenuOffset: deviceScreen.width * (2 / 3),
disableGestures: false,
menuPosition: 'left',
hiddenMenuOffset: 0,
onMove: () => {},
onStartShouldSetResponderCapture: () => true,
onChange: () => {},
onSliding: () => {},
animationStyle: (value) => ({
transform: [
{
translateX: value,
},
],
}),
animationFunction: (prop, value) =>
Animated.timing(prop, {
toValue: value,
duration: 300,
easing: Easing.elastic(0.5),
}),
isOpen: false,
bounceBackOnOverdraw: true,
autoClosing: true,
};

View File

@@ -0,0 +1,34 @@
// @flow
import {StyleSheet} from 'react-native';
const absoluteStretch = {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
};
export default StyleSheet.create({
container: {
...absoluteStretch,
justifyContent: 'center',
},
menu: {
...absoluteStretch,
},
frontView: {
flex: 1,
position: 'absolute',
left: 0,
top: 0,
backgroundColor: 'transparent',
overflow: 'hidden',
},
overlay: {
...absoluteStretch,
backgroundColor: 'black',
opacity: 0,
},
});

View File

@@ -1,15 +1,12 @@
import {createDrawerNavigator} from '@react-navigation/drawer';
import {NavigationContainer} from '@react-navigation/native';
import * as React from 'react';
import {Menu} from '../components/Menu';
import {DDS} from '../services/DeviceDetection';
import {eSubscribeEvent, eUnSubscribeEvent} from '../services/EventManager';
import {eCloseSideMenu, eOpenSideMenu} from '../utils/Events';
import {NavigationContainer} from '@react-navigation/native';
import {sideMenuRef} from '../utils/Refs';
import {Dimensions} from 'react-native';
import {NavigatorStack} from './NavigatorStack';
import {Menu} from '../components/Menu';
import NavigationService from '../services/Navigation';
import {createDrawerNavigator} from '@react-navigation/drawer';
import {DDS} from '../services/DeviceDetection';
import { dWidth } from '../utils';
const Drawer = createDrawerNavigator();
@@ -38,16 +35,17 @@ export const NavigationStack = ({component = NavigatorStack}) => {
<Drawer.Navigator
screenOptions={{
swipeEnabled: locked ? false : true,
}}
drawerStyle={{
width:
DDS.isLargeTablet()
? DDS.width * 0.15
: DDS.isSmallTab
? "30%"
: "65%",
width: DDS.isLargeTablet()
? DDS.width * 0.15
: DDS.isSmallTab
? '30%'
: '65%',
borderRightWidth: 0,
}}
edgeWidth={200}
drawerType={DDS.isTab || DDS.isSmallTab ? 'permanent' : 'slide'}
drawerContent={DrawerComponent}
@@ -58,10 +56,6 @@ export const NavigationStack = ({component = NavigatorStack}) => {
);
};
const DrawerComponent = () => {
return (
<Menu />
);
return <Menu />;
};

View File

@@ -1,17 +1,16 @@
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import * as React from 'react';
import {Animated} from 'react-native';
import { Animated } from 'react-native';
import Container from '../components/Container';
import {useTracked} from '../provider';
import {DDS} from '../services/DeviceDetection';
import {rootNavigatorRef} from '../utils/Refs';
import { useTracked } from '../provider';
import { rootNavigatorRef } from '../utils/Refs';
import Favorites from '../views/Favorites';
import Folders from '../views/Folders';
import Home from '../views/Home';
import Notebook from '../views/Notebook';
import Notes from '../views/Notes';
import {Search} from '../views/Search';
import { Search } from '../views/Search';
import Settings from '../views/Settings';
import Tags from '../views/Tags';
import Trash from '../views/Trash';
@@ -66,7 +65,6 @@ export const NavigatorStack = React.memo(
() => {
const [state] = useTracked();
const {settings} = state;
return (
<Container root={true}>

View File

@@ -29,5 +29,6 @@ export const Actions = {
HEADER_TEXT_STATE:'headerTextState',
MESSAGE_BOARD_STATE:'messageBoardState',
LOADING:"loading",
FULLSCREEN:"fullscreen"
FULLSCREEN:"fullscreen",
DEVICE_MODE:"deviceMode"
};

View File

@@ -33,6 +33,7 @@ export const defaultState = {
privacyScreen: false,
},
currentScreen: 'home',
deviceMode:null,
colors: {
night: false,
bg: 'white',

View File

@@ -248,6 +248,12 @@ export const reducer = (state, action) => {
fullscreen: action.state,
};
}
case Actions.DEVICE_MODE: {
return {
...state,
deviceMode: action.state,
};
}
default:
throw new Error('unknown action type');
}