diameter/lib/components/forms/auto_complete_dropdown_button.dart
2022-03-21 01:08:05 +01:00

231 lines
6.1 KiB
Dart

import 'dart:math';
import 'package:flutter/material.dart';
class AutoCompleteDropdownButton<T> extends StatefulWidget {
final String label;
final T? selectedItem;
final List<T> items;
final void Function(T? value) onChanged;
final List<T> Function(String? value)? applyQuery;
final TextEditingController controller;
const AutoCompleteDropdownButton(
{Key? key,
this.selectedItem,
required this.label,
required this.items,
required this.onChanged,
this.applyQuery,
required this.controller})
: super(key: key);
@override
_AutoCompleteDropdownButtonState<T> createState() =>
_AutoCompleteDropdownButtonState();
}
class _AutoCompleteDropdownButtonState<T>
extends State<AutoCompleteDropdownButton<T>> {
late List<T> options;
late List<T> suggestions;
final FocusNode focusNode = FocusNode();
final LayerLink layerLink = LayerLink();
OverlayEntry? entry;
bool isOpen = false;
@override
void initState() {
super.initState();
setState(() {
options = widget.items;
suggestions = [];
});
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
void toggleOverlay() {
isOpen ? hideOverlay() : showOverlay();
}
void showOverlay() {
hideOverlay();
List<Widget> items = [];
Divider? divider;
ScrollController _scrollController = ScrollController();
final overlay = Overlay.of(context)!;
final renderBox = context.findRenderObject() as RenderBox;
if (widget.selectedItem != null) {
items.add(buildListTile(widget.selectedItem!));
}
if (suggestions.isNotEmpty) {
items.addAll(suggestions
.where((item) => item != widget.selectedItem)
.map((item) => buildListTile(item)));
}
if ((widget.selectedItem != null || suggestions.isNotEmpty) &&
(options.length -
suggestions.length -
(widget.selectedItem == null ? 0 : 1) >
0)) {
divider = const Divider(height: 10);
items.add(divider);
}
items.addAll(options
.where((item) =>
!(widget.selectedItem != null &&
item.toString() == widget.selectedItem!.toString()) &&
!suggestions.contains(item))
.map((item) => buildListTile(item))
.toList());
final screenHeight = MediaQuery.of(context).size.height;
final neededHeight = renderBox.size.height * (items.length - 1) +
(divider?.height ?? renderBox.size.height);
final availableHeight = screenHeight -
(renderBox.localToGlobal(Offset.zero).dy + renderBox.size.height);
bool displayAbove = neededHeight > availableHeight &&
screenHeight - availableHeight > availableHeight;
final height = min(neededHeight,
max(availableHeight, screenHeight - availableHeight - 55) - 55);
entry = OverlayEntry(
builder: (context) => Positioned(
width: renderBox.size.width,
height: height,
child: CompositedTransformFollower(
link: layerLink,
targetAnchor: displayAbove ? Alignment.bottomLeft : Alignment.topLeft,
followerAnchor:
displayAbove ? Alignment.bottomLeft : Alignment.topLeft,
offset: Offset(0, renderBox.size.height * (displayAbove ? -1 : 1)),
showWhenUnlinked: false,
child: Scrollbar(
controller: _scrollController,
// thumbVisibility: true,
isAlwaysShown: true,
child: Material(
elevation: 8,
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: items,
),
),
),
),
),
),
);
overlay.insert(entry!);
isOpen = true;
}
ListTile buildListTile(T item) {
return ListTile(
onTap: () => handleChanged(item),
selected: item == widget.selectedItem,
title: Row(
children: [
Flexible(
child: Text(item.toString()),
),
],
),
);
}
void hideOverlay() {
entry?.remove();
setState(() {
entry = null;
isOpen = false;
});
}
void handleChanged(T? item) {
widget.onChanged(item);
hideOverlay();
}
void onChangeQuery(String value) {
if (value.trim() == '' ||
(widget.selectedItem != null &&
value == widget.selectedItem!.toString())) {
setState(() {
suggestions = [];
});
} else {
if (widget.applyQuery == null) {
setState(() {
suggestions = widget.items.where((item) {
String itemString = item.toString().toLowerCase();
String valueLowercase = value.toLowerCase();
return itemString.contains(valueLowercase);
}).toList();
});
} else {
setState(() {
suggestions = widget.applyQuery!(value)
.where((item) => item != widget.selectedItem)
.toList();
});
}
showOverlay();
}
}
@override
Widget build(BuildContext context) {
return Focus(
focusNode: focusNode,
onFocusChange: (isFocused) {
if (!isFocused) {
hideOverlay();
}
},
child: CompositedTransformTarget(
link: layerLink,
child: TextFormField(
onChanged: onChangeQuery,
onTap: toggleOverlay,
controller: widget.controller,
decoration: InputDecoration(
labelText: widget.label,
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
widget.selectedItem != null
? IconButton(
onPressed: () => handleChanged(null),
icon: const Icon(Icons.close),
iconSize: 20.0,
)
: Container(),
IconButton(
onPressed: toggleOverlay,
icon: const Icon(Icons.arrow_drop_down),
),
],
),
),
),
),
);
}
}