tasq/lib/screens/searchable_multi_select_dropdown.dart

178 lines
5.7 KiB
Dart

import 'package:flutter/material.dart';
/// Searchable multi-select dropdown with chips and 'Select All' option
class SearchableMultiSelectDropdown<T> extends StatefulWidget {
const SearchableMultiSelectDropdown({
Key? key,
required this.label,
required this.items,
required this.selectedIds,
required this.getId,
required this.getLabel,
required this.onChanged,
}) : super(key: key);
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(
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,
),
],
),
),
],
);
}
}