feat: support auto complete for text block (#764)
* feat: support auto complete for text block * feat: support auto complete for text block * test: add auto complete test * feat: support tab to auto complete * chore: optimize the code * chore: update token
This commit is contained in:
parent
6d12761929
commit
61d892a09c
12 changed files with 399 additions and 19 deletions
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
desktop:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-13, macos-12, macos-11, ubuntu-22.04, ubuntu-20.04, windows-2022, windows-2019]
|
||||
os: [macos-14, macos-13, macos-12, macos-11, ubuntu-22.04, ubuntu-20.04, windows-2022, windows-2019]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
|
|
@ -52,6 +52,6 @@ jobs:
|
|||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
os: ${{ matrix.os }}
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
token: 398c9ded-28ad-4750-bfdb-e7f502539137
|
||||
attempt_limit: 10
|
||||
attempt_delay: 10000
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:example/pages/auto_complete_editor.dart';
|
||||
import 'package:example/pages/collab_editor.dart';
|
||||
import 'package:example/pages/collab_selection_editor.dart';
|
||||
import 'package:example/pages/customize_theme_for_editor.dart';
|
||||
|
|
@ -161,6 +162,14 @@ class _HomePageState extends State<HomePage> {
|
|||
|
||||
// Theme Demo
|
||||
_buildSeparator(context, 'Showcases'),
|
||||
_buildListTile(context, 'Auto complete Editor', () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AutoCompleteEditor(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
_buildListTile(context, 'Collab Editor', () {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
|
|
|||
70
example/lib/pages/auto_complete_editor.dart
Normal file
70
example/lib/pages/auto_complete_editor.dart
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
final _options = {
|
||||
'hello ': 'world',
|
||||
'support@g': 'mail.com',
|
||||
'appflowy ': 'editor',
|
||||
};
|
||||
|
||||
class AutoCompleteEditor extends StatelessWidget {
|
||||
const AutoCompleteEditor({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 100),
|
||||
Container(
|
||||
height: 500,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.blue),
|
||||
),
|
||||
child: AppFlowyEditor(
|
||||
editorState: EditorState(
|
||||
document: Document.blank(withInitialText: true),
|
||||
),
|
||||
commandShortcutEvents: [
|
||||
tabToAutoCompleteCommand,
|
||||
...standardCommandShortcutEvents,
|
||||
],
|
||||
enableAutoComplete: true,
|
||||
autoCompleteTextProvider: (context, node, textSpan) {
|
||||
final editorState = context.read<EditorState>();
|
||||
final selection = editorState.selection;
|
||||
final delta = node.delta;
|
||||
if (selection == null ||
|
||||
delta == null ||
|
||||
!selection.isCollapsed ||
|
||||
selection.endIndex != delta.length ||
|
||||
!node.path.equals(selection.start.path)) {
|
||||
return null;
|
||||
}
|
||||
final text = delta.toPlainText().toLowerCase();
|
||||
for (final option in _options.keys) {
|
||||
if (text.endsWith(option)) {
|
||||
return _options[option];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'AutoComplete Options: $_options',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DesktopEditor extends StatefulWidget {
|
||||
const DesktopEditor({
|
||||
|
|
@ -84,6 +85,8 @@ class _DesktopEditorState extends State<DesktopEditor> {
|
|||
blockComponentBuilders: blockComponentBuilders,
|
||||
commandShortcutEvents: commandShortcuts,
|
||||
editorStyle: editorStyle,
|
||||
enableAutoComplete: true,
|
||||
autoCompleteTextProvider: _buildAutoCompleteTextProvider,
|
||||
header: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0),
|
||||
child: Image.asset(
|
||||
|
|
@ -188,4 +191,27 @@ class _DesktopEditorState extends State<DesktopEditor> {
|
|||
),
|
||||
];
|
||||
}
|
||||
|
||||
String? _buildAutoCompleteTextProvider(
|
||||
BuildContext context,
|
||||
Node node,
|
||||
TextSpan? textSpan,
|
||||
) {
|
||||
final editorState = context.read<EditorState>();
|
||||
final selection = editorState.selection;
|
||||
final delta = node.delta;
|
||||
if (selection == null ||
|
||||
delta == null ||
|
||||
!selection.isCollapsed ||
|
||||
selection.endIndex != delta.length ||
|
||||
!node.path.equals(selection.start.path)) {
|
||||
return null;
|
||||
}
|
||||
final text = delta.toPlainText();
|
||||
// An example, if the text ends with 'hello', then show the autocomplete.
|
||||
if (text.endsWith('hello')) {
|
||||
return ' world';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// Add your custom block keys if it supports auto complete
|
||||
final autoCompletableBlockTypes = {
|
||||
ParagraphBlockKeys.type,
|
||||
NumberedListBlockKeys.type,
|
||||
TodoListBlockKeys.type,
|
||||
BulletedListBlockKeys.type,
|
||||
QuoteBlockKeys.type,
|
||||
HeadingBlockKeys.type,
|
||||
};
|
||||
|
||||
/// Auto complete the current block
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
///
|
||||
final CommandShortcutEvent tabToAutoCompleteCommand = CommandShortcutEvent(
|
||||
key: 'tab to auto complete',
|
||||
getDescription: () => 'Tab to auto complete',
|
||||
command: 'tab',
|
||||
handler: _tabToAutoCompleteCommandHandler,
|
||||
);
|
||||
|
||||
CommandShortcutEventHandler _tabToAutoCompleteCommandHandler = (editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final context = editorState.document.root.context;
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
|
||||
// Now, this command only support auto complete the text if the cursor is at the end of the block
|
||||
if (context == null ||
|
||||
node == null ||
|
||||
!autoCompletableBlockTypes.contains(node.type) ||
|
||||
delta == null ||
|
||||
selection.endIndex != delta.length) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
// Support async auto complete text provider in the future
|
||||
final autoCompleteText = editorState.autoCompleteTextProvider?.call(
|
||||
context,
|
||||
node,
|
||||
null,
|
||||
);
|
||||
if (autoCompleteText == null || autoCompleteText.isEmpty) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction
|
||||
..insertText(
|
||||
node,
|
||||
selection.endIndex,
|
||||
autoCompleteText,
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
|
@ -20,6 +20,9 @@ class TextStyleConfiguration {
|
|||
color: Colors.red,
|
||||
backgroundColor: Color.fromARGB(98, 0, 195, 255),
|
||||
),
|
||||
this.autoComplete = const TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
});
|
||||
|
||||
/// default text style
|
||||
|
|
@ -43,6 +46,9 @@ class TextStyleConfiguration {
|
|||
/// code text style
|
||||
final TextStyle code;
|
||||
|
||||
/// auto complete text style
|
||||
final TextStyle autoComplete;
|
||||
|
||||
TextStyleConfiguration copyWith({
|
||||
TextStyle? text,
|
||||
TextStyle? bold,
|
||||
|
|
@ -51,6 +57,7 @@ class TextStyleConfiguration {
|
|||
TextStyle? strikethrough,
|
||||
TextStyle? href,
|
||||
TextStyle? code,
|
||||
TextStyle? autoComplete,
|
||||
}) {
|
||||
return TextStyleConfiguration(
|
||||
text: text ?? this.text,
|
||||
|
|
@ -60,6 +67,7 @@ class TextStyleConfiguration {
|
|||
strikethrough: strikethrough ?? this.strikethrough,
|
||||
href: href ?? this.href,
|
||||
code: code ?? this.code,
|
||||
autoComplete: autoComplete ?? this.autoComplete,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// paragraph
|
||||
export 'base_component/align_mixin.dart';
|
||||
export 'base_component/auto_complete_command.dart';
|
||||
export 'base_component/background_color_mixin.dart';
|
||||
export 'base_component/block_component_action_wrapper.dart';
|
||||
export 'base_component/block_component_configuration.dart';
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ typedef TextSpanDecoratorForAttribute = InlineSpan Function(
|
|||
);
|
||||
|
||||
typedef AppFlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan);
|
||||
typedef AppFlowyAutoCompleteTextProvider = String? Function(
|
||||
BuildContext context,
|
||||
Node node,
|
||||
TextSpan? textSpan,
|
||||
);
|
||||
|
||||
class AppFlowyRichText extends StatefulWidget {
|
||||
const AppFlowyRichText({
|
||||
|
|
@ -42,6 +47,7 @@ class AppFlowyRichText extends StatefulWidget {
|
|||
this.textAlign,
|
||||
this.cursorColor = const Color.fromARGB(255, 0, 0, 0),
|
||||
this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
|
||||
this.autoCompleteTextProvider,
|
||||
required this.delegate,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
|
|
@ -78,6 +84,9 @@ class AppFlowyRichText extends StatefulWidget {
|
|||
// get the cursor rect, selection rects or block rect from the delegate
|
||||
final SelectableMixin delegate;
|
||||
|
||||
// this span will be appended to the current text span, mostly, it is used for auto complete
|
||||
final AppFlowyAutoCompleteTextProvider? autoCompleteTextProvider;
|
||||
|
||||
/// customize the text span for custom attributes
|
||||
///
|
||||
/// You can use this to customize the text span for custom attributes
|
||||
|
|
@ -107,19 +116,35 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
widget.textSpanDecoratorForCustomAttributes ??
|
||||
widget.editorState.editorStyle.textSpanDecorator;
|
||||
|
||||
AppFlowyAutoCompleteTextProvider? get autoCompleteTextProvider =>
|
||||
widget.autoCompleteTextProvider ??
|
||||
widget.editorState.autoCompleteTextProvider;
|
||||
bool get enableAutoComplete =>
|
||||
widget.editorState.enableAutoComplete && autoCompleteTextProvider != null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = MouseRegion(
|
||||
cursor: SystemMouseCursors.text,
|
||||
child: widget.node.delta?.toPlainText().isEmpty ?? true
|
||||
? Stack(
|
||||
children: [
|
||||
_buildPlaceholderText(context),
|
||||
_buildRichText(context),
|
||||
],
|
||||
)
|
||||
: _buildRichText(context),
|
||||
);
|
||||
Widget child = _buildRichText(context);
|
||||
|
||||
final delta = widget.node.delta;
|
||||
if (delta == null || delta.isEmpty) {
|
||||
child = Stack(
|
||||
children: [
|
||||
_buildPlaceholderText(context),
|
||||
_buildRichText(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (enableAutoComplete) {
|
||||
final autoCompleteText = _buildAutoCompleteRichText();
|
||||
child = Stack(
|
||||
children: [
|
||||
autoCompleteText,
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return BlockSelectionContainer(
|
||||
delegate: widget.delegate,
|
||||
|
|
@ -128,7 +153,10 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
node: widget.node,
|
||||
cursorColor: widget.cursorColor,
|
||||
selectionColor: widget.selectionColor,
|
||||
child: child,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.text,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -314,7 +342,8 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
}
|
||||
|
||||
Widget _buildRichText(BuildContext context) {
|
||||
final textSpan = getTextSpan();
|
||||
final textInserts = widget.node.delta!.whereType<TextInsert>();
|
||||
TextSpan textSpan = getTextSpan(textInserts: textInserts);
|
||||
return RichText(
|
||||
key: textKey,
|
||||
textAlign: widget.textAlign ?? TextAlign.start,
|
||||
|
|
@ -331,6 +360,55 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildAutoCompleteRichText() {
|
||||
final textInserts = widget.node.delta!.whereType<TextInsert>();
|
||||
TextSpan textSpan = getTextSpan(textInserts: textInserts);
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: widget.editorState.selectionNotifier,
|
||||
builder: (_, __, ___) {
|
||||
final autoCompleteText = autoCompleteTextProvider?.call(
|
||||
context,
|
||||
widget.node,
|
||||
textSpan,
|
||||
);
|
||||
if (autoCompleteText == null || autoCompleteText.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
textSpan = getTextSpan(
|
||||
textInserts: [
|
||||
...textInserts.map(
|
||||
(e) => TextInsert(
|
||||
e.text,
|
||||
attributes: {
|
||||
AppFlowyRichTextKeys.transparent: true,
|
||||
},
|
||||
),
|
||||
),
|
||||
TextInsert(
|
||||
autoCompleteText,
|
||||
attributes: {
|
||||
AppFlowyRichTextKeys.autoComplete: true,
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
return RichText(
|
||||
textAlign: widget.textAlign ?? TextAlign.start,
|
||||
textHeightBehavior: const TextHeightBehavior(
|
||||
applyHeightToFirstAscent: false,
|
||||
applyHeightToLastDescent: false,
|
||||
),
|
||||
text: widget.textSpanDecorator != null
|
||||
? widget.textSpanDecorator!(textSpan)
|
||||
: textSpan,
|
||||
textDirection: textDirection(),
|
||||
textScaler:
|
||||
TextScaler.linear(widget.editorState.editorStyle.textScaleFactor),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
TextSpan getPlaceholderTextSpan() {
|
||||
final style = widget.editorState.editorStyle.textStyleConfiguration;
|
||||
return TextSpan(
|
||||
|
|
@ -343,11 +421,12 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
);
|
||||
}
|
||||
|
||||
TextSpan getTextSpan() {
|
||||
TextSpan getTextSpan({
|
||||
required Iterable<TextInsert> textInserts,
|
||||
}) {
|
||||
int offset = 0;
|
||||
List<InlineSpan> textSpans = [];
|
||||
final style = widget.editorState.editorStyle.textStyleConfiguration;
|
||||
final textInserts = widget.node.delta!.whereType<TextInsert>();
|
||||
for (final textInsert in textInserts) {
|
||||
TextStyle textStyle = style.text.copyWith(height: widget.lineHeight);
|
||||
final attributes = textInsert.attributes;
|
||||
|
|
@ -395,6 +474,14 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
TextStyle(fontSize: attributes.fontSize),
|
||||
);
|
||||
}
|
||||
if (attributes.autoComplete == true) {
|
||||
textStyle = textStyle.combine(style.autoComplete);
|
||||
}
|
||||
if (attributes.transparent == true) {
|
||||
textStyle = textStyle.combine(
|
||||
const TextStyle(color: Colors.transparent),
|
||||
);
|
||||
}
|
||||
}
|
||||
final textSpan = TextSpan(
|
||||
text: textInsert.text,
|
||||
|
|
@ -523,4 +610,8 @@ extension AppFlowyRichTextAttributes on Attributes {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool get autoComplete => this[AppFlowyRichTextKeys.autoComplete] == true;
|
||||
|
||||
bool get transparent => this[AppFlowyRichTextKeys.transparent] == true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ class AppFlowyRichTextKeys {
|
|||
static String href = 'href';
|
||||
static String fontFamily = 'font_family';
|
||||
static String fontSize = 'font_size';
|
||||
static String autoComplete = 'auto_complete';
|
||||
static String transparent = 'transparent';
|
||||
|
||||
static List<String> supportSliced = [
|
||||
bold,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ class AppFlowyEditor extends StatefulWidget {
|
|||
this.header,
|
||||
this.footer,
|
||||
this.focusNode,
|
||||
this.enableAutoComplete = false,
|
||||
this.autoCompleteTextProvider,
|
||||
}) : blockComponentBuilders =
|
||||
blockComponentBuilders ?? standardBlockComponentBuilderMap,
|
||||
characterShortcutEvents =
|
||||
|
|
@ -162,6 +164,12 @@ class AppFlowyEditor extends StatefulWidget {
|
|||
/// only works on iOS or Android.
|
||||
final bool showMagnifier;
|
||||
|
||||
/// If you want to enable the auto complete feature, you must set this value to true
|
||||
/// and provide the [autoCompleteTextProvider].
|
||||
final bool enableAutoComplete;
|
||||
|
||||
final AppFlowyAutoCompleteTextProvider? autoCompleteTextProvider;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
|
||||
final ContentInsertionConfiguration? contentInsertionConfiguration;
|
||||
|
||||
|
|
@ -296,6 +304,8 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
|||
editorState.editable = widget.editable;
|
||||
editorState.showHeader = widget.header != null;
|
||||
editorState.showFooter = widget.footer != null;
|
||||
editorState.enableAutoComplete = widget.enableAutoComplete;
|
||||
editorState.autoCompleteTextProvider = widget.autoCompleteTextProvider;
|
||||
}
|
||||
|
||||
BlockComponentRendererService get _renderer => BlockComponentRenderer(
|
||||
|
|
|
|||
|
|
@ -187,6 +187,9 @@ class EditorState {
|
|||
bool showHeader = false;
|
||||
bool showFooter = false;
|
||||
|
||||
bool enableAutoComplete = false;
|
||||
AppFlowyAutoCompleteTextProvider? autoCompleteTextProvider;
|
||||
|
||||
// only used for testing
|
||||
bool disableSealTimer = false;
|
||||
bool disableRules = false;
|
||||
|
|
@ -294,12 +297,16 @@ class EditorState {
|
|||
selection = _applyTransactionFromRemote(transaction);
|
||||
} else {
|
||||
// broadcast to other users here, before applying the transaction
|
||||
_observer.add((TransactionTime.before, transaction));
|
||||
if (!_observer.isClosed) {
|
||||
_observer.add((TransactionTime.before, transaction));
|
||||
}
|
||||
|
||||
_applyTransactionInLocal(transaction);
|
||||
|
||||
// broadcast to other users here, after applying the transaction
|
||||
_observer.add((TransactionTime.after, transaction));
|
||||
if (!_observer.isClosed) {
|
||||
_observer.add((TransactionTime.after, transaction));
|
||||
}
|
||||
|
||||
_recordRedoOrUndo(options, transaction);
|
||||
|
||||
|
|
|
|||
91
test/customer/auto_complete_test.dart
Normal file
91
test/customer/auto_complete_test.dart
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../new/util/util.dart';
|
||||
|
||||
void main() async {
|
||||
testWidgets('auto complete', (tester) async {
|
||||
const input = 'Hello';
|
||||
final document = Document.blank()
|
||||
..addParagraph(
|
||||
initialText: input,
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
final widget = AutoCompleteEditor(
|
||||
editorState: editorState,
|
||||
);
|
||||
await tester.pumpWidget(widget);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
editorState.selection = Selection.collapsed(
|
||||
Position(path: [0], offset: input.length),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.textContaining('world', findRichText: true),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(path: [0], offset: 0),
|
||||
end: Position(path: [0], offset: input.length),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.textContaining('world', findRichText: true),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class AutoCompleteEditor extends StatelessWidget {
|
||||
const AutoCompleteEditor({
|
||||
super.key,
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
width: 500,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.blue),
|
||||
),
|
||||
child: AppFlowyEditor(
|
||||
editorState: editorState,
|
||||
enableAutoComplete: true,
|
||||
autoCompleteTextProvider: (context, node, textSpan) {
|
||||
final editorState = context.read<EditorState>();
|
||||
final selection = editorState.selection;
|
||||
final delta = node.delta;
|
||||
if (selection == null ||
|
||||
delta == null ||
|
||||
!selection.isCollapsed ||
|
||||
selection.endIndex != delta.length ||
|
||||
!node.path.equals(selection.start.path)) {
|
||||
return null;
|
||||
}
|
||||
final text = delta.toPlainText();
|
||||
// An example, if the text ends with 'hello', then show the autocomplete.
|
||||
if (text.toLowerCase().endsWith('hello')) {
|
||||
return ' world';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue