Commit 3c5a84e8 by tungnq

TODO: Dựng khung cơ của chip input

parent a5e8f1e0
Subproject commit c95f83687e728b84c9eb4df75e9a89ad0afb1b1a
...@@ -18,6 +18,7 @@ const colors = { ...@@ -18,6 +18,7 @@ const colors = {
blue1: 'rgba(47, 107, 255, 0.1)', blue1: 'rgba(47, 107, 255, 0.1)',
blue2: 'rgba(158, 187, 255, 1)', blue2: 'rgba(158, 187, 255, 1)',
blue3:'rgba(191, 210, 255, 1)', blue3:'rgba(191, 210, 255, 1)',
blue4:'#dee8ff',
blueTextChip: 'rgba(47, 107, 255, 1)', blueTextChip: 'rgba(47, 107, 255, 1)',
orange: 'rgba(243, 154, 43, 1)', orange: 'rgba(243, 154, 43, 1)',
......
// FEATURE: Component EmailChipInput để nhập và hiển thị danh sách email dạng chip
import React, {useRef, useState} from 'react';
import {View, TextInput, TouchableOpacity, Image} from 'react-native';
import Chip from './chip';
import {BACKSPACE, DELIMITERS} from '../../config/constants';
import {isValidEmail} from '../../config/Functions';
import styles from './styles';
import {Text} from 'react-native';
import R from '../../assets/R';
/**
* FUNCTIONALITY: Component cho phép nhập email và hiển thị dạng chip
@param {Object} props
- Props của component
@param {string[]} props.entries
- Danh sách email ban đầu
@param {Function} props.onSubmit
- Callback khi submit danh sách email
@param {string[]} props.delimiters
- Các ký tự phân tách để tự động thêm email
@param {Object} props.containerStyle
- Style cho container chính
@param {Object} props.chipContainerStyle
- Style cho từng chip
@param {Object} props.chipTextStyle
- Style cho text trong chip
@param {Object} props.inputContainerStyle
- Style cho container input
@param {Object} props.inputStyle
- Style cho TextInput
@param {React.Element} props.chipImage
- Icon hiển thị trong chip (thường là nút X)
@param {string} props.keyboardAppearance
- Appearance của keyboard
@param {string} props.clearButtonMode
- Mode của clear button
@param {string} props.placeholder
- Placeholder text cho input
@param {string} props.placeholderTextColor
- Màu placeholder
@param {string} props.autoCapitalize
- Auto capitalize setting
@param {boolean} props.autoCorrect
- Bật/tắt auto correct
@param {boolean} props.autoFocus
- Auto focus khi mount
@param {boolean} props.blurOnSubmit
- Blur input khi submit
@param {string} props.keyboardType
- Loại keyboard
@param {Object} props.TextInputProps
- Props bổ sung cho TextInput
@param {string} props.widthContainerInput
- Chiều rộng của container input
*/
export const EmailChipInput = ({
title,
entries,
onSubmit,
chipImage,
autoCorrect,
TextInputProps,
containerStyle,
chipContainerStyle,
inputContainerStyle,
inputStyle,
placeholderTextColor,
chipTextStyle,
delimiters = DELIMITERS,
keyboardAppearance = 'default',
clearButtonMode = 'while-editing',
placeholder,
autoCapitalize = 'none',
autoFocus = false,
blurOnSubmit = true,
keyboardType = 'email-address',
widthContainerInput= '100%',
}) => {
// STATE: Refs và state management
const ref = useRef(null);
const [emails, setEmails] = useState(entries);
const [value, setValue] = useState('');
const [isExpanded, setIsExpanded] = useState(false); // Quản lý chế độ hiển thị chip
// FUNCTIONALITY: Xử lý nhấn vào chip để edit
const handleOnPressChip = index => {
// NOTE: Đưa email từ chip về input để chỉnh sửa
setValue(emails[index]);
setEmails(emails.filter((_, i) => i !== index));
// UI/UX: Focus vào input để tiếp tục chỉnh sửa
return ref.current?.focus();
};
// PERFORMANCE: Tối ưu việc lấy entry cuối cùng
const lastEntry = emails[emails.length - 1];
// FUNCTIONALITY: Xử lý thay đổi text trong input
const onTextChange = inputValue => {
// BUG: Tránh duplicate khi edit email cuối cùng
if (inputValue === lastEntry) {
return setValue('');
}
// FEATURE: Tự động thêm email khi gặp delimiter
if (
isValidEmail(inputValue) &&
delimiters.some(delimiter => inputValue.endsWith(delimiter))
) {
setEmails([
...emails,
inputValue.substring(0, inputValue.length - 1), // Loại bỏ delimiter
]);
setValue('');
return;
}
setValue(inputValue);
};
// FUNCTIONALITY: Xử lý phím backspace để xóa chip
const onKeyPress = ({nativeEvent: {key}}) => {
// FEATURE: Backspace khi input trống sẽ edit chip cuối cùng
if (!value && key === BACKSPACE && lastEntry) {
setValue(lastEntry);
setEmails(emails.filter(email => email !== lastEntry));
}
};
// FUNCTIONALITY: Xử lý blur/submit để finalize email
const onBlur = () => {
// VALIDATION: Nếu email không hợp lệ hoặc rỗng, chỉ submit danh sách hiện tại
if (!isValidEmail(value)) {
return onSubmit(emails);
}
// FEATURE: Thêm email hiện tại vào danh sách và submit
const newEntries = [...emails, value];
setEmails(newEntries);
setValue('');
onSubmit(newEntries);
};
// FUNCTIONALITY: Toggle chế độ hiển thị chip
const toggleExpanded = () => {
setIsExpanded(!isExpanded);
};
// FUNCTIONALITY: Render chip theo chế độ hiển thị
const renderChips = () => {
if (emails.length === 0) return null;
// Nếu chỉ có 1 chip hoặc đang ở chế độ mở rộng, hiển thị tất cả
if (emails.length === 1 || isExpanded) {
return emails.map((email, index) => (
<Chip
index={index}
value={email}
onPress={handleOnPressChip}
key={index}
chipContainerStyle={chipContainerStyle}
chipImage={chipImage}
chipTextStyle={chipTextStyle}
marginHorizontal={5}
/>
));
}
// Chế độ thu gọn: chỉ hiển thị chip đầu + text số lượng
return (
<>
<Chip
index={0}
value={emails[0]}
onPress={handleOnPressChip}
key={0}
chipContainerStyle={chipContainerStyle}
chipImage={chipImage}
chipTextStyle={chipTextStyle}
marginHorizontal={5}
/>
<Text style={{
fontSize: 12,
color: R.colors.gray,
fontFamily: R.fonts.fontMedium,
marginLeft: 5,
alignSelf: 'center'
}}>
+{emails.length - 1} more
</Text>
</>
);
};
return (
<View style={containerStyle}>
<View
style={[
{
flexDirection: 'row',
flexWrap: emails.length > 0 ? 'wrap' : 'nowrap',
alignItems: 'center',
}
]}>
{/* FEATURE: Render danh sách email chips */}
<Text
style={{
fontSize: 12,
color: R.colors.black,
fontFamily:R.fonts.fontMedium,
fontWeight:'600',
}}
>{title}</Text>
{renderChips()}
{/* FEATURE: Nút toggle thu gọn/phóng to */}
{emails.length > 1 && (
<View style={{ position: 'absolute', right: 0, top: 5}}>
<TouchableOpacity
onPress={toggleExpanded}
>
<Image
source={isExpanded ? R.images.icDrop : R.images.icDrop}
style={{
width: 30,
height: 20,
tintColor: R.colors.black
}}
/>
</TouchableOpacity>
</View>
)}
{/* FEATURE: Input để nhập email mới */}
<View style={[{width: widthContainerInput}, inputContainerStyle]}>
<TextInput
value={value}
ref={ref}
onChangeText={onTextChange}
onKeyPress={onKeyPress}
onSubmitEditing={onBlur}
onBlur={onBlur}
blurOnSubmit={blurOnSubmit}
keyboardType={keyboardType}
clearButtonMode={clearButtonMode}
autoCorrect={autoCorrect}
autoFocus={autoFocus}
placeholder={placeholder}
autoCapitalize={autoCapitalize}
keyboardAppearance={keyboardAppearance}
placeholderTextColor={placeholderTextColor}
style={[{
fontSize: 12,
color: R.colors.black,
fontFamily:R.fonts.fontMedium,
fontWeight:'600',
borderBottomWidth:1,
borderColor:R.colors.grayBorderInputTextHeader
}, inputStyle]}
{...TextInputProps}
/>
</View>
</View>
</View>
);
};
// PERFORMANCE: Memoize component để tránh re-render
export default React.memo(EmailChipInput);
import React from 'react';
import {TouchableOpacity, View, Text, StyleSheet} from 'react-native';
import R from '../../assets/R';
const Chip = ({
chipContainerStyle,
index,
onPress,
chipImage,
chipTextStyle,
value,
marginHorizontal,
marginVertical = 3
}) => {
const handlePress = () => {
onPress(index);
};
return (
<View
onPress={handlePress}
style={[{
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 15,
marginHorizontal: marginHorizontal,
marginVertical: marginVertical,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}, chipContainerStyle]}>
<Text style={[{
fontSize: 12,
color: R.colors.black,
}, chipTextStyle]}>
{value}
</Text>
{chipImage && (
<View style={{marginLeft:5}}>
{chipImage}
</View>
)}
</View>
);
};
export default React.memo(Chip);
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
flexWrap: 'wrap',
backgroundColor: 'white',
paddingVertical: 10,
},
inputContainer: {},
input: {
fontSize: 16,
paddingHorizontal: 5,
paddingVertical: 10,
minWidth: 150,
},
});
export default styles;
...@@ -234,6 +234,10 @@ export const removeItemFromArr2 = (items, index) => { ...@@ -234,6 +234,10 @@ export const removeItemFromArr2 = (items, index) => {
return fill; return fill;
}; };
export const isValidEmail = (email) =>
email.length > 0 && /(.+)@(.+){2,}\.(.+){2,}/gmi.test(email);
export const removeItemFromArr = (items, index) => { export const removeItemFromArr = (items, index) => {
items.splice(index, 1); items.splice(index, 1);
return items; return items;
......
...@@ -183,3 +183,6 @@ export const DEVICE_EVENT_KEY = { ...@@ -183,3 +183,6 @@ export const DEVICE_EVENT_KEY = {
RELOAD_BALANCE_WALLET: "reloadBalanceWallet", RELOAD_BALANCE_WALLET: "reloadBalanceWallet",
LOGOUT_EVENT: "logoutEvent", LOGOUT_EVENT: "logoutEvent",
}; };
export const BACKSPACE = 'Backspace';
export const DELIMITERS = [',', ';', ' '];
import React from 'react'; import React, { useState } from 'react';
import {Text, View, StyleSheet} from 'react-native'; import {Text, View, StyleSheet} from 'react-native';
import SendEmailView from './view'; import SendEmailView from './view';
const SendEmail = (props) => { const SendEmail = (props) => {
const [chip, setChips] = useState([]);
const handleChange = (newChips) => {
setChips(newChips);
};
const [dataList, setDataList] = useState([
['john@doe.com', 'jane@doe.com', 'tung@doe.com']
]);
return ( return (
<SendEmailView /> <SendEmailView handleChange={handleChange} chip={chip} dataList={dataList}/>
); );
}; };
......
...@@ -8,7 +8,8 @@ const styles = StyleSheet.create({ ...@@ -8,7 +8,8 @@ const styles = StyleSheet.create({
}, },
body:{ body:{
flex:1, flex:1,
backgroundColor:R.colors.white backgroundColor:R.colors.white,
padding:15
}, },
header:{ header:{
flexDirection:'row', flexDirection:'row',
...@@ -47,6 +48,10 @@ const styles = StyleSheet.create({ ...@@ -47,6 +48,10 @@ const styles = StyleSheet.create({
fontFamily:R.fonts.fontMedium, fontFamily:R.fonts.fontMedium,
fontWeight:'600', fontWeight:'600',
color:R.colors.white color:R.colors.white
},
iconClose:{
width:15,
height:15
} }
}) })
......
...@@ -4,9 +4,10 @@ import styles from './style'; ...@@ -4,9 +4,10 @@ import styles from './style';
import R from '../../../assets/R'; import R from '../../../assets/R';
import Button from '../../../components/Button'; import Button from '../../../components/Button';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import EmailChipInput from '../../../components/Chip/EmailChipInput';
const SendEmailView = (props) => { const SendEmailView = (props) => {
const { } = props; const { handleChange, chip, dataList } = props;
const navigation = useNavigation(); const navigation = useNavigation();
...@@ -28,8 +29,21 @@ const renderHeader = () => { ...@@ -28,8 +29,21 @@ const renderHeader = () => {
return ( return (
<View <View
style={styles.container}> style={styles.container}>
<View style={styles.body}>
{renderHeader()} {renderHeader()}
<View style={styles.body}>
<EmailChipInput
title="To"
delimiters={dataList}
entries={chip}
onSubmit={handleChange}
chipContainerStyle={{ backgroundColor: R.colors.blue4}}
chipTextStyle={{ color:R.colors.black }}
chipImage={
<Image source={R.images.icCancel} style={styles.iconClose} tintColor={R.colors.blue}/>
}
/>
</View> </View>
</View> </View>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment