mirror of
https://github.com/foss42/apidash.git
synced 2025-05-24 17:56:44 +08:00
add multi_trigger_autocomplete_plus
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
|
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete.dart';
|
||||||
import 'package:extended_text_field/extended_text_field.dart';
|
import 'package:extended_text_field/extended_text_field.dart';
|
||||||
import 'env_regexp_span_builder.dart';
|
import 'env_regexp_span_builder.dart';
|
||||||
import 'env_trigger_options.dart';
|
import 'env_trigger_options.dart';
|
||||||
|
30
packages/multi_trigger_autocomplete_plus/.gitignore
vendored
Normal file
30
packages/multi_trigger_autocomplete_plus/.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
/pubspec.lock
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
.packages
|
||||||
|
build/
|
24
packages/multi_trigger_autocomplete_plus/CHANGELOG.md
Normal file
24
packages/multi_trigger_autocomplete_plus/CHANGELOG.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
## 1.0.2
|
||||||
|
|
||||||
|
- Added `AutocompleteNoTrigger` to display autocomplete suggestions without requiring any trigger character or string.
|
||||||
|
|
||||||
|
## 1.0.1
|
||||||
|
|
||||||
|
- Made `triggerEnd` customizable instead of using a hardcoded space (`' '`).
|
||||||
|
- Enhanced the handling of triggers that share a common prefix, such as `{` and `{{`, ensuring that the correct trigger is identified and processed.
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Fixed `FieldView` focus getting lost on clicking the `OptionsView` on non-mobile platforms.
|
||||||
|
- Fixed `RangeError` when `textEditingValue.selection.isInvalid`.
|
||||||
|
[#11](https://github.com/xsahil03x/multi_trigger_autocomplete/issues/11)
|
||||||
|
- Fixed `AutocompleteTrigger` not getting triggered when the text is a multi-line
|
||||||
|
string. [#12](https://github.com/xsahil03x/multi_trigger_autocomplete/issues/12)
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
- Fixed Readme.
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
- Initial release.
|
21
packages/multi_trigger_autocomplete_plus/LICENSE
Normal file
21
packages/multi_trigger_autocomplete_plus/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Sahil Kumar
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
281
packages/multi_trigger_autocomplete_plus/README.md
Normal file
281
packages/multi_trigger_autocomplete_plus/README.md
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
# Multi Trigger Autocomplete Plus
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/MIT) [](https://github.com/xsahil03x/multi_trigger_autocomplete/blob/master/LICENSE) [](https://github.com/xsahil03x/multi_trigger_autocomplete/actions) [](https://codecov.io/gh/xsahil03x/multi_trigger_autocomplete) [](https://pub.dartlang.org/packages/multi_trigger_autocomplete)
|
||||||
|
|
||||||
|
> This is a fork from [multi_trigger_autocomplete](https://pub.dev/packages/multi_trigger_autocomplete)
|
||||||
|
|
||||||
|
A flutter widget to add trigger based autocomplete functionality to your app.
|
||||||
|
|
||||||
|
**Show some ❤️ and star the repo to support the project**
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus/blob/master/asset/package_demo.gif?raw=true" alt="An animated image of the MultiTriggerAutocomplete" height="400"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Improvements
|
||||||
|
|
||||||
|
This fork includes the following improvements over the original package:
|
||||||
|
|
||||||
|
- **AutocompleteNoTrigger**: A Special trigger which allows allows autcomplete suggestions without a trigger character/string.
|
||||||
|
- **Enhanced Customization**: Allows customization of `triggerEnd` instead of using a hardcoded space (`' '`).
|
||||||
|
- **Prefix Triggers Handling**: Correctly identifies and handles triggers that share a common prefix, such as `{` and `{{`.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add the following to your `pubspec.yaml` and replace `[version]` with the latest version:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
multi_trigger_autocomplete_plus: ^[version]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To use this package you must first wrap your top most widget
|
||||||
|
with [Portal](https://pub.dev/documentation/flutter_portal/latest/flutter_portal/Portal-class.html) as this package
|
||||||
|
uses [flutter_portal](https://pub.dev/packages/flutter_portal)
|
||||||
|
to show the options view.
|
||||||
|
|
||||||
|
(Credits to: [Remi Rousselet](https://github.com/rrousselGit))
|
||||||
|
|
||||||
|
> `Portal`, is the equivalent of [Overlay].
|
||||||
|
>
|
||||||
|
> This widget will need to be inserted above the widget that needs to render
|
||||||
|
> _under_ your overlays.
|
||||||
|
>
|
||||||
|
> If you want to display your overlays on the top of _everything_, a good place
|
||||||
|
> to insert that `Portal` is above `MaterialApp`:
|
||||||
|
>
|
||||||
|
> ```dart
|
||||||
|
> Portal(
|
||||||
|
> child: MaterialApp(
|
||||||
|
> ...
|
||||||
|
> )
|
||||||
|
> );
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> (works for `CupertinoApp` too)
|
||||||
|
>
|
||||||
|
> This way `Portal` will render above everything. But you could place it
|
||||||
|
> somewhere else to change the clip behavior.
|
||||||
|
|
||||||
|
Import the package:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the widget:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
MultiTriggerAutocomplete(
|
||||||
|
optionsAlignment: OptionsAlignment.topStart,
|
||||||
|
autocompleteTriggers: [
|
||||||
|
// Add the triggers you want to use for autocomplete
|
||||||
|
AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
||||||
|
return MentionAutocompleteOptions(
|
||||||
|
query: autocompleteQuery.query,
|
||||||
|
onMentionUserTap: (user) {
|
||||||
|
final autocomplete = MultiTriggerAutocomplete.of(context);
|
||||||
|
return autocomplete.acceptAutocompleteOption(user.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AutocompleteTrigger(
|
||||||
|
trigger: '#',
|
||||||
|
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
||||||
|
return HashtagAutocompleteOptions(
|
||||||
|
query: autocompleteQuery.query,
|
||||||
|
onHashtagTap: (hashtag) {
|
||||||
|
final autocomplete = MultiTriggerAutocomplete.of(context);
|
||||||
|
return autocomplete
|
||||||
|
.acceptAutocompleteOption(hashtag.name);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AutocompleteTrigger(
|
||||||
|
trigger: ':',
|
||||||
|
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
||||||
|
return EmojiAutocompleteOptions(
|
||||||
|
query: autocompleteQuery.query,
|
||||||
|
onEmojiTap: (emoji) {
|
||||||
|
final autocomplete = MultiTriggerAutocomplete.of(context);
|
||||||
|
return autocomplete.acceptAutocompleteOption(
|
||||||
|
emoji.char,
|
||||||
|
// Passing false as we don't want the trigger [:] to
|
||||||
|
// get prefixed to the option in case of emoji.
|
||||||
|
keepTrigger: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// Add the text field widget you want to use for autocomplete
|
||||||
|
fieldViewBuilder: (context, controller, focusNode) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: ChatMessageTextField(
|
||||||
|
focusNode: focusNode,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
| Mention Autocomplete | Hashtag Autocomplete | Emoji Autocomplete |
|
||||||
|
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| <img src="https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus/blob/master/asset/mention_demo.gif?raw=true" height="400" alt="Mention Autocomplete"/> | <img src="https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus/blob/master/asset/hashtag_demo.gif?raw=true" height="400" alt="Hashtag Autocomplete"/> | <img src="https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus/blob/master/asset/emoji_demo.gif?raw=true" height="400" alt="Emoji Autocomplete"/> |
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### MultiTriggerAutocomplete
|
||||||
|
|
||||||
|
```dart
|
||||||
|
MultiTriggerAutocomplete(
|
||||||
|
// Defines the autocomplete trigger that will be used to match the
|
||||||
|
// text.
|
||||||
|
autocompleteTriggers: autocompleteTriggers,
|
||||||
|
|
||||||
|
// Defines the alignment of the options view relative to the
|
||||||
|
// fieldView.
|
||||||
|
//
|
||||||
|
// By default, the options view is aligned to the bottom of the
|
||||||
|
// fieldView.
|
||||||
|
optionsAlignment: OptionsAlignment.topStart,
|
||||||
|
|
||||||
|
// Defines the width to make the options as a multiple of the width
|
||||||
|
// of the fieldView.
|
||||||
|
//
|
||||||
|
// Setting this to 1 makes the options view width matches the width
|
||||||
|
// of the fieldView.
|
||||||
|
//
|
||||||
|
// Use null to remove this constraint.
|
||||||
|
optionsWidthFactor: 1.0,
|
||||||
|
|
||||||
|
// Defines the duration of the debounce period for the
|
||||||
|
// [TextEditingController].
|
||||||
|
//
|
||||||
|
// This is the time between the last character typed and the matching
|
||||||
|
// is performed.
|
||||||
|
debounceDuration: const Duration(milliseconds: 350),
|
||||||
|
|
||||||
|
// Defines the initial value to set in the internal
|
||||||
|
// [TextEditingController].
|
||||||
|
//
|
||||||
|
// This value will be ignored if [TextEditingController] is provided.
|
||||||
|
initialValue: const TextEditingValue(text: 'Hello'),
|
||||||
|
|
||||||
|
// Defines the [TextEditingController] that will be used for the
|
||||||
|
// fieldView.
|
||||||
|
//
|
||||||
|
// If this parameter is provided, then [focusNode] must also be
|
||||||
|
// provided.
|
||||||
|
textEditingController: TextEditingController(text: 'Hello'),
|
||||||
|
|
||||||
|
// Defines the [FocusNode] that will be used for the fieldView.
|
||||||
|
//
|
||||||
|
// If this parameter is provided, then [textEditingController] must
|
||||||
|
// also be provided.
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
|
||||||
|
// Defines the fieldView that will be used to input the text.
|
||||||
|
//
|
||||||
|
// By default, a [TextFormField] is used.
|
||||||
|
fieldViewBuilder: (context, controller, focusNode) {
|
||||||
|
return TextField(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
### AutocompleteTrigger
|
||||||
|
|
||||||
|
```dart
|
||||||
|
AutocompleteTrigger(
|
||||||
|
// The trigger string/character that will be used to trigger the
|
||||||
|
// autocomplete.
|
||||||
|
trigger: '@',
|
||||||
|
|
||||||
|
// The trigger end string/character that will be used to determine
|
||||||
|
// the end of the trigger.
|
||||||
|
// By default, it's a space.
|
||||||
|
triggerEnd: ' ',
|
||||||
|
|
||||||
|
// If true, the [trigger] should only be recognised at
|
||||||
|
// the start of the input text.
|
||||||
|
//
|
||||||
|
// valid example: "@luke hello"
|
||||||
|
// invalid example: "Hello @luke"
|
||||||
|
triggerOnlyAtStart: false,
|
||||||
|
|
||||||
|
// If true, the [trigger] should only be recognised after
|
||||||
|
// a space.
|
||||||
|
//
|
||||||
|
// valid example: "@luke", "Hello @luke"
|
||||||
|
// invalid example: "Hello@luke"
|
||||||
|
triggerOnlyAfterSpace: true,
|
||||||
|
|
||||||
|
// A minimum number of characters can be provided to only show
|
||||||
|
// suggestions after the user has input enough characters.
|
||||||
|
//
|
||||||
|
// example:
|
||||||
|
// "Hello @l" -> Shows zero suggestions.
|
||||||
|
// "Hello @lu" -> Shows suggestions for @lu.
|
||||||
|
minimumRequiredCharacters: 2,
|
||||||
|
|
||||||
|
// The options view builder is used to build the options view
|
||||||
|
// that will be shown when the [trigger] is detected.
|
||||||
|
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
||||||
|
return MentionAutocompleteOptions(
|
||||||
|
query: autocompleteQuery.query,
|
||||||
|
onMentionUserTap: (user) {
|
||||||
|
// Accept the autocomplete option.
|
||||||
|
final autocomplete = MultiTriggerAutocomplete.of(context);
|
||||||
|
return autocomplete.acceptAutocompleteOption(user.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### AutocompleteNoTrigger
|
||||||
|
|
||||||
|
Can be used to display autocomplete suggestions without requiring a trigger string or character.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
AutocompleteNoTrigger(
|
||||||
|
// A minimum number of characters can be provided to only show
|
||||||
|
// suggestions after the user has input enough characters.
|
||||||
|
minimumRequiredCharacters: 2,
|
||||||
|
|
||||||
|
// The options view builder is used to build the options view
|
||||||
|
// that will be shown when the [trigger] is detected.
|
||||||
|
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
||||||
|
return MentionAutocompleteOptions(
|
||||||
|
query: autocompleteQuery.query,
|
||||||
|
onMentionUserTap: (user) {
|
||||||
|
// Accept the autocomplete option.
|
||||||
|
final autocomplete = MultiTriggerAutocomplete.of(context);
|
||||||
|
return autocomplete.acceptAutocompleteOption(user.id);
|
||||||
|
|
||||||
|
// Handle field unfocusing manually using a FocusNode
|
||||||
|
focusNode.unfocus();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT License](LICENSE)
|
@ -0,0 +1,4 @@
|
|||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
BIN
packages/multi_trigger_autocomplete_plus/asset/emoji_demo.gif
Normal file
BIN
packages/multi_trigger_autocomplete_plus/asset/emoji_demo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 MiB |
BIN
packages/multi_trigger_autocomplete_plus/asset/hashtag_demo.gif
Normal file
BIN
packages/multi_trigger_autocomplete_plus/asset/hashtag_demo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 MiB |
BIN
packages/multi_trigger_autocomplete_plus/asset/mention_demo.gif
Normal file
BIN
packages/multi_trigger_autocomplete_plus/asset/mention_demo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
BIN
packages/multi_trigger_autocomplete_plus/asset/package_demo.gif
Normal file
BIN
packages/multi_trigger_autocomplete_plus/asset/package_demo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 MiB |
47
packages/multi_trigger_autocomplete_plus/example/.gitignore
vendored
Normal file
47
packages/multi_trigger_autocomplete_plus/example/.gitignore
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Web related
|
||||||
|
lib/generated_plugin_registrant.dart
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
16
packages/multi_trigger_autocomplete_plus/example/README.md
Normal file
16
packages/multi_trigger_autocomplete_plus/example/README.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# example
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
@ -0,0 +1,29 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at
|
||||||
|
# https://dart-lang.github.io/linter/lints/index.html.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
133
packages/multi_trigger_autocomplete_plus/example/lib/main.dart
Normal file
133
packages/multi_trigger_autocomplete_plus/example/lib/main.dart
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import 'package:example/src/chat_message_list.dart';
|
||||||
|
import 'package:example/src/chat_message_text_field.dart';
|
||||||
|
import 'package:example/src/data.dart';
|
||||||
|
import 'package:example/src/options/options.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete.dart';
|
||||||
|
|
||||||
|
void main() => runApp(const MyApp());
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
// This widget is the root of your application.
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Portal(
|
||||||
|
child: MaterialApp(
|
||||||
|
title: 'Flutter Demo',
|
||||||
|
theme: ThemeData(
|
||||||
|
platform: TargetPlatform.iOS,
|
||||||
|
textTheme: GoogleFonts.robotoMonoTextTheme(
|
||||||
|
Theme.of(context).textTheme,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
home: const MyHomePage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyHomePage extends StatefulWidget {
|
||||||
|
const MyHomePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyHomePage> createState() => _MyHomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
|
final messages = [...sampleGroupConversation];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
appBar: AppBar(
|
||||||
|
toolbarHeight: 60,
|
||||||
|
backgroundColor: const Color(0xFF5B61B9),
|
||||||
|
title: const Text(
|
||||||
|
'Multi Trigger Autocomplete',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(24),
|
||||||
|
bottomRight: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(child: ChatMessageList(messages: messages)),
|
||||||
|
MultiTriggerAutocomplete(
|
||||||
|
optionsAlignment: OptionsAlignment.topStart,
|
||||||
|
autocompleteTriggers: [
|
||||||
|
AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
||||||
|
return MentionAutocompleteOptions(
|
||||||
|
query: autocompleteQuery.query,
|
||||||
|
onMentionUserTap: (user) {
|
||||||
|
final autocomplete = MultiTriggerAutocomplete.of(context);
|
||||||
|
return autocomplete.acceptAutocompleteOption(user.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AutocompleteTrigger(
|
||||||
|
trigger: '#',
|
||||||
|
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
||||||
|
return HashtagAutocompleteOptions(
|
||||||
|
query: autocompleteQuery.query,
|
||||||
|
onHashtagTap: (hashtag) {
|
||||||
|
final autocomplete = MultiTriggerAutocomplete.of(context);
|
||||||
|
return autocomplete
|
||||||
|
.acceptAutocompleteOption(hashtag.name);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AutocompleteTrigger(
|
||||||
|
trigger: ':',
|
||||||
|
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
||||||
|
return EmojiAutocompleteOptions(
|
||||||
|
query: autocompleteQuery.query,
|
||||||
|
onEmojiTap: (emoji) {
|
||||||
|
final autocomplete = MultiTriggerAutocomplete.of(context);
|
||||||
|
return autocomplete.acceptAutocompleteOption(
|
||||||
|
emoji.char,
|
||||||
|
// Passing false as we don't want the trigger [:] to
|
||||||
|
// get prefixed to the option in case of emoji.
|
||||||
|
keepTrigger: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
fieldViewBuilder: (context, controller, focusNode) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: ChatMessageTextField(
|
||||||
|
focusNode: focusNode,
|
||||||
|
controller: controller,
|
||||||
|
onSend: (message) {
|
||||||
|
controller.clear();
|
||||||
|
setState(() {
|
||||||
|
messages.add(message);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
import 'package:example/src/models.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_parsed_text/flutter_parsed_text.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
class ChatMessageList extends StatelessWidget {
|
||||||
|
const ChatMessageList({
|
||||||
|
Key? key,
|
||||||
|
required this.messages,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final List<ChatMessage> messages;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final messages = this.messages.reversed.toList();
|
||||||
|
return ListView.separated(
|
||||||
|
reverse: true,
|
||||||
|
itemCount: messages.length,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
separatorBuilder: (context, index) => const SizedBox(height: 8),
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final message = messages[index];
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFFE9EAF4),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topRight: Radius.circular(32.0),
|
||||||
|
topLeft: Radius.circular(32.0),
|
||||||
|
bottomLeft: Radius.circular(32.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ParsedText(
|
||||||
|
text: message.text,
|
||||||
|
style: GoogleFonts.robotoMono(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: const Color(0xFF79708F),
|
||||||
|
),
|
||||||
|
parse: <MatchText>[
|
||||||
|
MatchText(
|
||||||
|
pattern: r"@[A-Za-z0-9_.-]*",
|
||||||
|
style: const TextStyle(color: Colors.green),
|
||||||
|
),
|
||||||
|
MatchText(
|
||||||
|
pattern: r"\B#+([\w]+)\b",
|
||||||
|
style: const TextStyle(color: Colors.blue),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundImage: NetworkImage(message.sender.avatar),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
import 'package:example/src/models.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:math' show Random;
|
||||||
|
|
||||||
|
import 'package:example/src/data.dart';
|
||||||
|
|
||||||
|
class ChatMessageTextField extends StatelessWidget {
|
||||||
|
const ChatMessageTextField({
|
||||||
|
Key? key,
|
||||||
|
required this.focusNode,
|
||||||
|
required this.controller,
|
||||||
|
required this.onSend,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final FocusNode focusNode;
|
||||||
|
final TextEditingController controller;
|
||||||
|
final ValueSetter<ChatMessage> onSend;
|
||||||
|
|
||||||
|
final _sender = const [sahil, avni, gaurav];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 4, 4, 4),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFFF7F7F8),
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(40.0)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
focusNode: focusNode,
|
||||||
|
controller: controller,
|
||||||
|
decoration: const InputDecoration.collapsed(
|
||||||
|
hintText: "Type your message...",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
final message = ChatMessage(
|
||||||
|
text: controller.text,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
sender: _sender[Random().nextInt(_sender.length)],
|
||||||
|
);
|
||||||
|
onSend(message);
|
||||||
|
},
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
icon: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Color(0xFF5B61B9),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.send_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,243 @@
|
|||||||
|
import 'package:example/src/models.dart';
|
||||||
|
|
||||||
|
const sahil = User(
|
||||||
|
id: 'xsahil03x',
|
||||||
|
name: 'Sahil Kumar',
|
||||||
|
avatar: 'https://bit.ly/3yEVRrD',
|
||||||
|
);
|
||||||
|
|
||||||
|
const avni = User(
|
||||||
|
id: 'avu.saxena',
|
||||||
|
name: 'Avni Saxena',
|
||||||
|
avatar: 'https://bit.ly/3PbPBii',
|
||||||
|
);
|
||||||
|
|
||||||
|
const trapti = User(
|
||||||
|
id: 'trapti2711',
|
||||||
|
name: 'Trapti Gupta',
|
||||||
|
avatar: 'https://bit.ly/3aDHtba',
|
||||||
|
);
|
||||||
|
|
||||||
|
const gaurav = User(
|
||||||
|
id: 'itsmegb98',
|
||||||
|
name: 'Gaurav Bhadouriya',
|
||||||
|
avatar: 'https://bit.ly/3PmNdES',
|
||||||
|
);
|
||||||
|
|
||||||
|
const amit = User(
|
||||||
|
id: 'amitk_15',
|
||||||
|
name: 'Amit Kumar',
|
||||||
|
avatar: 'https://bit.ly/3P9GPB8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const ayush = User(
|
||||||
|
id: 'ayushpgupta',
|
||||||
|
name: 'Ayush Gupta',
|
||||||
|
avatar: 'https://bit.ly/3Rw61Dv',
|
||||||
|
);
|
||||||
|
|
||||||
|
const shubham = User(
|
||||||
|
id: 'someshubham',
|
||||||
|
name: 'Shubham Jain',
|
||||||
|
avatar: 'https://bit.ly/3Rs3uud',
|
||||||
|
);
|
||||||
|
|
||||||
|
const kUsers = <User>[
|
||||||
|
sahil,
|
||||||
|
avni,
|
||||||
|
gaurav,
|
||||||
|
trapti,
|
||||||
|
amit,
|
||||||
|
ayush,
|
||||||
|
shubham,
|
||||||
|
];
|
||||||
|
|
||||||
|
const kHashtags = <Hashtag>[
|
||||||
|
Hashtag(
|
||||||
|
name: 'dart',
|
||||||
|
weight: 1,
|
||||||
|
description:
|
||||||
|
'Dart is a language for building fast, scalable and maintainable applications.',
|
||||||
|
image: 'https://dwglogo.com/wp-content/uploads/2018/03/Dart_logo.png',
|
||||||
|
),
|
||||||
|
Hashtag(
|
||||||
|
name: 'flutter',
|
||||||
|
weight: 2,
|
||||||
|
description:
|
||||||
|
'Flutter is a framework for building native Android and iOS applications for Google\'s mobile platforms.',
|
||||||
|
image:
|
||||||
|
'https://storage.googleapis.com/cms-storage-bucket/0dbfcc7a59cd1cf16282.png',
|
||||||
|
),
|
||||||
|
Hashtag(
|
||||||
|
name: 'firebase',
|
||||||
|
weight: 3,
|
||||||
|
description:
|
||||||
|
'Firebase is a cloud platform for building mobile and web apps.',
|
||||||
|
image:
|
||||||
|
'https://firebase.google.com/static/downloads/brand-guidelines/PNG/logo-logomark.png',
|
||||||
|
),
|
||||||
|
Hashtag(
|
||||||
|
name: 'google',
|
||||||
|
weight: 4,
|
||||||
|
description:
|
||||||
|
'Google is a company that builds products and services for the world\'s users.',
|
||||||
|
image: 'https://dwglogo.com/wp-content/uploads/2016/06/G-icon-1068x735.png',
|
||||||
|
),
|
||||||
|
Hashtag(
|
||||||
|
name: 'apple',
|
||||||
|
weight: 5,
|
||||||
|
description:
|
||||||
|
'Apple is a company that builds products and services for the world\'s users.',
|
||||||
|
image:
|
||||||
|
'https://dwglogo.com/wp-content/uploads/2016/02/Apple_logo-1068x601.png',
|
||||||
|
),
|
||||||
|
Hashtag(
|
||||||
|
name: 'microsoft',
|
||||||
|
weight: 6,
|
||||||
|
description:
|
||||||
|
'Microsoft is a company that builds products and services for the world\'s users.',
|
||||||
|
image:
|
||||||
|
'https://upload.wikimedia.org/wikipedia/commons/thumb/4/44/Microsoft_logo.svg/2048px-Microsoft_logo.svg.png',
|
||||||
|
),
|
||||||
|
Hashtag(
|
||||||
|
name: 'facebook',
|
||||||
|
weight: 7,
|
||||||
|
description:
|
||||||
|
'Facebook is a company that builds products and services for the world\'s users.',
|
||||||
|
image: 'https://www.facebook.com/images/fb_icon_325x325.png',
|
||||||
|
),
|
||||||
|
Hashtag(
|
||||||
|
name: 'twitter',
|
||||||
|
weight: 8,
|
||||||
|
description:
|
||||||
|
'Twitter is a company that builds products and services for the world\'s users.',
|
||||||
|
image:
|
||||||
|
'https://dwglogo.com/wp-content/uploads/2019/02/Twitter_logo-1024x705.png',
|
||||||
|
),
|
||||||
|
Hashtag(
|
||||||
|
name: 'instagram',
|
||||||
|
weight: 9,
|
||||||
|
description:
|
||||||
|
'Instagram is a company that builds products and services for the world\'s users.',
|
||||||
|
image:
|
||||||
|
'https://w7.pngwing.com/pngs/648/943/png-transparent-instagram-logo-logo-instagram-computer-icons-camera-instagram-logo-text-trademark-magenta.png',
|
||||||
|
),
|
||||||
|
Hashtag(
|
||||||
|
name: 'snapchat',
|
||||||
|
weight: 10,
|
||||||
|
description:
|
||||||
|
'Snapchat is a company that builds products and services for the world\'s users.',
|
||||||
|
image:
|
||||||
|
'https://dwglogo.com/wp-content/uploads/2016/06/dotted_logo_of_snapchat-1068x601.png',
|
||||||
|
),
|
||||||
|
Hashtag(
|
||||||
|
name: 'youtube',
|
||||||
|
weight: 11,
|
||||||
|
description:
|
||||||
|
'YouTube is a company that builds products and services for the world\'s users.',
|
||||||
|
image:
|
||||||
|
'https://dwglogo.com/wp-content/uploads/2020/05/1200px-YouTube_logo-1024x729.png',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const kEmojis = <Emoji>[
|
||||||
|
Emoji(
|
||||||
|
char: '😀',
|
||||||
|
shortName: ':grinning:',
|
||||||
|
unicode: '1f600',
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
char: '😂',
|
||||||
|
shortName: ':joy:',
|
||||||
|
unicode: '1f602',
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
char: '😃',
|
||||||
|
shortName: ':smiley:',
|
||||||
|
unicode: '1f603',
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
char: '😄',
|
||||||
|
shortName: ':smile:',
|
||||||
|
unicode: '1f604',
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
char: '😅',
|
||||||
|
shortName: ':sweat_smile:',
|
||||||
|
unicode: '1f605',
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
char: '😆',
|
||||||
|
shortName: ':laughing:',
|
||||||
|
unicode: '1f606',
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
char: '😇',
|
||||||
|
shortName: ':wink:',
|
||||||
|
unicode: '1f609',
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
char: '😈',
|
||||||
|
shortName: ':smirk:',
|
||||||
|
unicode: '1f60f',
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
char: '😉',
|
||||||
|
shortName: ':wink2:',
|
||||||
|
unicode: '1f609',
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
char: '😊',
|
||||||
|
shortName: ':blush:',
|
||||||
|
unicode: '1f60a',
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
char: '😋',
|
||||||
|
shortName: ':yum:',
|
||||||
|
unicode: '1f60b',
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
char: '😌',
|
||||||
|
shortName: ':relieved:',
|
||||||
|
unicode: '1f60c',
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
char: '😍',
|
||||||
|
shortName: ':heart_eyes:',
|
||||||
|
unicode: '1f60d',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final sampleGroupConversation = [
|
||||||
|
ChatMessage(
|
||||||
|
text: 'Hey there! What\'s up?',
|
||||||
|
createdAt: DateTime.now().subtract(const Duration(seconds: 1)),
|
||||||
|
sender: sahil,
|
||||||
|
),
|
||||||
|
ChatMessage(
|
||||||
|
text: 'Nothing. Just chilling and watching YouTube. What about you?',
|
||||||
|
createdAt: DateTime.now().subtract(const Duration(seconds: 2)),
|
||||||
|
sender: avni,
|
||||||
|
),
|
||||||
|
ChatMessage(
|
||||||
|
text: 'Yeah I know. I\'m in the same position 😂',
|
||||||
|
createdAt: DateTime.now().subtract(const Duration(seconds: 3)),
|
||||||
|
sender: sahil,
|
||||||
|
),
|
||||||
|
ChatMessage(
|
||||||
|
text: 'I\'m just trying to get some sleep',
|
||||||
|
createdAt: DateTime.now().subtract(const Duration(seconds: 4)),
|
||||||
|
sender: gaurav,
|
||||||
|
),
|
||||||
|
ChatMessage(
|
||||||
|
text:
|
||||||
|
'Same here! Been watching YouTube for the past 5 hours despite of having so much to do! 😅',
|
||||||
|
createdAt: DateTime.now().subtract(const Duration(seconds: 5)),
|
||||||
|
sender: trapti,
|
||||||
|
),
|
||||||
|
ChatMessage(
|
||||||
|
text: 'It\'s hard to be productive',
|
||||||
|
createdAt: DateTime.now().subtract(const Duration(seconds: 6)),
|
||||||
|
sender: avni,
|
||||||
|
),
|
||||||
|
];
|
@ -0,0 +1,49 @@
|
|||||||
|
class Emoji {
|
||||||
|
const Emoji({
|
||||||
|
required this.char,
|
||||||
|
required this.shortName,
|
||||||
|
required this.unicode,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String char;
|
||||||
|
final String shortName;
|
||||||
|
final String unicode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Hashtag {
|
||||||
|
const Hashtag({
|
||||||
|
required this.name,
|
||||||
|
required this.weight,
|
||||||
|
required this.description,
|
||||||
|
required this.image,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final int weight;
|
||||||
|
final String description;
|
||||||
|
final String image;
|
||||||
|
}
|
||||||
|
|
||||||
|
class User {
|
||||||
|
const User({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.avatar,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMessage {
|
||||||
|
const ChatMessage({
|
||||||
|
required this.text,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.sender,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final User sender;
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
import 'package:example/src/data.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:example/src/models.dart';
|
||||||
|
|
||||||
|
class EmojiAutocompleteOptions extends StatelessWidget {
|
||||||
|
const EmojiAutocompleteOptions({
|
||||||
|
Key? key,
|
||||||
|
required this.query,
|
||||||
|
required this.onEmojiTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String query;
|
||||||
|
final ValueSetter<Emoji> onEmojiTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final emojis = kEmojis.where((it) {
|
||||||
|
final normalizedOption = it.shortName.toLowerCase();
|
||||||
|
final normalizedQuery = query.toLowerCase();
|
||||||
|
return normalizedOption.contains(normalizedQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (emojis.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.all(8),
|
||||||
|
elevation: 2,
|
||||||
|
// color: _streamChatTheme.colorTheme.barsBg,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
color: const Color(0xFFF7F7F8),
|
||||||
|
child: ListTile(
|
||||||
|
dense: true,
|
||||||
|
horizontalTitleGap: 0,
|
||||||
|
title: Text("Emoji's matching '$query'"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 0),
|
||||||
|
LimitedBox(
|
||||||
|
maxHeight: MediaQuery.of(context).size.height * 0.3,
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: emojis.length,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 0),
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
final emoji = emojis.elementAt(i);
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: Text(
|
||||||
|
emoji.char,
|
||||||
|
style: const TextStyle(fontSize: 24),
|
||||||
|
),
|
||||||
|
title: Text(emoji.shortName),
|
||||||
|
onTap: () => onEmojiTap(emoji),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
import 'package:example/src/data.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:example/src/models.dart';
|
||||||
|
|
||||||
|
class HashtagAutocompleteOptions extends StatelessWidget {
|
||||||
|
const HashtagAutocompleteOptions({
|
||||||
|
Key? key,
|
||||||
|
required this.query,
|
||||||
|
required this.onHashtagTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String query;
|
||||||
|
final ValueSetter<Hashtag> onHashtagTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hashtags = kHashtags.where((it) {
|
||||||
|
final normalizedOption = it.name.toLowerCase();
|
||||||
|
final normalizedQuery = query.toLowerCase();
|
||||||
|
return normalizedOption.contains(normalizedQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hashtags.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.all(8),
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
color: const Color(0xFFE9EAF4),
|
||||||
|
child: ListTile(
|
||||||
|
dense: true,
|
||||||
|
horizontalTitleGap: 0,
|
||||||
|
title: Text("Hashtags matching '$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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
import 'package:example/src/data.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:example/src/models.dart';
|
||||||
|
|
||||||
|
class MentionAutocompleteOptions extends StatelessWidget {
|
||||||
|
const MentionAutocompleteOptions({
|
||||||
|
Key? key,
|
||||||
|
required this.query,
|
||||||
|
required this.onMentionUserTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String query;
|
||||||
|
final ValueSetter<User> onMentionUserTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final users = kUsers.where((it) {
|
||||||
|
final normalizedId = it.id.toLowerCase();
|
||||||
|
final normalizedName = it.name.toLowerCase();
|
||||||
|
final normalizedQuery = query.toLowerCase();
|
||||||
|
return normalizedId.contains(normalizedQuery) ||
|
||||||
|
normalizedName.contains(normalizedQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (users.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.all(8),
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
color: const Color(0xFFF7F7F8),
|
||||||
|
child: ListTile(
|
||||||
|
dense: true,
|
||||||
|
horizontalTitleGap: 0,
|
||||||
|
title: Text("Users matching '$query'"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
LimitedBox(
|
||||||
|
maxHeight: MediaQuery.of(context).size.height * 0.3,
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: users.length,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 0),
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
final user = users.elementAt(i);
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundImage: NetworkImage(user.avatar),
|
||||||
|
),
|
||||||
|
title: Text(user.name),
|
||||||
|
subtitle: Text('@${user.id}'),
|
||||||
|
onTap: () => onMentionUserTap(user),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
export 'emoji_autocomplete_options.dart';
|
||||||
|
export 'hashtag_autocomplete_options.dart';
|
||||||
|
export 'mention_autocomplete_options.dart';
|
364
packages/multi_trigger_autocomplete_plus/example/pubspec.lock
Normal file
364
packages/multi_trigger_autocomplete_plus/example/pubspec.lock
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.12.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.6"
|
||||||
|
cupertino_icons:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cupertino_icons
|
||||||
|
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.8"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.2"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_lints
|
||||||
|
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
|
flutter_parsed_text:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_parsed_text
|
||||||
|
sha256: "529cf5793b7acdf16ee0f97b158d0d4ba0bf06e7121ef180abe1a5b59e32c1e2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
flutter_portal:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_portal
|
||||||
|
sha256: "4601b3dc24f385b3761721bd852a3f6c09cddd4e943dd184ed58ee1f43006257"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.4"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
google_fonts:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_fonts
|
||||||
|
sha256: e20ff62b158b96f392bfc8afe29dee1503c94fbea2cbe8186fd59b756b8ae982
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.0"
|
||||||
|
http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.0.8"
|
||||||
|
leak_tracker_flutter_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_flutter_testing
|
||||||
|
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.9"
|
||||||
|
leak_tracker_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_testing
|
||||||
|
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
|
lints:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.17"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.11.1"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.16.0"
|
||||||
|
multi_trigger_autocomplete_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: ".."
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "1.0.2"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
path_provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.15"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.6"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.1"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.4"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "14.3.1"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.7.0-0 <4.0.0"
|
||||||
|
flutter: ">=3.24.0"
|
@ -0,0 +1,91 @@
|
|||||||
|
name: example
|
||||||
|
description: A new Flutter project.
|
||||||
|
|
||||||
|
# The following line prevents the package from being accidentally published to
|
||||||
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
|
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
# The following defines the version and build number for your application.
|
||||||
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
|
# followed by an optional build number separated by a +.
|
||||||
|
# Both the version and the builder number may be overridden in flutter
|
||||||
|
# build by specifying --build-name and --build-number, respectively.
|
||||||
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||||
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||||
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
|
# Read more about iOS versioning at
|
||||||
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.18.0 <4.0.0"
|
||||||
|
|
||||||
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
|
# dependencies can be manually updated by changing the version numbers below to
|
||||||
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
|
# versions available, run `flutter pub outdated`.
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# The following adds the Cupertino Icons font to your application.
|
||||||
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
cupertino_icons: ^1.0.5
|
||||||
|
flutter_parsed_text: ^2.2.1
|
||||||
|
google_fonts: ^5.0.0
|
||||||
|
multi_trigger_autocomplete_plus:
|
||||||
|
path: ../
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
|
# package. See that file for information about deactivating specific lint
|
||||||
|
# rules and activating additional ones.
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
|
flutter:
|
||||||
|
# The following line ensures that the Material Icons font is
|
||||||
|
# included with your application, so that you can use the icons in
|
||||||
|
# the material Icons class.
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
|
# To add assets to your application, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|
||||||
|
# For details regarding adding assets from package dependencies, see
|
||||||
|
# https://flutter.dev/assets-and-images/#from-packages
|
||||||
|
|
||||||
|
# To add custom fonts to your application, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts from package dependencies,
|
||||||
|
# see https://flutter.dev/custom-fonts/#from-packages
|
@ -0,0 +1,4 @@
|
|||||||
|
# melos_managed_dependency_overrides: multi_trigger_autocomplete_plus
|
||||||
|
dependency_overrides:
|
||||||
|
multi_trigger_autocomplete_plus:
|
||||||
|
path: ..
|
@ -0,0 +1,8 @@
|
|||||||
|
library multi_trigger_autocomplete_plus;
|
||||||
|
|
||||||
|
export 'package:flutter_portal/flutter_portal.dart' show Portal;
|
||||||
|
|
||||||
|
export 'src/multi_trigger_autocomplete.dart';
|
||||||
|
export 'src/autocomplete_trigger.dart';
|
||||||
|
export 'src/autocomplete_no_trigger.dart';
|
||||||
|
export 'src/autocomplete_query.dart';
|
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete.dart';
|
||||||
|
|
||||||
|
class AutocompleteNoTrigger extends AutocompleteTrigger {
|
||||||
|
/// Creates a [AutocompleteNoTrigger] which can be used to trigger
|
||||||
|
/// suggestions without a trigger.
|
||||||
|
const AutocompleteNoTrigger({
|
||||||
|
required super.optionsViewBuilder,
|
||||||
|
super.minimumRequiredCharacters = 0,
|
||||||
|
}) : super(
|
||||||
|
trigger: '',
|
||||||
|
triggerEnd: '',
|
||||||
|
triggerOnlyAtStart: false,
|
||||||
|
triggerOnlyAfterSpace: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is AutocompleteNoTrigger &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
optionsViewBuilder == other.optionsViewBuilder &&
|
||||||
|
minimumRequiredCharacters == other.minimumRequiredCharacters;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
optionsViewBuilder.hashCode ^ minimumRequiredCharacters.hashCode;
|
||||||
|
|
||||||
|
/// Returns the [AutocompleteQuery] for the current text field value.
|
||||||
|
@override
|
||||||
|
AutocompleteQuery? invokingTrigger(TextEditingValue textEditingValue) {
|
||||||
|
final text = textEditingValue.text;
|
||||||
|
final selection = textEditingValue.selection;
|
||||||
|
|
||||||
|
if (text.length < minimumRequiredCharacters) return null;
|
||||||
|
|
||||||
|
return AutocompleteQuery(
|
||||||
|
query: text,
|
||||||
|
selection: selection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AutocompleteQuery {
|
||||||
|
/// Creates a [AutocompleteQuery] with the specified [query] and
|
||||||
|
/// [selection].
|
||||||
|
const AutocompleteQuery({
|
||||||
|
required this.query,
|
||||||
|
required this.selection,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The query string.
|
||||||
|
final String query;
|
||||||
|
|
||||||
|
/// The selection in the text field.
|
||||||
|
final TextSelection selection;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is AutocompleteQuery &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
query == other.query &&
|
||||||
|
selection == other.selection;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => query.hashCode ^ selection.hashCode;
|
||||||
|
}
|
@ -0,0 +1,125 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:multi_trigger_autocomplete_plus/src/autocomplete_query.dart';
|
||||||
|
|
||||||
|
/// The type of the [AutocompleteTrigger] callback which returns a [Widget] that
|
||||||
|
/// displays the specified [options].
|
||||||
|
typedef AutocompleteTriggerOptionsViewBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
AutocompleteQuery autocompleteQuery,
|
||||||
|
TextEditingController textEditingController,
|
||||||
|
);
|
||||||
|
|
||||||
|
class AutocompleteTrigger {
|
||||||
|
/// Creates a [AutocompleteTrigger] which can be used to trigger
|
||||||
|
/// autocomplete suggestions.
|
||||||
|
const AutocompleteTrigger({
|
||||||
|
required this.trigger,
|
||||||
|
this.triggerEnd = ' ',
|
||||||
|
required this.optionsViewBuilder,
|
||||||
|
this.triggerOnlyAtStart = false,
|
||||||
|
this.triggerOnlyAfterSpace = true,
|
||||||
|
this.minimumRequiredCharacters = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The trigger character.
|
||||||
|
///
|
||||||
|
/// eg. '@', '#', ':'
|
||||||
|
final String trigger;
|
||||||
|
|
||||||
|
/// The trigger end character.
|
||||||
|
/// This is used to determine when the trigger ends.
|
||||||
|
/// By default, it's a space.
|
||||||
|
final String triggerEnd;
|
||||||
|
|
||||||
|
/// Whether the [trigger] should only be recognised at the start of the input.
|
||||||
|
final bool triggerOnlyAtStart;
|
||||||
|
|
||||||
|
/// Whether the [trigger] should only be recognised after a space.
|
||||||
|
final bool triggerOnlyAfterSpace;
|
||||||
|
|
||||||
|
/// The minimum required characters for the [trigger] to start recognising
|
||||||
|
/// a autocomplete options.
|
||||||
|
final int minimumRequiredCharacters;
|
||||||
|
|
||||||
|
/// Builds the selectable options widgets from a list of options objects.
|
||||||
|
///
|
||||||
|
/// The options are displayed floating above or below the field using a
|
||||||
|
/// [PortalTarget] inside of an [Portal].
|
||||||
|
final AutocompleteTriggerOptionsViewBuilder optionsViewBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is AutocompleteTrigger &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
trigger == other.trigger &&
|
||||||
|
triggerEnd == other.triggerEnd &&
|
||||||
|
triggerOnlyAtStart == other.triggerOnlyAtStart &&
|
||||||
|
triggerOnlyAfterSpace == other.triggerOnlyAfterSpace &&
|
||||||
|
minimumRequiredCharacters == other.minimumRequiredCharacters;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
trigger.hashCode ^
|
||||||
|
triggerOnlyAtStart.hashCode ^
|
||||||
|
triggerOnlyAfterSpace.hashCode ^
|
||||||
|
minimumRequiredCharacters.hashCode;
|
||||||
|
|
||||||
|
/// Checks if the user is invoking the recognising [trigger] and returns
|
||||||
|
/// the autocomplete query if so.
|
||||||
|
AutocompleteQuery? invokingTrigger(TextEditingValue textEditingValue) {
|
||||||
|
final text = textEditingValue.text;
|
||||||
|
final selection = textEditingValue.selection;
|
||||||
|
|
||||||
|
// If the selection is invalid, then it's not a trigger.
|
||||||
|
if (!selection.isValid) return null;
|
||||||
|
final cursorPosition = selection.baseOffset;
|
||||||
|
|
||||||
|
// Find the first [trigger] location before the input cursor.
|
||||||
|
final firstTriggerIndexBeforeCursor =
|
||||||
|
text.substring(0, cursorPosition).lastIndexOf(trigger);
|
||||||
|
|
||||||
|
// If the [trigger] is not found before the cursor, then it's not a trigger.
|
||||||
|
if (firstTriggerIndexBeforeCursor == -1) return null;
|
||||||
|
|
||||||
|
// If the [trigger] is found before the cursor, but the [trigger] is only
|
||||||
|
// recognised at the start of the input, then it's not a trigger.
|
||||||
|
if (triggerOnlyAtStart && firstTriggerIndexBeforeCursor != 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show typing suggestions after a space, new line or at the start of the input.
|
||||||
|
// valid examples: "@user", "Hello @user", "Hello\n@user"
|
||||||
|
// invalid examples: "Hello@user"
|
||||||
|
final textBeforeTrigger = text.substring(0, firstTriggerIndexBeforeCursor);
|
||||||
|
if (triggerOnlyAfterSpace &&
|
||||||
|
textBeforeTrigger.isNotEmpty &&
|
||||||
|
!(textBeforeTrigger.endsWith(triggerEnd) ||
|
||||||
|
textBeforeTrigger.endsWith('\n'))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The suggestion range. Protect against invalid ranges.
|
||||||
|
final suggestionStart = firstTriggerIndexBeforeCursor + trigger.length;
|
||||||
|
final suggestionEnd = cursorPosition;
|
||||||
|
if (suggestionStart > suggestionEnd) return null;
|
||||||
|
|
||||||
|
// Fetch the suggestion text. The suggestions can't have spaces/triggerEnd.
|
||||||
|
// valid example: "@luke_skywa..."
|
||||||
|
// invalid example: "@luke skywa..."
|
||||||
|
final suggestionText = text.substring(suggestionStart, suggestionEnd);
|
||||||
|
if (suggestionText.contains(triggerEnd)) return null;
|
||||||
|
|
||||||
|
// A minimum number of characters can be provided to only show
|
||||||
|
// suggestions after the customer has input enough characters.
|
||||||
|
if (suggestionText.length < minimumRequiredCharacters) return null;
|
||||||
|
|
||||||
|
return AutocompleteQuery(
|
||||||
|
query: suggestionText,
|
||||||
|
selection: TextSelection(
|
||||||
|
baseOffset: suggestionStart,
|
||||||
|
extentOffset: suggestionEnd,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,483 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_portal/flutter_portal.dart';
|
||||||
|
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete.dart';
|
||||||
|
|
||||||
|
/// The type of the Autocomplete callback which returns the widget that
|
||||||
|
/// contains the input [TextField] or [TextFormField].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [RawAutocomplete.fieldViewBuilder], which is of this type.
|
||||||
|
typedef MultiTriggerAutocompleteFieldViewBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
TextEditingController textEditingController,
|
||||||
|
FocusNode focusNode,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Positions the [AutocompleteTrigger] options around the [TextField] or
|
||||||
|
/// [TextFormField] that triggered the autocomplete.
|
||||||
|
enum OptionsAlignment {
|
||||||
|
/// Positions the options to the top of the field.
|
||||||
|
top,
|
||||||
|
|
||||||
|
/// Positions the options to the bottom of the field.
|
||||||
|
bottom,
|
||||||
|
|
||||||
|
/// Positions the options to the top left of the field.
|
||||||
|
topStart,
|
||||||
|
|
||||||
|
/// Positions the options to the top right of the field.
|
||||||
|
topEnd,
|
||||||
|
|
||||||
|
/// Positions the options to the bottom left of the field.
|
||||||
|
bottomStart,
|
||||||
|
|
||||||
|
/// Positions the options to the bottom right of the field.
|
||||||
|
bottomEnd;
|
||||||
|
|
||||||
|
Anchor _toAnchor({double? widthFactor = 1.0}) {
|
||||||
|
switch (this) {
|
||||||
|
case OptionsAlignment.top:
|
||||||
|
return Aligned(
|
||||||
|
widthFactor: widthFactor,
|
||||||
|
follower: Alignment.bottomCenter,
|
||||||
|
target: Alignment.topCenter,
|
||||||
|
);
|
||||||
|
case OptionsAlignment.bottom:
|
||||||
|
return Aligned(
|
||||||
|
widthFactor: widthFactor,
|
||||||
|
follower: Alignment.topCenter,
|
||||||
|
target: Alignment.bottomCenter,
|
||||||
|
);
|
||||||
|
case OptionsAlignment.topStart:
|
||||||
|
return Aligned(
|
||||||
|
widthFactor: widthFactor,
|
||||||
|
follower: Alignment.bottomLeft,
|
||||||
|
target: Alignment.topLeft,
|
||||||
|
);
|
||||||
|
case OptionsAlignment.topEnd:
|
||||||
|
return Aligned(
|
||||||
|
widthFactor: widthFactor,
|
||||||
|
follower: Alignment.bottomRight,
|
||||||
|
target: Alignment.topRight,
|
||||||
|
);
|
||||||
|
case OptionsAlignment.bottomStart:
|
||||||
|
return Aligned(
|
||||||
|
widthFactor: widthFactor,
|
||||||
|
follower: Alignment.topLeft,
|
||||||
|
target: Alignment.bottomLeft,
|
||||||
|
);
|
||||||
|
case OptionsAlignment.bottomEnd:
|
||||||
|
return Aligned(
|
||||||
|
widthFactor: widthFactor,
|
||||||
|
follower: Alignment.topRight,
|
||||||
|
target: Alignment.bottomRight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that provides a text field with autocomplete functionality.
|
||||||
|
class MultiTriggerAutocomplete extends StatefulWidget {
|
||||||
|
/// Create an instance of StreamAutocomplete.
|
||||||
|
///
|
||||||
|
/// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must
|
||||||
|
/// not be null.
|
||||||
|
const MultiTriggerAutocomplete({
|
||||||
|
super.key,
|
||||||
|
required this.autocompleteTriggers,
|
||||||
|
this.fieldViewBuilder = _defaultFieldViewBuilder,
|
||||||
|
this.focusNode,
|
||||||
|
this.textEditingController,
|
||||||
|
this.initialValue,
|
||||||
|
this.optionsAlignment = OptionsAlignment.bottom,
|
||||||
|
this.optionsWidthFactor = 1.0,
|
||||||
|
this.debounceDuration = const Duration(milliseconds: 300),
|
||||||
|
}) : assert((focusNode == null) == (textEditingController == null)),
|
||||||
|
assert(
|
||||||
|
!(textEditingController != null && initialValue != null),
|
||||||
|
'textEditingController and initialValue cannot be simultaneously defined.',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The triggers that trigger autocomplete.
|
||||||
|
final Iterable<AutocompleteTrigger> autocompleteTriggers;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.RawAutocomplete.fieldViewBuilder}
|
||||||
|
/// Builds the field whose input is used to get the options.
|
||||||
|
///
|
||||||
|
/// Pass the provided [TextEditingController] to the field built here so that
|
||||||
|
/// RawAutocomplete can listen for changes.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final MultiTriggerAutocompleteFieldViewBuilder fieldViewBuilder;
|
||||||
|
|
||||||
|
/// The [FocusNode] that is used for the text field.
|
||||||
|
///
|
||||||
|
/// {@template flutter.widgets.RawAutocomplete.split}
|
||||||
|
/// The main purpose of this parameter is to allow the use of a separate text
|
||||||
|
/// field located in another part of the widget tree instead of the text
|
||||||
|
/// field built by [fieldViewBuilder]. For example, it may be desirable to
|
||||||
|
/// place the text field in the AppBar and the options below in the main body.
|
||||||
|
///
|
||||||
|
/// When following this pattern, [fieldViewBuilder] can return
|
||||||
|
/// `SizedBox.shrink()` so that nothing is drawn where the text field would
|
||||||
|
/// normally be. A separate text field can be created elsewhere, and a
|
||||||
|
/// FocusNode and TextEditingController can be passed both to that text field
|
||||||
|
/// and to RawAutocomplete.
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This examples shows how to create an autocomplete widget with the text
|
||||||
|
/// field in the AppBar and the results in the main body of the app.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// If this parameter is not null, then [textEditingController] must also be
|
||||||
|
/// not null.
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
|
/// The [TextEditingController] that is used for the text field.
|
||||||
|
///
|
||||||
|
/// If this parameter is not null, then [focusNode] must also be not null.
|
||||||
|
final TextEditingController? textEditingController;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.RawAutocomplete.initialValue}
|
||||||
|
/// The initial value to use for the text field.
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// Setting the initial value does not notify [textEditingController]'s
|
||||||
|
/// listeners, and thus will not cause the options UI to appear.
|
||||||
|
///
|
||||||
|
/// This parameter is ignored if [textEditingController] is defined.
|
||||||
|
final TextEditingValue? initialValue;
|
||||||
|
|
||||||
|
/// The alignment of the options.
|
||||||
|
///
|
||||||
|
/// The default value is [MultiTriggerAutocompleteAlignment.below].
|
||||||
|
final OptionsAlignment optionsAlignment;
|
||||||
|
|
||||||
|
/// The width to make the options as a multiple of the width of the
|
||||||
|
/// field.
|
||||||
|
///
|
||||||
|
/// The default value is 1.0, which makes the options the same width
|
||||||
|
/// as the field.
|
||||||
|
final double? optionsWidthFactor;
|
||||||
|
|
||||||
|
/// The duration of the debounce period for the [TextEditingController].
|
||||||
|
///
|
||||||
|
/// The default value is [300ms].
|
||||||
|
final Duration debounceDuration;
|
||||||
|
|
||||||
|
static Widget _defaultFieldViewBuilder(
|
||||||
|
BuildContext context,
|
||||||
|
TextEditingController textEditingController,
|
||||||
|
FocusNode focusNode,
|
||||||
|
) {
|
||||||
|
return _MultiTriggerAutocompleteField(
|
||||||
|
focusNode: focusNode,
|
||||||
|
textEditingController: textEditingController,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the nearest [StreamAutocomplete] ancestor of the given context.
|
||||||
|
static MultiTriggerAutocompleteState of(BuildContext context) {
|
||||||
|
final state =
|
||||||
|
context.findAncestorStateOfType<MultiTriggerAutocompleteState>();
|
||||||
|
assert(state != null, 'MultiTriggerAutocomplete not found');
|
||||||
|
return state!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
MultiTriggerAutocompleteState createState() =>
|
||||||
|
MultiTriggerAutocompleteState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiTriggerAutocompleteState extends State<MultiTriggerAutocomplete> {
|
||||||
|
late TextEditingController _textEditingController;
|
||||||
|
late FocusNode _focusNode;
|
||||||
|
|
||||||
|
AutocompleteQuery? _currentQuery;
|
||||||
|
AutocompleteTrigger? _currentTrigger;
|
||||||
|
|
||||||
|
bool _hideOptions = false;
|
||||||
|
String _lastFieldText = '';
|
||||||
|
|
||||||
|
// True if the state indicates that the options should be visible.
|
||||||
|
bool get _shouldShowOptions {
|
||||||
|
return !_hideOptions &&
|
||||||
|
_focusNode.hasFocus &&
|
||||||
|
_currentQuery != null &&
|
||||||
|
_currentTrigger != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void acceptAutocompleteOption(
|
||||||
|
String option, {
|
||||||
|
bool keepTrigger = true,
|
||||||
|
}) {
|
||||||
|
if (option.isEmpty) return;
|
||||||
|
|
||||||
|
final query = _currentQuery;
|
||||||
|
final trigger = _currentTrigger;
|
||||||
|
if (query == null || trigger == null) return;
|
||||||
|
|
||||||
|
final querySelection = query.selection;
|
||||||
|
final text = _textEditingController.text;
|
||||||
|
|
||||||
|
var start = querySelection.baseOffset;
|
||||||
|
if (!keepTrigger) start -= 1;
|
||||||
|
|
||||||
|
final end = querySelection.extentOffset;
|
||||||
|
|
||||||
|
final alreadyContainsTriggerEnd =
|
||||||
|
text.substring(end).startsWith(trigger.triggerEnd);
|
||||||
|
// Having triggerEnd dismissing the auto-completion view.
|
||||||
|
if (!alreadyContainsTriggerEnd) option += trigger.triggerEnd;
|
||||||
|
|
||||||
|
var selectionOffset = start + option.length;
|
||||||
|
// In case the triggerEnd is already there, we need to move the cursor
|
||||||
|
// after the triggerEnd.
|
||||||
|
if (alreadyContainsTriggerEnd) selectionOffset += trigger.triggerEnd.length;
|
||||||
|
|
||||||
|
final newText = text.replaceRange(start, end, option);
|
||||||
|
final newSelection = TextSelection.collapsed(offset: selectionOffset);
|
||||||
|
|
||||||
|
_textEditingController.value = TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: newSelection,
|
||||||
|
);
|
||||||
|
|
||||||
|
return closeOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
void closeOptions() {
|
||||||
|
final prevQuery = _currentQuery;
|
||||||
|
final prevTrigger = _currentTrigger;
|
||||||
|
if (prevQuery == null || prevTrigger == null) return;
|
||||||
|
|
||||||
|
_currentQuery = null;
|
||||||
|
_currentTrigger = null;
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void showOptions(
|
||||||
|
AutocompleteQuery query,
|
||||||
|
AutocompleteTrigger trigger,
|
||||||
|
) {
|
||||||
|
final prevQuery = _currentQuery;
|
||||||
|
final prevTrigger = _currentTrigger;
|
||||||
|
if (prevQuery == query && prevTrigger == trigger) return;
|
||||||
|
|
||||||
|
_currentQuery = query;
|
||||||
|
_currentTrigger = trigger;
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if there is any invoked autocomplete trigger and returns the
|
||||||
|
// one with has the longest trigger length along with the query that
|
||||||
|
// matches the current input.
|
||||||
|
_AutocompleteInvokedTriggerWithQuery? _getInvokedTriggerWithQuery(
|
||||||
|
TextEditingValue textEditingValue,
|
||||||
|
) {
|
||||||
|
final autocompleteTriggers = widget.autocompleteTriggers.toSet();
|
||||||
|
AutocompleteTrigger? finalTrigger;
|
||||||
|
AutocompleteQuery? finalQuery;
|
||||||
|
|
||||||
|
for (final autocompleteTrigger in autocompleteTriggers) {
|
||||||
|
final query = autocompleteTrigger.invokingTrigger(textEditingValue);
|
||||||
|
if (query != null &&
|
||||||
|
(finalTrigger == null ||
|
||||||
|
autocompleteTrigger.trigger.length >
|
||||||
|
finalTrigger.trigger.length)) {
|
||||||
|
finalTrigger = autocompleteTrigger;
|
||||||
|
finalQuery = query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalTrigger != null && finalQuery != null) {
|
||||||
|
return _AutocompleteInvokedTriggerWithQuery(finalTrigger, finalQuery);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer? _debounceTimer;
|
||||||
|
|
||||||
|
// Called when _textEditingController changes.
|
||||||
|
void _onChangedField() {
|
||||||
|
if (_debounceTimer?.isActive == true) _debounceTimer?.cancel();
|
||||||
|
_debounceTimer = Timer(widget.debounceDuration, () {
|
||||||
|
final textEditingValue = _textEditingController.value;
|
||||||
|
|
||||||
|
// If the content has not changed, then there is nothing to do.
|
||||||
|
if (textEditingValue.text == _lastFieldText) return;
|
||||||
|
|
||||||
|
// Make sure the options are no longer hidden if the content of the
|
||||||
|
// field changes.
|
||||||
|
_hideOptions = false;
|
||||||
|
_lastFieldText = textEditingValue.text;
|
||||||
|
|
||||||
|
// If the text field is empty, then there is no need to do anything.
|
||||||
|
if (textEditingValue.text.isEmpty) return closeOptions();
|
||||||
|
|
||||||
|
// If the text field is not empty, then we need to check if the
|
||||||
|
// text field contains a trigger.
|
||||||
|
final triggerWithQuery = _getInvokedTriggerWithQuery(textEditingValue);
|
||||||
|
|
||||||
|
// If the text field does not contain a trigger, then there is no need
|
||||||
|
// to do anything.
|
||||||
|
if (triggerWithQuery == null) return closeOptions();
|
||||||
|
|
||||||
|
// If the text field contains a trigger, then we need to open the
|
||||||
|
// portal.
|
||||||
|
final trigger = triggerWithQuery.trigger;
|
||||||
|
final query = triggerWithQuery.query;
|
||||||
|
return showOptions(query, trigger);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the field's FocusNode changes.
|
||||||
|
void _onChangedFocus() {
|
||||||
|
// Options should no longer be hidden when the field is re-focused.
|
||||||
|
_hideOptions = !_focusNode.hasFocus;
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a potential change in textEditingController by properly disposing of
|
||||||
|
// the old one and setting up the new one, if needed.
|
||||||
|
void _updateTextEditingController(
|
||||||
|
TextEditingController? old, TextEditingController? current) {
|
||||||
|
if ((old == null && current == null) || old == current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (old == null) {
|
||||||
|
_textEditingController.removeListener(_onChangedField);
|
||||||
|
_textEditingController.dispose();
|
||||||
|
_textEditingController = current!;
|
||||||
|
} else if (current == null) {
|
||||||
|
_textEditingController.removeListener(_onChangedField);
|
||||||
|
_textEditingController = TextEditingController();
|
||||||
|
} else {
|
||||||
|
_textEditingController.removeListener(_onChangedField);
|
||||||
|
_textEditingController = current;
|
||||||
|
}
|
||||||
|
_textEditingController.addListener(_onChangedField);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a potential change in focusNode by properly disposing of the old one
|
||||||
|
// and setting up the new one, if needed.
|
||||||
|
void _updateFocusNode(FocusNode? old, FocusNode? current) {
|
||||||
|
if ((old == null && current == null) || old == current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (old == null) {
|
||||||
|
_focusNode.removeListener(_onChangedFocus);
|
||||||
|
_focusNode.dispose();
|
||||||
|
_focusNode = current!;
|
||||||
|
} else if (current == null) {
|
||||||
|
_focusNode.removeListener(_onChangedFocus);
|
||||||
|
_focusNode = FocusNode();
|
||||||
|
} else {
|
||||||
|
_focusNode.removeListener(_onChangedFocus);
|
||||||
|
_focusNode = current;
|
||||||
|
}
|
||||||
|
_focusNode.addListener(_onChangedFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_textEditingController = widget.textEditingController ??
|
||||||
|
TextEditingController.fromValue(widget.initialValue);
|
||||||
|
_textEditingController.addListener(_onChangedField);
|
||||||
|
_focusNode = widget.focusNode ?? FocusNode();
|
||||||
|
_focusNode.addListener(_onChangedFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(MultiTriggerAutocomplete oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_updateTextEditingController(
|
||||||
|
oldWidget.textEditingController,
|
||||||
|
widget.textEditingController,
|
||||||
|
);
|
||||||
|
_updateFocusNode(oldWidget.focusNode, widget.focusNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_textEditingController.removeListener(_onChangedField);
|
||||||
|
if (widget.textEditingController == null) {
|
||||||
|
_textEditingController.dispose();
|
||||||
|
}
|
||||||
|
_focusNode.removeListener(_onChangedFocus);
|
||||||
|
if (widget.focusNode == null) {
|
||||||
|
_focusNode.dispose();
|
||||||
|
}
|
||||||
|
_debounceTimer?.cancel();
|
||||||
|
_currentTrigger = null;
|
||||||
|
_currentQuery = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Adding additional builder so that [MultiTriggerAutocomplete.of] works.
|
||||||
|
return Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final anchor = widget.optionsAlignment._toAnchor(
|
||||||
|
widthFactor: widget.optionsWidthFactor,
|
||||||
|
);
|
||||||
|
final shouldShowOptions = _shouldShowOptions;
|
||||||
|
final optionViewBuilder = shouldShowOptions
|
||||||
|
? TextFieldTapRegion(
|
||||||
|
child: _currentTrigger!.optionsViewBuilder(
|
||||||
|
context,
|
||||||
|
_currentQuery!,
|
||||||
|
_textEditingController,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return PortalTarget(
|
||||||
|
anchor: anchor,
|
||||||
|
visible: shouldShowOptions,
|
||||||
|
portalFollower: optionViewBuilder,
|
||||||
|
child: widget.fieldViewBuilder(
|
||||||
|
context,
|
||||||
|
_textEditingController,
|
||||||
|
_focusNode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AutocompleteInvokedTriggerWithQuery {
|
||||||
|
const _AutocompleteInvokedTriggerWithQuery(this.trigger, this.query);
|
||||||
|
|
||||||
|
final AutocompleteTrigger trigger;
|
||||||
|
final AutocompleteQuery query;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The default Material-style Autocomplete text field.
|
||||||
|
class _MultiTriggerAutocompleteField extends StatelessWidget {
|
||||||
|
const _MultiTriggerAutocompleteField({
|
||||||
|
Key? key,
|
||||||
|
required this.focusNode,
|
||||||
|
required this.textEditingController,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final FocusNode focusNode;
|
||||||
|
|
||||||
|
final TextEditingController textEditingController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextFormField(
|
||||||
|
controller: textEditingController,
|
||||||
|
focusNode: focusNode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
35
packages/multi_trigger_autocomplete_plus/pubspec.yaml
Normal file
35
packages/multi_trigger_autocomplete_plus/pubspec.yaml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
name: multi_trigger_autocomplete_plus
|
||||||
|
homepage: https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus
|
||||||
|
description: A flutter widget to add trigger based autocomplete functionality to your app.
|
||||||
|
version: 1.0.2
|
||||||
|
repository: https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus
|
||||||
|
issue_tracker: https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus/issues
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.18.0 <4.0.0"
|
||||||
|
flutter: ">=3.3.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_portal: ^1.0.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
|
|
||||||
|
topics:
|
||||||
|
- mentions
|
||||||
|
- autocomplete
|
||||||
|
- autocomplete-search
|
||||||
|
- autocomplete-textfield
|
||||||
|
- autocomplete-suggestions
|
||||||
|
|
||||||
|
screenshots:
|
||||||
|
- description: "Emoji Autocomplete Demo"
|
||||||
|
path: asset/emoji_demo.gif
|
||||||
|
- description: "Hashtag Autocomplete Demo"
|
||||||
|
path: asset/hashtag_demo.gif
|
||||||
|
- description: "Mention Autocomplete Demo"
|
||||||
|
path: asset/mention_demo.gif
|
@ -0,0 +1,300 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:multi_trigger_autocomplete_plus/src/autocomplete_trigger.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Autocomplete with trigger `@`', () {
|
||||||
|
final trigger = AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
optionsViewBuilder: (
|
||||||
|
context,
|
||||||
|
autocompleteQuery,
|
||||||
|
textEditingController,
|
||||||
|
) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null if `@` is not found', () {
|
||||||
|
const text = 'Hello There';
|
||||||
|
const value = TextEditingValue(
|
||||||
|
text: text,
|
||||||
|
selection: TextSelection.collapsed(offset: text.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
final invoked = trigger.invokingTrigger(value);
|
||||||
|
|
||||||
|
expect(invoked, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'should return null if `@` is found but the cursor is not at the triggered word',
|
||||||
|
() {
|
||||||
|
const text = 'Hello there @Sahil Kumar';
|
||||||
|
const value = TextEditingValue(
|
||||||
|
text: text,
|
||||||
|
selection: TextSelection.collapsed(offset: text.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
final invoked = trigger.invokingTrigger(value);
|
||||||
|
|
||||||
|
expect(invoked, isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'should return the autocomplete query if `@` is found and the cursor is at the triggered word',
|
||||||
|
() {
|
||||||
|
const text = 'Hello there @Sahil Kumar';
|
||||||
|
const value = TextEditingValue(
|
||||||
|
text: text,
|
||||||
|
selection: TextSelection.collapsed(offset: 18),
|
||||||
|
);
|
||||||
|
|
||||||
|
final invoked = trigger.invokingTrigger(value);
|
||||||
|
|
||||||
|
expect(invoked, isNotNull);
|
||||||
|
expect(invoked!.query, 'Sahil');
|
||||||
|
expect(
|
||||||
|
invoked.selection,
|
||||||
|
const TextSelection(baseOffset: 13, extentOffset: 18),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'should return null if `@` is found but the cursor is not after a space',
|
||||||
|
() {
|
||||||
|
const text = 'Hello there@Sahil Kumar';
|
||||||
|
const value = TextEditingValue(
|
||||||
|
text: text,
|
||||||
|
selection: TextSelection.collapsed(offset: 16),
|
||||||
|
);
|
||||||
|
|
||||||
|
final invoked = trigger.invokingTrigger(value);
|
||||||
|
|
||||||
|
expect(invoked, isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'should return the autocomplete query if `@` is found and the cursor is at the triggered word containing `_` between them',
|
||||||
|
() {
|
||||||
|
const text = 'Hello there @Sahil_Kumar';
|
||||||
|
const value = TextEditingValue(
|
||||||
|
text: text,
|
||||||
|
selection: TextSelection.collapsed(offset: text.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
final invoked = trigger.invokingTrigger(value);
|
||||||
|
|
||||||
|
expect(invoked, isNotNull);
|
||||||
|
expect(invoked!.query, 'Sahil_Kumar');
|
||||||
|
expect(
|
||||||
|
invoked.selection,
|
||||||
|
const TextSelection(baseOffset: 13, extentOffset: 24),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Autocomplete trigger with `triggerOnlyAtStart` true', () {
|
||||||
|
final trigger = AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
triggerOnlyAtStart: true,
|
||||||
|
optionsViewBuilder: (
|
||||||
|
context,
|
||||||
|
autocompleteQuery,
|
||||||
|
textEditingController,
|
||||||
|
) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'should return query if `@` is invoked at the start and cursor is after the word',
|
||||||
|
() {
|
||||||
|
final invoked = trigger.invokingTrigger(
|
||||||
|
const TextEditingValue(
|
||||||
|
text: '@Sahil hey',
|
||||||
|
selection: TextSelection.collapsed(offset: 6),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(invoked, isNotNull);
|
||||||
|
expect(invoked!.query, 'Sahil');
|
||||||
|
expect(
|
||||||
|
invoked.selection,
|
||||||
|
const TextSelection(baseOffset: 1, extentOffset: 6),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"should return null if `@` is found but it's not invoked at the start",
|
||||||
|
() {
|
||||||
|
const text = 'Hey @Sahil';
|
||||||
|
final invoked = trigger.invokingTrigger(
|
||||||
|
const TextEditingValue(
|
||||||
|
text: text,
|
||||||
|
selection: TextSelection.collapsed(offset: text.length),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(invoked, isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Autocomplete trigger with `minimumRequiredCharacters` 3', () {
|
||||||
|
final trigger = AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
minimumRequiredCharacters: 3,
|
||||||
|
optionsViewBuilder: (
|
||||||
|
context,
|
||||||
|
autocompleteQuery,
|
||||||
|
textEditingController,
|
||||||
|
) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'should return query if `@` is invoked cursor is after the word which is at least 3 characters long',
|
||||||
|
() {
|
||||||
|
const text = 'Hey @Sahil';
|
||||||
|
final invoked = trigger.invokingTrigger(
|
||||||
|
const TextEditingValue(
|
||||||
|
text: text,
|
||||||
|
selection: TextSelection.collapsed(offset: text.length),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(invoked, isNotNull);
|
||||||
|
expect(invoked!.query, 'Sahil');
|
||||||
|
expect(
|
||||||
|
invoked.selection,
|
||||||
|
const TextSelection(baseOffset: 5, extentOffset: 10),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"should return null if `@` is found but the word is less than 3 characters long",
|
||||||
|
() {
|
||||||
|
const text = 'Hey @Sahil';
|
||||||
|
final invoked = trigger.invokingTrigger(
|
||||||
|
const TextEditingValue(
|
||||||
|
text: text,
|
||||||
|
selection: TextSelection.collapsed(offset: 6),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(invoked, isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('2 Autocomplete trigger cannot be considered equal if', () {
|
||||||
|
test('they have different `trigger`', () {
|
||||||
|
final trigger1 = AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
optionsViewBuilder: (
|
||||||
|
context,
|
||||||
|
autocompleteQuery,
|
||||||
|
textEditingController,
|
||||||
|
) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final trigger2 = AutocompleteTrigger(
|
||||||
|
trigger: '#',
|
||||||
|
optionsViewBuilder: (
|
||||||
|
context,
|
||||||
|
autocompleteQuery,
|
||||||
|
textEditingController,
|
||||||
|
) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(trigger1, isNot(trigger2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('they have different `triggerOnlyAtStart`', () {
|
||||||
|
final trigger1 = AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
triggerOnlyAtStart: true,
|
||||||
|
optionsViewBuilder: (
|
||||||
|
context,
|
||||||
|
autocompleteQuery,
|
||||||
|
textEditingController,
|
||||||
|
) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final trigger2 = AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
triggerOnlyAtStart: false,
|
||||||
|
optionsViewBuilder: (
|
||||||
|
context,
|
||||||
|
autocompleteQuery,
|
||||||
|
textEditingController,
|
||||||
|
) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(trigger1, isNot(trigger2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('they have different `triggerOnlyAfterSpace`', () {
|
||||||
|
final trigger1 = AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
triggerOnlyAfterSpace: true,
|
||||||
|
optionsViewBuilder: (
|
||||||
|
context,
|
||||||
|
autocompleteQuery,
|
||||||
|
textEditingController,
|
||||||
|
) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final trigger2 = AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
triggerOnlyAfterSpace: false,
|
||||||
|
optionsViewBuilder: (
|
||||||
|
context,
|
||||||
|
autocompleteQuery,
|
||||||
|
textEditingController,
|
||||||
|
) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(trigger1, isNot(trigger2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('they have different `minimumRequiredCharacters`', () {
|
||||||
|
final trigger1 = AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
minimumRequiredCharacters: 3,
|
||||||
|
optionsViewBuilder: (
|
||||||
|
context,
|
||||||
|
autocompleteQuery,
|
||||||
|
textEditingController,
|
||||||
|
) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final trigger2 = AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
minimumRequiredCharacters: 4,
|
||||||
|
optionsViewBuilder: (
|
||||||
|
context,
|
||||||
|
autocompleteQuery,
|
||||||
|
textEditingController,
|
||||||
|
) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(trigger1, isNot(trigger2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,328 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('should render fine', (tester) async {
|
||||||
|
const multiTriggerAutocompleteKey = Key('multiTriggerAutocomplete');
|
||||||
|
const multiTriggerAutocomplete = Boilerplate(
|
||||||
|
child: MultiTriggerAutocomplete(
|
||||||
|
key: multiTriggerAutocompleteKey,
|
||||||
|
autocompleteTriggers: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(multiTriggerAutocomplete);
|
||||||
|
|
||||||
|
expect(find.byKey(multiTriggerAutocompleteKey), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should render fine if both `textEditingController` and `focusNode` is provided',
|
||||||
|
(tester) async {
|
||||||
|
const multiTriggerAutocompleteKey = Key('multiTriggerAutocomplete');
|
||||||
|
final multiTriggerAutocomplete = Boilerplate(
|
||||||
|
child: MultiTriggerAutocomplete(
|
||||||
|
key: multiTriggerAutocompleteKey,
|
||||||
|
autocompleteTriggers: const [],
|
||||||
|
textEditingController: TextEditingController(),
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(multiTriggerAutocomplete);
|
||||||
|
|
||||||
|
expect(find.byKey(multiTriggerAutocompleteKey), findsOneWidget);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
"should throw assertion if `textEditingController` is provided but `focusNode` isn't",
|
||||||
|
(tester) async {
|
||||||
|
expect(
|
||||||
|
() => Boilerplate(
|
||||||
|
child: MultiTriggerAutocomplete(
|
||||||
|
autocompleteTriggers: const [],
|
||||||
|
textEditingController: TextEditingController(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
throwsAssertionError,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
"should throw assertion if `focusNode` is provided but `textEditingController` isn't",
|
||||||
|
(tester) async {
|
||||||
|
expect(
|
||||||
|
() => Boilerplate(
|
||||||
|
child: MultiTriggerAutocomplete(
|
||||||
|
autocompleteTriggers: const [],
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
throwsAssertionError,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
"should render fine if `initialValue` is defined without `textEditingController`",
|
||||||
|
(tester) async {
|
||||||
|
const multiTriggerAutocompleteKey = Key('multiTriggerAutocomplete');
|
||||||
|
const multiTriggerAutocomplete = Boilerplate(
|
||||||
|
child: MultiTriggerAutocomplete(
|
||||||
|
key: multiTriggerAutocompleteKey,
|
||||||
|
autocompleteTriggers: [],
|
||||||
|
initialValue: TextEditingValue(text: 'initialValue'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(multiTriggerAutocomplete);
|
||||||
|
|
||||||
|
expect(find.byKey(multiTriggerAutocompleteKey), findsOneWidget);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
"should throw assertion if `initialValue` is defined along with `textEditingController`",
|
||||||
|
(tester) async {
|
||||||
|
expect(
|
||||||
|
() => Boilerplate(
|
||||||
|
child: MultiTriggerAutocomplete(
|
||||||
|
autocompleteTriggers: const [],
|
||||||
|
initialValue: const TextEditingValue(text: 'initialValue'),
|
||||||
|
textEditingController: TextEditingController(),
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
throwsAssertionError,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('MultiTriggerAutocomplete', () {
|
||||||
|
const kUsers = [
|
||||||
|
User(id: 'xsahil03x', name: 'Sahil Kumar'),
|
||||||
|
User(id: 'avu.saxena', name: 'Avni Saxena'),
|
||||||
|
User(id: 'trapti2711', name: 'Trapti Gupta'),
|
||||||
|
User(id: 'itsmegb98', name: 'Gaurav Bhadouriya'),
|
||||||
|
User(id: 'amitk_15', name: 'Amit Kumar'),
|
||||||
|
User(id: 'ayushpgupta', name: 'Ayush Gupta'),
|
||||||
|
User(id: 'someshubham', name: 'Shubham Jain'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const kDebounceDuration = Duration(milliseconds: 300);
|
||||||
|
final mentionTrigger = AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
||||||
|
final query = autocompleteQuery.query;
|
||||||
|
final filteredUsers = kUsers.where((user) {
|
||||||
|
return user.name.toLowerCase().contains(query.toLowerCase());
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: filteredUsers.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final user = filteredUsers[index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(user.name),
|
||||||
|
onTap: () {
|
||||||
|
final autocomplete = MultiTriggerAutocomplete.of(context);
|
||||||
|
return autocomplete.acceptAutocompleteOption(user.name);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'can filter and select a list of string options',
|
||||||
|
(tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Boilerplate(
|
||||||
|
child: MultiTriggerAutocomplete(
|
||||||
|
debounceDuration: kDebounceDuration,
|
||||||
|
autocompleteTriggers: [mentionTrigger],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// The field is always rendered, but the options are not unless needed.
|
||||||
|
expect(find.byType(TextFormField), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsNothing);
|
||||||
|
|
||||||
|
// Entering the trigger text. All the options are displayed.
|
||||||
|
await tester.enterText(find.byType(TextFormField), '@');
|
||||||
|
await tester.pumpAndSettle(kDebounceDuration);
|
||||||
|
expect(find.byType(ListView), findsOneWidget);
|
||||||
|
ListView list =
|
||||||
|
find.byType(ListView).evaluate().first.widget as ListView;
|
||||||
|
expect(list.semanticChildCount, kUsers.length);
|
||||||
|
|
||||||
|
// Enter query text. The options are filtered by the text.
|
||||||
|
await tester.enterText(find.byType(TextFormField), '@sa');
|
||||||
|
await tester.pumpAndSettle(kDebounceDuration);
|
||||||
|
expect(find.byType(TextFormField), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsOneWidget);
|
||||||
|
list = find.byType(ListView).evaluate().first.widget as ListView;
|
||||||
|
// '@Sahil kumar' and '@Avni Saxena' are displayed.
|
||||||
|
expect(list.semanticChildCount, 2);
|
||||||
|
|
||||||
|
// Select a option. The options hide and the field updates to show the
|
||||||
|
// selection.
|
||||||
|
await tester.tap(find.byType(InkWell).first);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.byType(TextFormField), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsNothing);
|
||||||
|
final TextFormField field =
|
||||||
|
find.byType(TextFormField).evaluate().first.widget as TextFormField;
|
||||||
|
expect(field.controller!.text, '@Sahil Kumar ');
|
||||||
|
|
||||||
|
// Modify the field text. The options appear again and are filtered.
|
||||||
|
await tester.enterText(find.byType(TextFormField), '@av');
|
||||||
|
await tester.pumpAndSettle(kDebounceDuration);
|
||||||
|
expect(find.byType(TextFormField), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsOneWidget);
|
||||||
|
list = find.byType(ListView).evaluate().first.widget as ListView;
|
||||||
|
// '@Avni Saxena' and '@Gaurav Bhadouriya' are displayed.
|
||||||
|
expect(list.semanticChildCount, 2);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'options position changes when alignment changed',
|
||||||
|
(tester) async {
|
||||||
|
late StateSetter setState;
|
||||||
|
OptionsAlignment alignment = OptionsAlignment.bottom;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Boilerplate(
|
||||||
|
child: StatefulBuilder(
|
||||||
|
builder: (context, setter) {
|
||||||
|
setState = setter;
|
||||||
|
return MultiTriggerAutocomplete(
|
||||||
|
optionsAlignment: alignment,
|
||||||
|
debounceDuration: kDebounceDuration,
|
||||||
|
autocompleteTriggers: [mentionTrigger],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Field is shown but not options.
|
||||||
|
expect(find.byType(TextFormField), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsNothing);
|
||||||
|
|
||||||
|
// Enter text to show the options.
|
||||||
|
await tester.enterText(find.byType(TextFormField), '@');
|
||||||
|
await tester.pumpAndSettle(kDebounceDuration);
|
||||||
|
expect(find.byType(TextFormField), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsOneWidget);
|
||||||
|
|
||||||
|
// Options are just below the field.
|
||||||
|
final optionsOffset = tester.getTopLeft(find.byType(ListView));
|
||||||
|
Offset fieldOffset = tester.getTopLeft(find.byType(TextFormField));
|
||||||
|
final fieldSize = tester.getSize(find.byType(TextFormField));
|
||||||
|
expect(optionsOffset.dy, fieldOffset.dy + fieldSize.height);
|
||||||
|
|
||||||
|
// Changing the alignment should change the position of options.
|
||||||
|
setState(() => alignment = OptionsAlignment.top);
|
||||||
|
await tester.pump();
|
||||||
|
fieldOffset = tester.getBottomLeft(find.byType(TextFormField));
|
||||||
|
final optionsOffsetOpen = tester.getBottomLeft(find.byType(ListView));
|
||||||
|
expect(optionsOffsetOpen.dy, isNot(equals(optionsOffset.dy)));
|
||||||
|
expect(optionsOffsetOpen.dy, fieldOffset.dy - fieldSize.height);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'initialValue sets initial text field value',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Boilerplate(
|
||||||
|
child: MultiTriggerAutocomplete(
|
||||||
|
// Should initialize text field with '@sa'.
|
||||||
|
initialValue: const TextEditingValue(text: '@sa'),
|
||||||
|
debounceDuration: kDebounceDuration,
|
||||||
|
autocompleteTriggers: [mentionTrigger],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// The field is always rendered, but the options are not unless needed.
|
||||||
|
expect(find.byType(TextFormField), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsNothing);
|
||||||
|
|
||||||
|
// The text editing controller value starts off with initialized value.
|
||||||
|
final TextFormField field =
|
||||||
|
find.byType(TextFormField).evaluate().first.widget as TextFormField;
|
||||||
|
expect(field.controller!.text, '@sa');
|
||||||
|
|
||||||
|
// Focus the empty field. All the options are displayed.
|
||||||
|
await tester.tap(find.byType(TextFormField));
|
||||||
|
await tester.pumpAndSettle(kDebounceDuration);
|
||||||
|
expect(find.byType(ListView), findsOneWidget);
|
||||||
|
ListView list =
|
||||||
|
find.byType(ListView).evaluate().first.widget as ListView;
|
||||||
|
// '@Sahil kumar' and '@Avni Saxena' are displayed.
|
||||||
|
expect(list.semanticChildCount, 2);
|
||||||
|
|
||||||
|
// Select an option. The options hide and the field updates to show the
|
||||||
|
// selection.
|
||||||
|
await tester.tap(find.byType(InkWell).first);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.byType(TextFormField), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsNothing);
|
||||||
|
expect(field.controller!.text, '@Sahil Kumar ');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('can build a custom field', (WidgetTester tester) async {
|
||||||
|
const fieldKey = Key('fieldKey');
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Boilerplate(
|
||||||
|
child: MultiTriggerAutocomplete(
|
||||||
|
autocompleteTriggers: [mentionTrigger],
|
||||||
|
fieldViewBuilder: (context, controller, focusNode) {
|
||||||
|
return Container(key: fieldKey);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// The custom field is rendered and not the default TextFormField.
|
||||||
|
expect(find.byKey(fieldKey), findsOneWidget);
|
||||||
|
expect(find.byType(TextFormField), findsNothing);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class User {
|
||||||
|
const User({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Boilerplate extends StatelessWidget {
|
||||||
|
const Boilerplate({Key? key, this.child}) : super(key: key);
|
||||||
|
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Portal(
|
||||||
|
child: MaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
home: Scaffold(
|
||||||
|
body: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
126
pubspec.lock
126
pubspec.lock
@ -5,23 +5,23 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "72.0.0"
|
version: "76.0.0"
|
||||||
_macros:
|
_macros:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: dart
|
description: dart
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.3.2"
|
version: "0.3.3"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.7.0"
|
version: "6.11.0"
|
||||||
ansi_styles:
|
ansi_styles:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -72,10 +72,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.0"
|
version: "2.12.0"
|
||||||
audio_session:
|
audio_session:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -104,10 +104,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: boolean_selector
|
name: boolean_selector
|
||||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.2"
|
||||||
build:
|
build:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -176,10 +176,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.4.0"
|
||||||
charcode:
|
charcode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -224,10 +224,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: clock
|
name: clock
|
||||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.2"
|
||||||
code_builder:
|
code_builder:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -240,10 +240,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.19.1"
|
||||||
conventional_commit:
|
conventional_commit:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -375,10 +375,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.2"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -391,10 +391,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file
|
name: file
|
||||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
version: "7.0.1"
|
||||||
file_selector:
|
file_selector:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -876,18 +876,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.5"
|
version: "10.0.8"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.5"
|
version: "3.0.9"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -924,10 +924,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: macros
|
name: macros
|
||||||
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2-main.4"
|
version: "0.1.3-main.0"
|
||||||
markdown:
|
markdown:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -940,10 +940,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.16+1"
|
version: "0.12.17"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -964,10 +964,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
version: "1.16.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1000,15 +1000,13 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.6.0"
|
version: "3.6.0"
|
||||||
multi_trigger_autocomplete:
|
multi_trigger_autocomplete_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "packages/multi_trigger_autocomplete_plus"
|
||||||
ref: cb22bab30dd14452d184bc6ad3bb41b612b22c70
|
relative: true
|
||||||
resolved-ref: cb22bab30dd14452d184bc6ad3bb41b612b22c70
|
source: path
|
||||||
url: "https://github.com/foss42/multi_trigger_autocomplete.git"
|
version: "1.0.2"
|
||||||
source: git
|
|
||||||
version: "1.0.1"
|
|
||||||
mustache_template:
|
mustache_template:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1069,10 +1067,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.1"
|
||||||
path_parsing:
|
path_parsing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1157,10 +1155,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.5"
|
version: "3.1.6"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1228,10 +1226,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: process
|
name: process
|
||||||
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
|
sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
version: "5.0.3"
|
||||||
prompts:
|
prompts:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1468,7 +1466,7 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
version: "0.0.0"
|
||||||
source_gen:
|
source_gen:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1505,10 +1503,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_span
|
name: source_span
|
||||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.10.1"
|
||||||
spot:
|
spot:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -1529,10 +1527,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.1"
|
version: "1.12.1"
|
||||||
state_notifier:
|
state_notifier:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1545,10 +1543,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.4"
|
||||||
stream_transform:
|
stream_transform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1561,10 +1559,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.4.1"
|
||||||
sync_http:
|
sync_http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1577,34 +1575,34 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: term_glyph
|
name: term_glyph
|
||||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.2"
|
||||||
test:
|
test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
|
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.25.7"
|
version: "1.25.15"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.2"
|
version: "0.7.4"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
|
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.4"
|
version: "0.6.8"
|
||||||
textwrap:
|
textwrap:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1801,10 +1799,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.5"
|
version: "14.3.1"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1841,10 +1839,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webdriver
|
name: webdriver
|
||||||
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
|
sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.4"
|
||||||
webkit_inspection_protocol:
|
webkit_inspection_protocol:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1911,5 +1909,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.5.3 <3.999.0"
|
dart: ">=3.7.0-0 <3.999.0"
|
||||||
flutter: ">=3.24.2"
|
flutter: ">=3.24.2"
|
||||||
|
@ -45,10 +45,8 @@ dependencies:
|
|||||||
markdown: ^7.2.2
|
markdown: ^7.2.2
|
||||||
mime_dart: ^3.0.0
|
mime_dart: ^3.0.0
|
||||||
multi_split_view: ^3.2.2
|
multi_split_view: ^3.2.2
|
||||||
multi_trigger_autocomplete:
|
multi_trigger_autocomplete_plus:
|
||||||
git:
|
path: packages/multi_trigger_autocomplete_plus
|
||||||
url: https://github.com/foss42/multi_trigger_autocomplete.git
|
|
||||||
ref: cb22bab30dd14452d184bc6ad3bb41b612b22c70
|
|
||||||
package_info_plus: ^8.0.2
|
package_info_plus: ^8.0.2
|
||||||
path: ^1.8.3
|
path: ^1.8.3
|
||||||
path_provider: ^2.1.2
|
path_provider: ^2.1.2
|
||||||
|
Reference in New Issue
Block a user