64 lines
1.8 KiB
Dart
64 lines
1.8 KiB
Dart
import 'dart:async';
|
|
|
|
/// Shared helpers for case-insensitive, trimmed, multi-field table search.
|
|
class TableSearch {
|
|
TableSearch._();
|
|
|
|
/// Trims leading/trailing whitespace. Does not alter casing.
|
|
static String normalize(String? query) => query?.trim() ?? '';
|
|
|
|
/// Trimmed + lowercased query for matching.
|
|
static String normalizedQuery(String? query) => normalize(query).toLowerCase();
|
|
|
|
/// Returns true when [query] is empty or any [values] contain the query.
|
|
static bool matches(String? query, Iterable<Object?> values) {
|
|
final q = normalizedQuery(query);
|
|
if (q.isEmpty) return true;
|
|
|
|
for (final value in values) {
|
|
if (value == null) continue;
|
|
final text = value.toString().trim().toLowerCase();
|
|
if (text.isEmpty) continue;
|
|
if (text.contains(q)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Filters [items] to those where any extracted field matches [query].
|
|
static List<T> filter<T>(
|
|
Iterable<T> items,
|
|
String? query,
|
|
Iterable<Object?> Function(T item) valuesOf,
|
|
) {
|
|
final q = normalizedQuery(query);
|
|
if (q.isEmpty) {
|
|
return items is List<T> ? items : items.toList();
|
|
}
|
|
return items.where((item) => matches(q, valuesOf(item))).toList();
|
|
}
|
|
}
|
|
|
|
/// Debounces search input so API-backed lists are not hit on every keystroke.
|
|
class SearchDebouncer {
|
|
SearchDebouncer({this.duration = const Duration(milliseconds: 300)});
|
|
|
|
final Duration duration;
|
|
Timer? _timer;
|
|
|
|
/// Runs [action] after [duration]. Empty queries run immediately.
|
|
void run(String query, void Function(String normalized) action) {
|
|
final normalized = TableSearch.normalize(query);
|
|
_timer?.cancel();
|
|
if (normalized.isEmpty) {
|
|
action(normalized);
|
|
return;
|
|
}
|
|
_timer = Timer(duration, () => action(normalized));
|
|
}
|
|
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
}
|
|
}
|