181 lines
5.8 KiB
Dart
181 lines
5.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/app_surfaces.dart';
|
|
|
|
/// Searchable multi-select dropdown with chips and 'Select All' option
|
|
class SearchableMultiSelectDropdown<T> extends StatefulWidget {
|
|
const SearchableMultiSelectDropdown({
|
|
super.key,
|
|
required this.label,
|
|
required this.items,
|
|
required this.selectedIds,
|
|
required this.getId,
|
|
required this.getLabel,
|
|
required this.onChanged,
|
|
});
|
|
|
|
final String label;
|
|
final List<T> items;
|
|
final List<String> selectedIds;
|
|
final String Function(T) getId;
|
|
final String Function(T) getLabel;
|
|
final ValueChanged<List<String>> onChanged;
|
|
|
|
@override
|
|
State<SearchableMultiSelectDropdown<T>> createState() =>
|
|
SearchableMultiSelectDropdownState<T>();
|
|
}
|
|
|
|
class SearchableMultiSelectDropdownState<T>
|
|
extends State<SearchableMultiSelectDropdown<T>> {
|
|
late List<String> _selectedIds;
|
|
String _search = '';
|
|
bool _selectAll = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectedIds = List<String>.from(widget.selectedIds);
|
|
_selectAll =
|
|
_selectedIds.length == widget.items.length && widget.items.isNotEmpty;
|
|
}
|
|
|
|
void _openDropdown() async {
|
|
await showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return StatefulBuilder(
|
|
builder: (context, setState) {
|
|
final filtered = widget.items
|
|
.where(
|
|
(item) => widget
|
|
.getLabel(item)
|
|
.toLowerCase()
|
|
.contains(_search.toLowerCase()),
|
|
)
|
|
.toList();
|
|
return AlertDialog(
|
|
shape: AppSurfaces.of(context).dialogShape,
|
|
title: Text('Select ${widget.label}'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextField(
|
|
decoration: const InputDecoration(hintText: 'Search...'),
|
|
onChanged: (v) => setState(() => _search = v),
|
|
),
|
|
CheckboxListTile(
|
|
value: _selectAll,
|
|
title: const Text('Select All'),
|
|
onChanged: (checked) {
|
|
setState(() {
|
|
_selectAll = checked ?? false;
|
|
if (_selectAll) {
|
|
_selectedIds = widget.items
|
|
.map(widget.getId)
|
|
.toList();
|
|
} else {
|
|
_selectedIds.clear();
|
|
}
|
|
});
|
|
},
|
|
),
|
|
SizedBox(
|
|
height: 200,
|
|
child: ListView(
|
|
children: [
|
|
for (final item in filtered)
|
|
CheckboxListTile(
|
|
value: _selectedIds.contains(widget.getId(item)),
|
|
title: Text(widget.getLabel(item)),
|
|
onChanged: (checked) {
|
|
setState(() {
|
|
final id = widget.getId(item);
|
|
if (checked == true) {
|
|
_selectedIds.add(id);
|
|
} else {
|
|
_selectedIds.remove(id);
|
|
}
|
|
_selectAll =
|
|
_selectedIds.length ==
|
|
widget.items.length &&
|
|
widget.items.isNotEmpty;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.of(context).pop(_selectedIds),
|
|
child: const Text('Done'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
).then((result) {
|
|
if (result is List<String>) {
|
|
setState(() {
|
|
_selectedIds = result;
|
|
_selectAll =
|
|
_selectedIds.length == widget.items.length &&
|
|
widget.items.isNotEmpty;
|
|
});
|
|
widget.onChanged(_selectedIds);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
InputDecorator(
|
|
decoration: InputDecoration(labelText: widget.label),
|
|
child: Wrap(
|
|
spacing: 8,
|
|
runSpacing: 4,
|
|
children: [
|
|
..._selectedIds.map((id) {
|
|
T item;
|
|
try {
|
|
item = widget.items.firstWhere((e) => widget.getId(e) == id);
|
|
} catch (_) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
return Chip(
|
|
label: Text(widget.getLabel(item)),
|
|
onDeleted: () {
|
|
setState(() {
|
|
_selectedIds.remove(id);
|
|
_selectAll =
|
|
_selectedIds.length == widget.items.length &&
|
|
widget.items.isNotEmpty;
|
|
});
|
|
widget.onChanged(_selectedIds);
|
|
},
|
|
);
|
|
}),
|
|
ActionChip(
|
|
label: Text('Select'),
|
|
avatar: const Icon(Icons.arrow_drop_down),
|
|
onPressed: _openDropdown,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|