UNPKG

@monchilin/react-native-dropdown

Version:
632 lines (563 loc) 17.3 kB
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Animated, Dimensions, Easing, FlatList, StyleSheet, Text, TouchableHighlight, TouchableNativeFeedback, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native'; import Modal from './Modal'; const truth = () => true; const isNumber = o => typeof o === 'number'; function id(v) { return v; } const TOUCHABLE_ELEMENTS = ['TouchableHighlight', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback']; // TODO react native for web not support useWindowDimensions() const { width: windowWidth, height: windowHeight } = Dimensions.get('window'); const useEffectWithSkipFirst = (callback, deps) => { const isFirstRun = useRef(true); useEffect(() => { if (isFirstRun.current) { isFirstRun.current = false; return; } return callback(); // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); }; const useAnimation = ({ visible, transitionHide, transitionShow, meta }) => { const [state, setState] = useState(false); const anim = useRef(new Animated.Value(90)).current; const [style, setStyle] = useState({}); useEffectWithSkipFirst(() => { if (visible) { const transitionShowConfig = transitions[transitionShow]; const interpolate = anim.interpolate(transitionShowConfig.interpolate(meta)); anim.setValue(transitionShowConfig.initialValue); setState(true); switch (transitionShow) { case 'flipUp': setStyle({ transform: [{ rotateX: interpolate }] }); break; case 'scaleIn': setStyle({ transform: [{ scaleX: interpolate, scaleY: 1 }] }); break; case 'fadeIn': setStyle({ opacity: interpolate }); break; case 'slideUp': setStyle({ height: interpolate }); break; default: setStyle({}); } // @ts-ignore Animated[transitionShowConfig.animationType](anim, transitionShowConfig.config).start(); } else { const transitionHideConfig = transitions[transitionHide]; const interpolate = anim.interpolate(transitionHideConfig.interpolate(meta)); anim.setValue(transitionHideConfig.initialValue); switch (transitionHide) { case 'flipDown': setStyle({ transform: [{ rotateX: interpolate }] }); break; case 'scaleOut': setStyle({ transform: [{ scaleX: interpolate, scaleY: 1 }] }); break; case 'fadeOut': setStyle({ opacity: interpolate }); break; case 'slideDown': setStyle({ height: interpolate }); break; default: setStyle({}); } // @ts-ignore Animated[transitionHideConfig.animationType](anim, transitionHideConfig.config).start(() => { setState(false); }); } }, [visible]); return { visible: state, style }; }; const transitions = { flipUp: { config: { toValue: 0, friction: 10, tension: 20, useNativeDriver: true }, interpolate: () => ({ inputRange: [0, 180], outputRange: ['0deg', '180deg'] }), initialValue: 90, animationType: 'spring' }, flipDown: { config: { toValue: 90, friction: 10, tension: 40, useNativeDriver: true }, interpolate: () => ({ inputRange: [0, 90], outputRange: ['0deg', '90deg'] }), initialValue: 0, animationType: 'spring' }, scaleIn: { config: { toValue: 1, friction: 10, tension: 10, useNativeDriver: true }, interpolate: () => ({ inputRange: [0, 1], outputRange: [0, 1] }), initialValue: 0, animationType: 'spring' }, scaleOut: { config: { toValue: 0, friction: 10, tension: 10, useNativeDriver: true }, interpolate: () => ({ inputRange: [0, 1], outputRange: [0, 1] }), initialValue: 1, animationType: 'spring' }, fadeIn: { config: { toValue: 1, duration: 500, useNativeDriver: true }, interpolate: () => ({ inputRange: [0, 1], outputRange: [0, 1] }), initialValue: 0, animationType: 'timing' }, fadeOut: { config: { toValue: 0, duration: 500, useNativeDriver: true }, interpolate: () => ({ inputRange: [0, 1], outputRange: [0, 1] }), initialValue: 1, animationType: 'timing' }, slideUp: { config: { toValue: 100, useNativeDriver: false }, interpolate: meta => ({ inputRange: [0, 100], delay: 800, easing: Easing.in, outputRange: [0, meta.dropdownHeight] }), initialValue: 0, animationType: 'timing' }, slideDown: { config: { toValue: 0, useNativeDriver: false }, interpolate: meta => ({ inputRange: [0, 100], delay: 800, easing: Easing.sin, outputRange: [0, meta.dropdownHeight] }), initialValue: 100, animationType: 'timing' } }; const usePosition = ({ heightSourceStyle, widthSourceStyle }) => { const height = useMemo(() => { const style = StyleSheet.flatten(heightSourceStyle.find(item => StyleSheet.flatten(item).height)); const _height = (style === null || style === void 0 ? void 0 : style.height) ? style.height.toString() : '-1'; return Number.parseFloat(_height); }, [heightSourceStyle]); const width = useMemo(() => { const style = StyleSheet.flatten(widthSourceStyle.find(item => StyleSheet.flatten(item).width)); const _width = (style === null || style === void 0 ? void 0 : style.width) ? style.width.toString() : '-1'; return Number.parseFloat(_width); }, [widthSourceStyle]); return { height, width }; }; const SplitLine = () => { return /*#__PURE__*/React.createElement(View, { style: styles.splitLint }); }; function Component({ defaultIndex, defaultLabel = 'Please select', index, onSelect = truth, dataSource, disabled = false, loading = false, animated = true, transitionShow = 'flipUp', transitionHide = 'flipDown', scrollEnabled = true, keyExtractor = (_, itemIndex) => itemIndex.toString(), adjustFrame = id, renderItem, renderSeparator = SplitLine, showSeparator = true, renderLabel, onDropdownWillShow = truth, onDropdownWillHide = truth, rootContainerStyle = {}, rootContainerProps = {}, labelContainerStyle = {}, labelContainerDisabledStyle = {}, labelContainerProps = {}, labelStyle = {}, labelDisabledStyle = {}, labelProps = {}, modalProps = {}, dropdownStyle = {}, dropdownProps = {}, itemTouchableProps = {}, itemLabelStyle = {}, itemLabelProps = {}, itemLabelHighlightStyle = {}, children }, ref) { const _button = useRef(null); const _buttonFrame = useRef({ x: 0, y: 0, w: 0, h: 0 }); const [dropdownVisible, setDropdownVisible] = useState(false); const [selectedIndex, setSelectedIndex] = useState(() => { return isNumber(index) ? index : isNumber(defaultIndex) ? defaultIndex : -1; }); const dropDownSize = usePosition({ heightSourceStyle: [rootContainerStyle, dropdownStyle, styles.dropdown], widthSourceStyle: [rootContainerStyle, dropdownStyle, rootContainerStyle] }); const { style: dropdownAnimatedStyle, visible: _dropdownVisibleForAnimation } = useAnimation({ visible: dropdownVisible, transitionHide, transitionShow, meta: { dropdownHeight: dropDownSize.height, dropdownWidth: dropDownSize.width } }); const labelText = useMemo(() => { return renderLabel ? renderLabel(dataSource[selectedIndex], selectedIndex) : dataSource[selectedIndex] ? dataSource[selectedIndex] : defaultLabel; }, [dataSource, defaultLabel, renderLabel, selectedIndex]); useEffectWithSkipFirst(() => { if (isNumber(index)) { setSelectedIndex(index); } else { setSelectedIndex(-1); } }, [index]); const hide = useCallback(() => { setDropdownVisible(false); }, []); const show = useCallback(() => { setDropdownVisible(true); }, []); const select = useCallback(newIndex => { setSelectedIndex(newIndex); onSelect(newIndex); }, [onSelect]); useImperativeHandle(ref, () => ({ select, hide, show })); const onLayout = () => { var _button$current; if (!((_button$current = _button.current) === null || _button$current === void 0 ? void 0 : _button$current.measure)) { return; } _button.current.measure((_fx, _fy, width, height, px, py) => { _buttonFrame.current = { x: px, y: py, w: width, h: height }; }); }; const _onRequestClose = () => { if (onDropdownWillHide(dataSource[selectedIndex], selectedIndex) !== false) { hide(); } }; const _onModalPress = () => { if (onDropdownWillHide(dataSource[selectedIndex], selectedIndex) !== false) { hide(); } }; const _onLabelPress = () => { if (onDropdownWillShow(dataSource[selectedIndex], selectedIndex) !== false) { show(); } }; const _onPressModalItem = (newIndex, item) => { // 除非是 false 否则更新 index if (onSelect(newIndex, item) !== false) { setSelectedIndex(newIndex); } if (onDropdownWillHide(dataSource[newIndex], newIndex) !== false) { setDropdownVisible(false); } }; const getDropdownHorizontalBorderWidthFromStyle = useCallback(() => { var _style$borderWidth, _style$borderLeftWidt, _style$borderRightWid; const style = StyleSheet.flatten([dropdownStyle, rootContainerStyle, styles.dropdown]); const borderWidth = (_style$borderWidth = style.borderWidth) !== null && _style$borderWidth !== void 0 ? _style$borderWidth : 0; const borderLeftWidth = (_style$borderLeftWidt = style.borderLeftWidth) !== null && _style$borderLeftWidt !== void 0 ? _style$borderLeftWidt : 0; const borderRightWidth = (_style$borderRightWid = style.borderRightWidth) !== null && _style$borderRightWid !== void 0 ? _style$borderRightWid : 0; if (!borderLeftWidth && !borderRightWidth) { return borderWidth * 2; } return borderLeftWidth + borderRightWidth; }, [rootContainerStyle, dropdownStyle]); const _calcPosition = () => { // 首先根据 style 的对象获取 dropdown 容器的高度 const dropdownHeight = dropDownSize.height; // x: 按钮的 x 点(相对于屏幕左上角) // y: 按钮的 y 点(相对于屏幕顶点) // w: 按钮的 width // h: 按钮的 height const { x, y, w, h } = _buttonFrame.current; // 距离底部的空间 const buttonSpace = windowHeight - y - h; // 距离右边的空间 const rightSpace = windowWidth - x; // 如果距离底部的空间大于等于 dropdown 的高度 或者 底部空间 const showInBottom = buttonSpace >= dropdownHeight || buttonSpace >= y; const showInLeft = rightSpace >= x; const positionStyle = { height: dropdownHeight, top: showInBottom ? y + h : Math.max(0, y - dropdownHeight) }; if (showInLeft) { positionStyle.left = x; } else { const dropdownWidth = dropDownSize.width; if (dropdownWidth !== -1) { positionStyle.width = dropdownWidth; } positionStyle.right = rightSpace - w; } return adjustFrame(positionStyle); }; const _renderLoading = () => { return /*#__PURE__*/React.createElement(ActivityIndicator, { size: "small" }); }; const _renderLabel = () => { return /*#__PURE__*/React.createElement(TouchableOpacity, _extends({ ref: _button, disabled: disabled, onPress: _onLabelPress, style: [labelContainerStyle, disabled && styles.labelContainerDisabled, disabled && labelContainerDisabledStyle] }, labelContainerProps), children !== null && children !== void 0 ? children : /*#__PURE__*/React.createElement(Text, _extends({ style: [styles.label, labelStyle, disabled && styles.labelDisabled, disabled && labelDisabledStyle], numberOfLines: 1 }, labelProps), labelText)); }; const _renderItem = ({ index: newIndex, item }) => { const highlighted = newIndex === selectedIndex; const row = renderItem ? renderItem(item, newIndex, highlighted) : /*#__PURE__*/React.createElement(Text, _extends({ style: [styles.dropdownTextStyle, itemLabelStyle, highlighted && styles.highlightedRowText, highlighted && itemLabelHighlightStyle] }, itemLabelProps), item); const preservedProps = { onPress: () => _onPressModalItem(newIndex, item), ...itemTouchableProps }; if (TOUCHABLE_ELEMENTS.find(name => name === row.type.displayName)) { const props = { ...row.props, onPress: preservedProps.onPress }; const { children: realChildren } = row.props; switch (row.type.displayName) { case 'TouchableHighlight': { return /*#__PURE__*/React.createElement(TouchableHighlight, props, realChildren); } case 'TouchableOpacity': { return /*#__PURE__*/React.createElement(TouchableOpacity, props, realChildren); } case 'TouchableWithoutFeedback': { return /*#__PURE__*/React.createElement(TouchableWithoutFeedback, props, realChildren); } // TODO react native web not support TouchableNativeFeedback case 'TouchableNativeFeedback': { console.warn('react native web not support TouchableNativeFeedback'); return /*#__PURE__*/React.createElement(TouchableNativeFeedback, props, row); } default: break; } } return /*#__PURE__*/React.createElement(TouchableHighlight, preservedProps, row); }; const _renderDropdown = () => { const dropdownWidth = dropDownSize.width !== -1 ? dropDownSize.width - getDropdownHorizontalBorderWidthFromStyle() : undefined; return /*#__PURE__*/React.createElement(FlatList, _extends({ scrollEnabled: scrollEnabled, style: [dropdownStyle, { width: dropdownWidth, height: dropDownSize.height }], data: dataSource, renderItem: _renderItem, keyExtractor: keyExtractor, ItemSeparatorComponent: showSeparator ? renderSeparator : null, automaticallyAdjustContentInsets: false, showsVerticalScrollIndicator: false }, dropdownProps)); }; const _renderModel = () => { const frameStyle = _calcPosition(); return /*#__PURE__*/React.createElement(Modal, _extends({ animated: animated, animationType: 'none', visible: _dropdownVisibleForAnimation, transparent: true, onRequestClose: _onRequestClose, supportedOrientations: ['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right'] }, modalProps), /*#__PURE__*/React.createElement(TouchableWithoutFeedback, { disabled: !dropdownVisible, onPress: _onModalPress }, /*#__PURE__*/React.createElement(View, { style: styles.modal }, /*#__PURE__*/React.createElement(Animated.View, { style: [styles.dropdown, frameStyle, animated && dropdownAnimatedStyle] }, loading ? _renderLoading() : _renderDropdown())))); }; return /*#__PURE__*/React.createElement(View, _extends({ onLayout: onLayout, style: rootContainerStyle }, rootContainerProps), _renderLabel(), _renderModel()); } export default /*#__PURE__*/React.forwardRef(Component); const styles = StyleSheet.create({ label: { fontSize: 12 }, modal: { flexGrow: 1 }, dropdown: { height: (33 + StyleSheet.hairlineWidth) * 4, position: 'absolute', borderWidth: StyleSheet.hairlineWidth, borderColor: 'lightgray', borderRadius: 2, backgroundColor: '#ffffff', justifyContent: 'center' }, loading: { alignSelf: 'center' }, dropdownTextStyle: { paddingHorizontal: 6, paddingVertical: 10, fontSize: 11, color: 'rgba(0,0,0,.65)', backgroundColor: 'white', textAlignVertical: 'center' }, highlightedRowText: { color: 'black' }, highlightedRow: { backgroundColor: '#f5f5f5' }, labelDisabled: { color: 'rgba(0,0,0,.25)' }, labelContainerDisabled: { backgroundColor: '#fff' }, splitLint: { backgroundColor: 'rgb(217, 217, 217)', height: 1, width: '100%' } }); //# sourceMappingURL=Dropdown.js.map