314 lines
8.4 KiB
Dart
314 lines
8.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
|
|
|
class SuperportShadSelect<T> extends StatefulWidget {
|
|
final String label;
|
|
final String? placeholder;
|
|
final List<T> items;
|
|
final T? value;
|
|
final String Function(T) itemLabel;
|
|
final ValueChanged<T?>? onChanged;
|
|
final bool enabled;
|
|
final bool searchable;
|
|
final String? errorText;
|
|
final String? helperText;
|
|
final Widget? prefixIcon;
|
|
final VoidCallback? onClear;
|
|
final Future<List<T>> Function(String)? onSearch;
|
|
final bool required;
|
|
|
|
const SuperportShadSelect({
|
|
super.key,
|
|
required this.label,
|
|
required this.items,
|
|
required this.itemLabel,
|
|
this.placeholder,
|
|
this.value,
|
|
this.onChanged,
|
|
this.enabled = true,
|
|
this.searchable = false,
|
|
this.errorText,
|
|
this.helperText,
|
|
this.prefixIcon,
|
|
this.onClear,
|
|
this.onSearch,
|
|
this.required = false,
|
|
});
|
|
|
|
@override
|
|
State<SuperportShadSelect<T>> createState() => _SuperportShadSelectState<T>();
|
|
}
|
|
|
|
class _SuperportShadSelectState<T> extends State<SuperportShadSelect<T>> {
|
|
final TextEditingController _searchController = TextEditingController();
|
|
List<T> _filteredItems = [];
|
|
bool _isSearching = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_filteredItems = widget.items;
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(SuperportShadSelect<T> oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.items != widget.items) {
|
|
_filteredItems = widget.items;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _handleSearch(String query) async {
|
|
if (query.isEmpty) {
|
|
setState(() {
|
|
_filteredItems = widget.items;
|
|
_isSearching = false;
|
|
});
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isSearching = true;
|
|
});
|
|
|
|
if (widget.onSearch != null) {
|
|
final results = await widget.onSearch!(query);
|
|
setState(() {
|
|
_filteredItems = results;
|
|
_isSearching = false;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
_filteredItems = widget.items.where((item) {
|
|
return widget.itemLabel(item)
|
|
.toLowerCase()
|
|
.contains(query.toLowerCase());
|
|
}).toList();
|
|
_isSearching = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = ShadTheme.of(context);
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
widget.label,
|
|
style: theme.textTheme.small.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
if (widget.required)
|
|
Text(
|
|
' *',
|
|
style: theme.textTheme.small.copyWith(
|
|
color: theme.colorScheme.destructive,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
ShadSelect<T>(
|
|
placeholder: widget.placeholder != null
|
|
? Text(widget.placeholder!)
|
|
: const Text('선택하세요'),
|
|
selectedOptionBuilder: (context, value) {
|
|
if (value == null) return const Text('선택하세요');
|
|
return Text(widget.itemLabel(value));
|
|
},
|
|
enabled: widget.enabled,
|
|
options: _filteredItems.map((item) {
|
|
return ShadOption(
|
|
value: item,
|
|
child: Row(
|
|
children: [
|
|
if (widget.value == item)
|
|
const Padding(
|
|
padding: EdgeInsets.only(right: 8),
|
|
child: Icon(
|
|
Icons.check,
|
|
size: 16,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
widget.itemLabel(item),
|
|
style: theme.textTheme.small.copyWith(
|
|
fontWeight: widget.value == item
|
|
? FontWeight.w600
|
|
: FontWeight.w400,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
onChanged: widget.onChanged,
|
|
),
|
|
if (widget.errorText != null) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
widget.errorText!,
|
|
style: theme.textTheme.small.copyWith(
|
|
color: theme.colorScheme.destructive,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
if (widget.helperText != null && widget.errorText == null) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
widget.helperText!,
|
|
style: theme.textTheme.small.copyWith(
|
|
color: theme.colorScheme.mutedForeground,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class CascadeSelect<T, U> extends StatefulWidget {
|
|
final String parentLabel;
|
|
final String childLabel;
|
|
final List<T> parentItems;
|
|
final String Function(T) parentItemLabel;
|
|
final Future<List<U>> Function(T) getChildItems;
|
|
final String Function(U) childItemLabel;
|
|
final T? parentValue;
|
|
final U? childValue;
|
|
final ValueChanged<T?>? onParentChanged;
|
|
final ValueChanged<U?>? onChildChanged;
|
|
final bool enabled;
|
|
final bool searchable;
|
|
final bool required;
|
|
|
|
const CascadeSelect({
|
|
super.key,
|
|
required this.parentLabel,
|
|
required this.childLabel,
|
|
required this.parentItems,
|
|
required this.parentItemLabel,
|
|
required this.getChildItems,
|
|
required this.childItemLabel,
|
|
this.parentValue,
|
|
this.childValue,
|
|
this.onParentChanged,
|
|
this.onChildChanged,
|
|
this.enabled = true,
|
|
this.searchable = true,
|
|
this.required = false,
|
|
});
|
|
|
|
@override
|
|
State<CascadeSelect<T, U>> createState() => _CascadeSelectState<T, U>();
|
|
}
|
|
|
|
class _CascadeSelectState<T, U> extends State<CascadeSelect<T, U>> {
|
|
List<U> _childItems = [];
|
|
bool _isLoadingChildren = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (widget.parentValue != null) {
|
|
_loadChildItems(widget.parentValue as T);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(CascadeSelect<T, U> oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.parentValue != widget.parentValue &&
|
|
widget.parentValue != null) {
|
|
_loadChildItems(widget.parentValue as T);
|
|
}
|
|
}
|
|
|
|
Future<void> _loadChildItems(T parentValue) async {
|
|
setState(() {
|
|
_isLoadingChildren = true;
|
|
});
|
|
|
|
try {
|
|
final items = await widget.getChildItems(parentValue);
|
|
if (mounted) {
|
|
setState(() {
|
|
_childItems = items;
|
|
_isLoadingChildren = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_childItems = [];
|
|
_isLoadingChildren = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
SuperportShadSelect<T>(
|
|
label: widget.parentLabel,
|
|
items: widget.parentItems,
|
|
itemLabel: widget.parentItemLabel,
|
|
value: widget.parentValue,
|
|
onChanged: (value) {
|
|
widget.onParentChanged?.call(value);
|
|
widget.onChildChanged?.call(null);
|
|
if (value != null) {
|
|
_loadChildItems(value);
|
|
} else {
|
|
setState(() {
|
|
_childItems = [];
|
|
});
|
|
}
|
|
},
|
|
enabled: widget.enabled,
|
|
searchable: widget.searchable,
|
|
required: widget.required,
|
|
placeholder: '${widget.parentLabel}을(를) 선택하세요',
|
|
),
|
|
const SizedBox(height: 16),
|
|
SuperportShadSelect<U>(
|
|
label: widget.childLabel,
|
|
items: _childItems,
|
|
itemLabel: widget.childItemLabel,
|
|
value: widget.childValue,
|
|
onChanged: widget.onChildChanged,
|
|
enabled: widget.enabled &&
|
|
widget.parentValue != null &&
|
|
!_isLoadingChildren,
|
|
searchable: widget.searchable,
|
|
required: widget.required,
|
|
placeholder: _isLoadingChildren
|
|
? '로딩 중...'
|
|
: widget.parentValue == null
|
|
? '먼저 ${widget.parentLabel}을(를) 선택하세요'
|
|
: '${widget.childLabel}을(를) 선택하세요',
|
|
),
|
|
],
|
|
);
|
|
}
|
|
} |