@uiw/react-native
Version:
UIW for React Native
232 lines (231 loc) • 8.56 kB
JavaScript
import React, { useMemo, useState } from 'react';
import { usePropsValue } from '../utils/hooks';
import { getTreeDeep } from '../utils/tree-select';
import { View, TouchableOpacity, ScrollView, Pressable } from 'react-native';
import Icon from '../Icon';
import Ellipsis from '../Ellipsis';
import Modal from '../Modal';
import { useTheme } from '@shopify/restyle';
import { StyleSheet } from 'react-native';
import Text from '../Typography/Text';
export const TreeSelect = (p) => {
const theme = useTheme();
const style = createStyles({
bgColor: theme.colors.mask,
themeColor: theme.colors.primary_background,
themeText: theme.colors.text,
});
const defaultProps = {
options: [],
fieldNames: {},
defaultValue: [],
activeColor: theme.colors.primary_background || '#3578e5',
placeholder: '请选择',
extra: undefined,
showClear: true,
contentStyle: {},
placeholderColor: '',
disabled: false,
height: 300,
modalProps: {},
};
const props = { ...defaultProps, ...p };
const labelName = props.fieldNames.label || 'label';
const valueName = props.fieldNames.value || 'value';
const childrenName = props.fieldNames.children || 'children';
const [visible, setVisible] = useState(false);
const [value, setValue] = usePropsValue({
value: props.value,
defaultValue: props.defaultValue,
});
const [deep, optionsMap, optionsParentMap] = useMemo(() => {
const deep = getTreeDeep(props.options, childrenName);
const optionsMap = new Map();
const optionsParentMap = new Map();
function traverse(current, children) {
children.forEach((item) => {
optionsParentMap.set(item[valueName], current);
optionsMap.set(item[valueName], item);
if (item[childrenName]) {
traverse(item, item[childrenName]);
}
});
}
traverse(undefined, props.options);
return [deep, optionsMap, optionsParentMap];
}, [props.options]);
const initialLabelValues = useMemo(() => {
const labels = [];
props.defaultValue?.forEach((defaultValue) => {
const defaultOption = optionsMap.get(defaultValue);
if (defaultOption) {
labels.push(defaultOption[labelName]);
}
});
return labels;
}, [props.defaultValue, optionsMap, labelName]);
const [labelValues, setLabelValues] = useState(initialLabelValues);
const onItemSelect = (node) => {
// 找到父级节点
const parentNodes = [];
let current = node;
while (current) {
parentNodes.unshift(current);
const next = optionsParentMap.get(current[valueName]);
current = next;
}
const values = parentNodes.map((i) => i[valueName]);
const labels = parentNodes.map((i) => i[labelName]);
setLabelValues(labels);
setValue(values);
props.onChange?.(values, {
options: parentNodes,
});
};
// item样式
const activeStyles = (index, isActive, isLast) => {
let styles;
// 选中第一排
if (isActive && index === 0) {
styles = {
...style.active_first_item,
};
}
// 未选中第一排
if (!isActive && index === 0) {
styles = {
...style.not_active_first_item,
};
}
// 选中后排
if (isActive && index !== 0) {
styles = {
...style.active_nth_item,
borderColor: props.activeColor,
marginRight: isLast ? 10 : 0,
};
}
// 未选中后排
if (!isActive && index !== 0) {
styles = {
...style.not_active_nth_item,
marginRight: isLast ? 10 : 0,
};
}
return styles;
};
const renderItems = (columnOptions = [], index) => {
return columnOptions.map((item) => {
const isActive = item[valueName] === value[index];
const active_font_color = index === 0 ? theme.colors.primary_text : props.activeColor;
// 是否是最后一列
const isLast = deep - 1 === index;
return (<TouchableOpacity activeOpacity={0.9} key={item[valueName]} onPress={() => {
if (!isActive) {
onItemSelect(item);
}
}} style={[style.item, { ...activeStyles(index, isActive, isLast) }]}>
<Text style={isActive ? { color: active_font_color, fontWeight: 'bold' } : { color: theme.colors.text }}>
{item[labelName]}
</Text>
</TouchableOpacity>);
});
};
const renderColumns = () => {
const columns = [];
for (let i = 0; i < deep; i++) {
let width = `${100 / deep}%`;
// 两列的第一列宽度为 33.33,两列的第二列为 66.67%
if (deep === 2 && i === 0) {
width = `33.33%`;
}
if (deep === 2 && i === 1) {
width = `66.67%`;
}
const column = (<ScrollView key={i} style={{ width, flex: 1, backgroundColor: i === 0 ? theme.colors.gray100 : theme.colors.mask, }}>
{renderItems(i === 0 ? props.options : optionsMap.get(value[i - 1])?.[childrenName], i)}
</ScrollView>);
columns.push(column);
}
return columns;
};
return (<React.Fragment>
<Pressable onPress={() => {
if (props.disabled)
return;
setVisible(true);
}}>
<View style={[props.disabled ? style.disabled : style.content, props.contentStyle]}>
<Ellipsis style={[style.contentTitle, { color: props.placeholderColor || theme.colors.text }]} maxLen={30}>
{labelValues.join() || props.placeholder}
</Ellipsis>
{React.isValidElement(props.extra) ? (props.extra) : value && props.showClear ? (<Pressable onPress={() => {
setValue([]);
setLabelValues([]);
props.onChange?.([], {
options: [],
});
}} style={{ paddingRight: 3 }}>
<Icon name="circle-close-o" size={18} color={theme.colors.text}/>
</Pressable>) : (<Icon name="right" size={18} color="#A19EA0"/>)}
</View>
</Pressable>
<Modal visible={visible} onClosed={() => setVisible(false)} {...props.modalProps}>
<View style={{ marginBottom: 10 }}/>
<View style={{ height: props.height, flexDirection: 'row' }}>{renderColumns()}</View>
</Modal>
</React.Fragment>);
function createStyles({ bgColor, themeText }) {
return StyleSheet.create({
item: {
minHeight: 50,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
active_first_item: {
backgroundColor: bgColor,
},
not_active_first_item: {
backgroundColor: bgColor,
},
active_nth_item: {
backgroundColor: bgColor,
borderWidth: 1,
borderRadius: 5,
marginLeft: 10,
marginBottom: 10,
},
not_active_nth_item: {
backgroundColor: bgColor,
borderColor: bgColor,
borderWidth: 1,
borderRadius: 5,
marginLeft: 10,
marginBottom: 10,
},
content: {
flexDirection: 'row',
height: 35,
alignItems: 'center',
justifyContent: 'space-between',
paddingRight: 5,
backgroundColor: bgColor,
paddingHorizontal: 16,
},
disabled: {
flexDirection: 'row',
height: 35,
alignItems: 'center',
justifyContent: 'space-between',
paddingRight: 5,
backgroundColor: bgColor,
paddingHorizontal: 16,
},
contentTitle: {
fontSize: 16,
color: themeText,
},
});
}
};