mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
Merge pull request #40 from dougborg/investigate/dropdown-dismiss
Fix suggestion dropdown dismiss on tap outside
This commit is contained in:
@@ -144,6 +144,7 @@ const int kDefaultMaxNodes = 500; // Default maximum number of nodes to render o
|
|||||||
|
|
||||||
// NSI (Name Suggestion Index) configuration
|
// NSI (Name Suggestion Index) configuration
|
||||||
const int kNSIMinimumHitCount = 500; // Minimum hit count for NSI suggestions to be considered useful
|
const int kNSIMinimumHitCount = 500; // Minimum hit count for NSI suggestions to be considered useful
|
||||||
|
const int kNSIMaxSuggestions = 10; // Maximum number of tag value suggestions to fetch and display
|
||||||
|
|
||||||
// Map interaction configuration
|
// Map interaction configuration
|
||||||
const double kNodeDoubleTapZoomDelta = 1.0; // How much to zoom in when double-tapping nodes (was 1.0)
|
const double kNodeDoubleTapZoomDelta = 1.0; // How much to zoom in when double-tapping nodes (was 1.0)
|
||||||
|
|||||||
@@ -84,8 +84,7 @@ class NSIService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit to top 10 suggestions for UI performance
|
if (suggestions.length >= kNSIMaxSuggestions) break;
|
||||||
if (suggestions.length >= 10) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return suggestions;
|
return suggestions;
|
||||||
|
|||||||
@@ -25,33 +25,26 @@ class NSITagValueField extends StatefulWidget {
|
|||||||
|
|
||||||
class _NSITagValueFieldState extends State<NSITagValueField> {
|
class _NSITagValueFieldState extends State<NSITagValueField> {
|
||||||
late TextEditingController _controller;
|
late TextEditingController _controller;
|
||||||
List<String> _suggestions = [];
|
|
||||||
bool _showingSuggestions = false;
|
|
||||||
final LayerLink _layerLink = LayerLink();
|
|
||||||
late OverlayEntry _overlayEntry;
|
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
List<String> _suggestions = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = TextEditingController(text: widget.initialValue);
|
_controller = TextEditingController(text: widget.initialValue);
|
||||||
_loadSuggestions();
|
_loadSuggestions();
|
||||||
|
|
||||||
_focusNode.addListener(_onFocusChanged);
|
|
||||||
_controller.addListener(_onTextChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(NSITagValueField oldWidget) {
|
void didUpdateWidget(NSITagValueField oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
// If the tag key changed, reload suggestions
|
// If the tag key changed, reload suggestions
|
||||||
if (oldWidget.tagKey != widget.tagKey) {
|
if (oldWidget.tagKey != widget.tagKey) {
|
||||||
_hideSuggestions(); // Hide old suggestions immediately
|
|
||||||
_suggestions.clear();
|
_suggestions.clear();
|
||||||
_loadSuggestions(); // Load new suggestions for new key
|
_loadSuggestions();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the initial value changed, update the controller
|
// If the initial value changed, update the controller
|
||||||
if (oldWidget.initialValue != widget.initialValue) {
|
if (oldWidget.initialValue != widget.initialValue) {
|
||||||
_controller.text = widget.initialValue;
|
_controller.text = widget.initialValue;
|
||||||
@@ -62,38 +55,17 @@ class _NSITagValueFieldState extends State<NSITagValueField> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
_hideSuggestions();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get filtered suggestions based on current text input (case-sensitive)
|
|
||||||
List<String> _getFilteredSuggestions() {
|
|
||||||
final currentText = _controller.text;
|
|
||||||
if (currentText.isEmpty) {
|
|
||||||
return _suggestions;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _suggestions
|
|
||||||
.where((suggestion) => suggestion.contains(currentText))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle text changes to update suggestion filtering
|
|
||||||
void _onTextChanged() {
|
|
||||||
if (_showingSuggestions) {
|
|
||||||
// Update the overlay with filtered suggestions
|
|
||||||
_updateSuggestionsOverlay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _loadSuggestions() async {
|
void _loadSuggestions() async {
|
||||||
if (widget.tagKey.trim().isEmpty) return;
|
if (widget.tagKey.trim().isEmpty) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final suggestions = await NSIService().getAllSuggestions(widget.tagKey);
|
final suggestions = await NSIService().getAllSuggestions(widget.tagKey);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_suggestions = suggestions.take(10).toList(); // Limit to 10 suggestions
|
_suggestions = suggestions;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -106,52 +78,71 @@ class _NSITagValueFieldState extends State<NSITagValueField> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onFocusChanged() {
|
InputDecoration _buildDecoration({required bool showDropdownIcon}) {
|
||||||
final filteredSuggestions = _getFilteredSuggestions();
|
return InputDecoration(
|
||||||
if (_focusNode.hasFocus && filteredSuggestions.isNotEmpty && !widget.readOnly) {
|
hintText: widget.hintText,
|
||||||
_showSuggestions();
|
border: const OutlineInputBorder(),
|
||||||
} else {
|
isDense: true,
|
||||||
_hideSuggestions();
|
suffixIcon: showDropdownIcon
|
||||||
}
|
? Icon(
|
||||||
|
Icons.arrow_drop_down,
|
||||||
|
color: _focusNode.hasFocus
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: Colors.grey,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showSuggestions() {
|
@override
|
||||||
final filteredSuggestions = _getFilteredSuggestions();
|
Widget build(BuildContext context) {
|
||||||
if (_showingSuggestions || filteredSuggestions.isEmpty) return;
|
if (widget.readOnly) {
|
||||||
|
return TextField(
|
||||||
_overlayEntry = _buildSuggestionsOverlay(filteredSuggestions);
|
controller: _controller,
|
||||||
Overlay.of(context).insert(_overlayEntry);
|
focusNode: _focusNode,
|
||||||
setState(() {
|
readOnly: true,
|
||||||
_showingSuggestions = true;
|
decoration: _buildDecoration(showDropdownIcon: false),
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the suggestions overlay with current filtered suggestions
|
|
||||||
void _updateSuggestionsOverlay() {
|
|
||||||
final filteredSuggestions = _getFilteredSuggestions();
|
|
||||||
|
|
||||||
if (filteredSuggestions.isEmpty) {
|
|
||||||
_hideSuggestions();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_showingSuggestions) {
|
|
||||||
// Remove current overlay and create new one with filtered suggestions
|
|
||||||
_overlayEntry.remove();
|
|
||||||
_overlayEntry = _buildSuggestionsOverlay(filteredSuggestions);
|
|
||||||
Overlay.of(context).insert(_overlayEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the suggestions overlay with the given suggestions list
|
return RawAutocomplete<String>(
|
||||||
OverlayEntry _buildSuggestionsOverlay(List<String> suggestions) {
|
textEditingController: _controller,
|
||||||
return OverlayEntry(
|
focusNode: _focusNode,
|
||||||
builder: (context) => Positioned(
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||||
width: 250, // Slightly wider to fit more content in refine tags
|
if (_suggestions.isEmpty) return const Iterable<String>.empty();
|
||||||
child: CompositedTransformFollower(
|
if (textEditingValue.text.isEmpty) return _suggestions;
|
||||||
link: _layerLink,
|
return _suggestions
|
||||||
showWhenUnlinked: false,
|
.where((s) => s.contains(textEditingValue.text));
|
||||||
offset: const Offset(0.0, 35.0), // Below the text field
|
},
|
||||||
|
onSelected: (String selection) {
|
||||||
|
widget.onChanged(selection);
|
||||||
|
},
|
||||||
|
fieldViewBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
TextEditingController controller,
|
||||||
|
FocusNode focusNode,
|
||||||
|
VoidCallback onFieldSubmitted,
|
||||||
|
) {
|
||||||
|
return TextField(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
decoration: _buildDecoration(
|
||||||
|
showDropdownIcon: _suggestions.isNotEmpty,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
widget.onChanged(value);
|
||||||
|
},
|
||||||
|
onSubmitted: (_) => onFieldSubmitted(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
optionsViewBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
AutocompleteOnSelected<String> onSelected,
|
||||||
|
Iterable<String> options,
|
||||||
|
) {
|
||||||
|
final optionList = options.toList(growable: false);
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: 4.0,
|
elevation: 4.0,
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
@@ -160,68 +151,20 @@ class _NSITagValueFieldState extends State<NSITagValueField> {
|
|||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: suggestions.length,
|
itemCount: optionList.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final suggestion = suggestions[index];
|
final option = optionList[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(suggestion, style: const TextStyle(fontSize: 14)),
|
title: Text(option, style: const TextStyle(fontSize: 14)),
|
||||||
onTap: () => _selectSuggestion(suggestion),
|
onTap: () => onSelected(option),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
void _hideSuggestions() {
|
|
||||||
if (!_showingSuggestions) return;
|
|
||||||
|
|
||||||
_overlayEntry.remove();
|
|
||||||
setState(() {
|
|
||||||
_showingSuggestions = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectSuggestion(String suggestion) {
|
|
||||||
_controller.text = suggestion;
|
|
||||||
widget.onChanged(suggestion);
|
|
||||||
_hideSuggestions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final filteredSuggestions = _getFilteredSuggestions();
|
|
||||||
|
|
||||||
return CompositedTransformTarget(
|
|
||||||
link: _layerLink,
|
|
||||||
child: TextField(
|
|
||||||
controller: _controller,
|
|
||||||
focusNode: _focusNode,
|
|
||||||
readOnly: widget.readOnly,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: widget.hintText,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
isDense: true,
|
|
||||||
suffixIcon: _suggestions.isNotEmpty && !widget.readOnly
|
|
||||||
? Icon(
|
|
||||||
Icons.arrow_drop_down,
|
|
||||||
color: _showingSuggestions ? Theme.of(context).primaryColor : Colors.grey,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
onChanged: widget.readOnly ? null : (value) {
|
|
||||||
widget.onChanged(value);
|
|
||||||
},
|
|
||||||
onTap: () {
|
|
||||||
if (!widget.readOnly && filteredSuggestions.isNotEmpty) {
|
|
||||||
_showSuggestions();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user