Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New regex option #10

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ AutocompleteTrigger(
// "Hello @l" -> Shows zero suggestions.
// "Hello @lu" -> Shows suggestions for @lu.
minimumRequiredCharacters: 2,

// The pattern accepted by [trigger] to recognize in the
// input text
pattern: RegExp(r'^[\w.]*$'),

// The options view builder is used to build the options view
// that will be shown when the [trigger] is detected.
Expand Down
2 changes: 1 addition & 1 deletion example/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app')
}

task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
1 change: 1 addition & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class _MyHomePageState extends State<MyHomePage> {
),
AutocompleteTrigger(
trigger: ':',
pattern: RegExp(r'^[\w:]*$'),
optionsViewBuilder: (context, autocompleteQuery, controller) {
return EmojiAutocompleteOptions(
query: autocompleteQuery.query,
Expand Down
115 changes: 83 additions & 32 deletions example/lib/src/options/hashtag_autocomplete_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:flutter/material.dart';

import 'package:example/src/models.dart';

class HashtagAutocompleteOptions extends StatelessWidget {
class HashtagAutocompleteOptions extends StatefulWidget {
const HashtagAutocompleteOptions({
Key? key,
required this.query,
Expand All @@ -14,15 +14,49 @@ class HashtagAutocompleteOptions extends StatelessWidget {
final ValueSetter<Hashtag> onHashtagTap;

@override
Widget build(BuildContext context) {
final hashtags = kHashtags.where((it) {
State<HashtagAutocompleteOptions> createState() =>
_HashtagAutocompleteOptionsState();
}

class _HashtagAutocompleteOptionsState
extends State<HashtagAutocompleteOptions> {
bool _isLoading = false;
Iterable<Hashtag> _items = List.empty();

@override
void initState() {
super.initState();

_search();
}

@override
void didUpdateWidget(HashtagAutocompleteOptions oldWidget) {
super.didUpdateWidget(oldWidget);

_search();
}

Future<void> _search() async {
setState(() {
_isLoading = true;
});

await Future.delayed(const Duration(milliseconds: 500));

_items = kHashtags.where((it) {
final normalizedOption = it.name.toLowerCase();
final normalizedQuery = query.toLowerCase();
final normalizedQuery = widget.query.toLowerCase();
return normalizedOption.contains(normalizedQuery);
});

if (hashtags.isEmpty) return const SizedBox.shrink();
_isLoading = false;

setState(() {});
}

@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(8),
elevation: 2,
Expand All @@ -38,41 +72,58 @@ class HashtagAutocompleteOptions extends StatelessWidget {
child: ListTile(
dense: true,
horizontalTitleGap: 0,
title: Text("Hashtags matching '$query'"),
title: Text("Hashtags matching '${widget.query}'"),
),
),
const Divider(height: 0),
LimitedBox(
maxHeight: MediaQuery.of(context).size.height * 0.3,
child: ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: hashtags.length,
separatorBuilder: (_, __) => const Divider(height: 0),
itemBuilder: (context, i) {
final hashtag = hashtags.elementAt(i);
return ListTile(
dense: true,
leading: CircleAvatar(
backgroundColor: const Color(0xFFF7F7F8),
backgroundImage: NetworkImage(
hashtag.image,
scale: 0.5,
),
),
title: Text('#${hashtag.name}'),
subtitle: Text(
hashtag.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => onHashtagTap(hashtag),
);
},
),
child: (_isLoading) ? _buildLoader() : _buildList(),
),
],
),
);
}

Widget _buildLoader() {
return const Padding(
padding: EdgeInsets.all(16.0),
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
);
}

Widget _buildList() {
return ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: _items.length,
separatorBuilder: (_, __) => const Divider(height: 0),
itemBuilder: (context, i) {
final hashtag = _items.elementAt(i);
return ListTile(
dense: true,
leading: CircleAvatar(
backgroundColor: const Color(0xFFF7F7F8),
backgroundImage: NetworkImage(
hashtag.image,
scale: 0.5,
),
),
title: Text('#${hashtag.name}'),
subtitle: Text(
hashtag.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => widget.onHashtagTap(hashtag),
);
},
);
}
}
2 changes: 1 addition & 1 deletion example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
flutter_parsed_text: ^2.2.1
google_fonts: ^3.0.1
google_fonts: ^4.0.4
multi_trigger_autocomplete:
path: ../

Expand Down
17 changes: 14 additions & 3 deletions lib/src/autocomplete_trigger.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:multi_trigger_autocomplete/src/autocomplete_query.dart';

Expand All @@ -12,9 +14,10 @@ typedef AutocompleteTriggerOptionsViewBuilder = Widget Function(
class AutocompleteTrigger {
/// Creates a [AutocompleteTrigger] which can be used to trigger
/// autocomplete suggestions.
const AutocompleteTrigger({
AutocompleteTrigger({
required this.trigger,
required this.optionsViewBuilder,
this.pattern,
this.triggerOnlyAtStart = false,
this.triggerOnlyAfterSpace = true,
this.minimumRequiredCharacters = 0,
Expand All @@ -28,6 +31,9 @@ class AutocompleteTrigger {
/// Whether the [trigger] should only be recognised at the start of the input.
final bool triggerOnlyAtStart;

/// The pattern accepted by [trigger] to recognize autocomplete options
RegExp? pattern;

/// Whether the [trigger] should only be recognised after a space.
final bool triggerOnlyAfterSpace;

Expand Down Expand Up @@ -61,6 +67,9 @@ class AutocompleteTrigger {
/// Checks if the user is invoking the recognising [trigger] and returns
/// the autocomplete query if so.
AutocompleteQuery? invokingTrigger(TextEditingValue textEditingValue) {
// If the pattern is not defined, the default is set
pattern ??= RegExp(r'^[\w.]*$');

final text = textEditingValue.text;
final cursorPosition = textEditingValue.selection.baseOffset;

Expand All @@ -81,9 +90,11 @@ class AutocompleteTrigger {
// valid examples: "@user", "Hello @user"
// invalid examples: "Hello@user"
final textBeforeTrigger = text.substring(0, firstTriggerIndexBeforeCursor);
final lastCharBeforeTrigger =
textBeforeTrigger.substring(max(textBeforeTrigger.length - 1, 0));
if (triggerOnlyAfterSpace &&
textBeforeTrigger.isNotEmpty &&
!textBeforeTrigger.endsWith(' ')) {
!(lastCharBeforeTrigger == ' ' || lastCharBeforeTrigger == '\n')) {
return null;
}

Expand All @@ -96,7 +107,7 @@ class AutocompleteTrigger {
// valid example: "@luke_skywa..."
// invalid example: "@luke skywa..."
final suggestionText = text.substring(suggestionStart, suggestionEnd);
if (suggestionText.contains(' ')) return null;
if (!pattern!.hasMatch(suggestionText)) return null;

// A minimum number of characters can be provided to only show
// suggestions after the customer has input enough characters.
Expand Down