180 lines
5.1 KiB
TypeScript
180 lines
5.1 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import {
|
|
Dimensions,
|
|
Modal,
|
|
StyleProp,
|
|
StyleSheet,
|
|
Text,
|
|
TextStyle,
|
|
TouchableOpacity,
|
|
TouchableWithoutFeedback,
|
|
View,
|
|
ViewStyle
|
|
} from 'react-native';
|
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
import { runOnJS } from 'react-native-reanimated';
|
|
|
|
interface MenuItem {
|
|
label: string;
|
|
svg?: React.ReactNode;
|
|
onPress: () => void;
|
|
textStyle?: StyleProp<TextStyle>;
|
|
}
|
|
|
|
interface ContextMenuProps {
|
|
children: React.ReactNode;
|
|
items: MenuItem[];
|
|
menuStyle?: StyleProp<ViewStyle>;
|
|
menuItemStyle?: StyleProp<ViewStyle>;
|
|
menuTextStyle?: StyleProp<TextStyle>;
|
|
dividerStyle?: StyleProp<ViewStyle>;
|
|
onOpen?: () => void;
|
|
onClose?: () => void;
|
|
longPressDuration?: number;
|
|
activeOpacity?: number;
|
|
cancel?: boolean;
|
|
}
|
|
|
|
const ContextMenu: React.FC<ContextMenuProps> = ({
|
|
children,
|
|
items,
|
|
menuStyle,
|
|
menuItemStyle,
|
|
menuTextStyle,
|
|
dividerStyle,
|
|
cancel,
|
|
onOpen,
|
|
onClose,
|
|
longPressDuration = 500,
|
|
activeOpacity = 0.8,
|
|
}) => {
|
|
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
|
const [menuVisible, setMenuVisible] = useState(false);
|
|
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
|
const containerRef = useRef<View>(null);
|
|
|
|
const showMenu = (x: number, y: number) => {
|
|
setMenuPosition({ x, y });
|
|
setMenuVisible(true);
|
|
onOpen?.();
|
|
};
|
|
|
|
const hideMenu = () => {
|
|
setMenuVisible(false);
|
|
onClose?.();
|
|
};
|
|
|
|
const handleItemPress = (onPress: () => void) => {
|
|
onPress();
|
|
hideMenu();
|
|
};
|
|
|
|
const gesture = Gesture.LongPress()
|
|
.minDuration(longPressDuration)
|
|
.onStart((e) => {
|
|
const absoluteX = e.absoluteX;
|
|
const absoluteY = e.absoluteY;
|
|
runOnJS(showMenu)(absoluteX, absoluteY);
|
|
});
|
|
|
|
useEffect(() => {
|
|
setMenuVisible(!cancel);
|
|
}, [cancel])
|
|
|
|
return (
|
|
<>
|
|
<View ref={containerRef} collapsable={false} style={{ flex: 1 }}>
|
|
<GestureDetector gesture={gesture}>
|
|
<View style={{ flex: 1 }}>
|
|
{children}
|
|
</View>
|
|
</GestureDetector>
|
|
</View>
|
|
|
|
<Modal
|
|
visible={menuVisible}
|
|
transparent
|
|
animationType="fade"
|
|
onRequestClose={hideMenu}
|
|
>
|
|
<TouchableWithoutFeedback onPress={hideMenu}>
|
|
<View style={styles.modalOverlay} />
|
|
</TouchableWithoutFeedback>
|
|
|
|
<View
|
|
style={[
|
|
styles.menu,
|
|
{
|
|
position: 'absolute',
|
|
top: Math.min(
|
|
menuPosition.y,
|
|
screenHeight - 300
|
|
),
|
|
left: Math.min(
|
|
menuPosition.x > screenWidth / 2 ? menuPosition.x - 150 : menuPosition.x,
|
|
screenWidth - 160
|
|
),
|
|
},
|
|
menuStyle,
|
|
]}
|
|
onStartShouldSetResponder={() => true}
|
|
>
|
|
{items.map((item, index) => (
|
|
<React.Fragment key={item.label}>
|
|
<TouchableOpacity
|
|
style={[styles.menuItem, menuItemStyle]}
|
|
onPress={() => handleItemPress(item.onPress)}
|
|
activeOpacity={activeOpacity}
|
|
>
|
|
{item.svg}
|
|
<Text style={[styles.menuText, menuTextStyle, item.textStyle]}>
|
|
{item.label}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
{index < items.length - 1 && (
|
|
<View style={[styles.divider, dividerStyle]} />
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</View>
|
|
</Modal>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
modalOverlay: {
|
|
...StyleSheet.absoluteFillObject,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
|
},
|
|
menu: {
|
|
backgroundColor: 'white',
|
|
borderRadius: 8,
|
|
minWidth: 100,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 6,
|
|
elevation: 10,
|
|
zIndex: 1000,
|
|
},
|
|
menuItem: {
|
|
paddingVertical: 12,
|
|
paddingHorizontal: 16,
|
|
minWidth: 100,
|
|
flexDirection: 'row',
|
|
gap: 4,
|
|
alignItems: 'center'
|
|
},
|
|
menuText: {
|
|
fontSize: 16,
|
|
color: '#333',
|
|
},
|
|
divider: {
|
|
height: 1,
|
|
backgroundColor: '#f0f0f0',
|
|
marginHorizontal: 8,
|
|
},
|
|
});
|
|
|
|
export default ContextMenu; |