Files
worker/lib/shared/widgets/vietnamese_phone_field.dart
Phuoc Nguyen b5f90c364d update icon
2025-11-14 18:02:37 +07:00

273 lines
7.7 KiB
Dart

/// Vietnamese Phone Number Input Field
///
/// Specialized input field for Vietnamese phone numbers with
/// auto-formatting and validation.
library;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/utils/formatters.dart';
import 'package:worker/core/utils/validators.dart';
/// Phone number input field with Vietnamese formatting
class VietnamesePhoneField extends StatefulWidget {
final TextEditingController? controller;
final String? labelText;
final String? hintText;
final String? initialValue;
final ValueChanged<String>? onChanged;
final ValueChanged<String>? onSubmitted;
final FormFieldValidator<String>? validator;
final bool enabled;
final bool autoFocus;
final TextInputAction? textInputAction;
final FocusNode? focusNode;
final bool required;
final Widget? prefixIcon;
final Widget? suffixIcon;
const VietnamesePhoneField({
super.key,
this.controller,
this.labelText,
this.hintText,
this.initialValue,
this.onChanged,
this.onSubmitted,
this.validator,
this.enabled = true,
this.autoFocus = false,
this.textInputAction,
this.focusNode,
this.required = true,
this.prefixIcon,
this.suffixIcon,
});
@override
State<VietnamesePhoneField> createState() => _VietnamesePhoneFieldState();
}
class _VietnamesePhoneFieldState extends State<VietnamesePhoneField> {
late TextEditingController _controller;
bool _isControllerInternal = false;
@override
void initState() {
super.initState();
if (widget.controller == null) {
_controller = TextEditingController(text: widget.initialValue);
_isControllerInternal = true;
} else {
_controller = widget.controller!;
}
}
@override
void dispose() {
if (_isControllerInternal) {
_controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: _controller,
focusNode: widget.focusNode,
enabled: widget.enabled,
autofocus: widget.autoFocus,
keyboardType: TextInputType.phone,
textInputAction: widget.textInputAction ?? TextInputAction.next,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(11),
_PhoneNumberFormatter(),
],
decoration: InputDecoration(
labelText: widget.labelText ?? 'Số điện thoại',
hintText: widget.hintText ?? '0xxx xxx xxx',
prefixIcon: widget.prefixIcon ?? const FaIcon(FontAwesomeIcons.phone, size: 20),
suffixIcon: widget.suffixIcon,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
),
contentPadding: InputFieldSpecs.contentPadding,
),
validator:
widget.validator ??
(widget.required ? Validators.phone : Validators.phoneOptional),
onChanged: widget.onChanged,
onFieldSubmitted: widget.onSubmitted,
);
}
}
/// Phone number text input formatter
class _PhoneNumberFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final text = newValue.text;
if (text.isEmpty) {
return newValue;
}
// Format as: 0xxx xxx xxx
String formatted = text;
if (text.length > 4 && text.length <= 7) {
formatted = '${text.substring(0, 4)} ${text.substring(4)}';
} else if (text.length > 7) {
formatted =
'${text.substring(0, 4)} ${text.substring(4, 7)} ${text.substring(7)}';
}
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}
/// Read-only phone display field
class PhoneDisplayField extends StatelessWidget {
final String phoneNumber;
final String? labelText;
final Widget? prefixIcon;
final VoidCallback? onTap;
const PhoneDisplayField({
super.key,
required this.phoneNumber,
this.labelText,
this.prefixIcon,
this.onTap,
});
@override
Widget build(BuildContext context) {
return TextFormField(
initialValue: PhoneFormatter.format(phoneNumber),
readOnly: true,
enabled: onTap != null,
onTap: onTap,
decoration: InputDecoration(
labelText: labelText ?? 'Số điện thoại',
prefixIcon: prefixIcon ?? const FaIcon(FontAwesomeIcons.phone, size: 20),
suffixIcon: onTap != null ? const FaIcon(FontAwesomeIcons.penToSquare, size: 18) : null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
),
contentPadding: InputFieldSpecs.contentPadding,
),
);
}
}
/// Phone field with country code selector
class InternationalPhoneField extends StatefulWidget {
final TextEditingController? controller;
final String? labelText;
final String? hintText;
final ValueChanged<String>? onChanged;
final FormFieldValidator<String>? validator;
final bool enabled;
final String defaultCountryCode;
const InternationalPhoneField({
super.key,
this.controller,
this.labelText,
this.hintText,
this.onChanged,
this.validator,
this.enabled = true,
this.defaultCountryCode = '+84',
});
@override
State<InternationalPhoneField> createState() =>
_InternationalPhoneFieldState();
}
class _InternationalPhoneFieldState extends State<InternationalPhoneField> {
late TextEditingController _controller;
late String _selectedCountryCode;
bool _isControllerInternal = false;
@override
void initState() {
super.initState();
_selectedCountryCode = widget.defaultCountryCode;
if (widget.controller == null) {
_controller = TextEditingController();
_isControllerInternal = true;
} else {
_controller = widget.controller!;
}
}
@override
void dispose() {
if (_isControllerInternal) {
_controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: _controller,
enabled: widget.enabled,
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.next,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],
decoration: InputDecoration(
labelText: widget.labelText ?? 'Số điện thoại',
hintText: widget.hintText ?? 'xxx xxx xxx',
prefixIcon: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: DropdownButton<String>(
value: _selectedCountryCode,
underline: const SizedBox(),
items: const [
DropdownMenuItem(value: '+84', child: Text('+84')),
DropdownMenuItem(value: '+1', child: Text('+1')),
DropdownMenuItem(value: '+86', child: Text('+86')),
],
onChanged: widget.enabled
? (value) {
if (value != null) {
setState(() {
_selectedCountryCode = value;
});
widget.onChanged?.call('$value${_controller.text}');
}
}
: null,
),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
),
contentPadding: InputFieldSpecs.contentPadding,
),
validator: widget.validator,
onChanged: (value) {
widget.onChanged?.call('$_selectedCountryCode$value');
},
);
}
}