feat: implement to-do list component

This commit is contained in:
Lucas.Xu 2023-04-18 15:44:11 +08:00
commit a1ef08006b
20 changed files with 545 additions and 166 deletions

View file

@ -3,24 +3,59 @@
"type": "editor",
"children": [
{
"type": "text",
"type": "paragraph",
"attributes": {
"subtype": "heading",
"heading": "h2"
},
"delta": [
{ "insert": "👋 " },
{ "insert": "Welcome to", "attributes": { "bold": true } },
{ "insert": " " },
{
"insert": "AppFlowy Editor",
"attributes": {
"href": "appflowy.io",
"italic": true,
"bold": true
"delta": [
{ "insert": "👋 " },
{ "insert": "Welcome to", "attributes": { "bold": true } },
{ "insert": " " },
{
"insert": "AppFlowy Editor",
"attributes": {
"href": "appflowy.io",
"italic": true,
"bold": true
}
}
}
]
]
}
},
{
"type": "paragraph",
"attributes": {
"delta": [
{ "insert": "👋 " },
{ "insert": "Welcome to", "attributes": { "bold": true } },
{ "insert": " " },
{
"insert": "AppFlowy Editor",
"attributes": {
"href": "appflowy.io",
"italic": true,
"bold": true
}
}
]
}
},
{
"type": "todo_list",
"attributes": {
"checked": false,
"delta": [
{ "insert": "👋 " },
{ "insert": "Welcome to", "attributes": { "bold": true } },
{ "insert": " " },
{
"insert": "AppFlowy Editor",
"attributes": {
"href": "appflowy.io",
"italic": true,
"bold": true
}
}
]
}
},
{ "type": "text", "delta": [] },
{

View file

@ -38,6 +38,12 @@ class SimpleEditor extends StatelessWidget {
editorState: editorState,
themeData: themeData,
autoFocus: editorState.document.isEmpty,
customBuilders: {
'paragraph': TextBlockComponentBuilder(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
),
'todo_list': TodoListBlockComponentBuilder(),
},
);
} else {
return const Center(

View file

@ -52,3 +52,5 @@ export 'src/extensions/extensions.dart';
export 'src/service/default_text_operations/format_rich_text_style.dart';
export 'src/infra/html_converter.dart';
export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
export 'src/block_component/block_component.dart';

View file

@ -0,0 +1,5 @@
// paragraph
export 'text_block_component/text_block_component.dart';
// to-do list
export 'todo_list_block_component/todo_list_block_component.dart';

View file

@ -0,0 +1,164 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
abstract class TextInputService {
TextInputService({
required this.onInsert,
required this.onDelete,
required this.onReplace,
required this.onNonTextUpdate,
});
Future<void> Function(TextEditingDeltaInsertion insertion) onInsert;
Future<void> Function(TextEditingDeltaDeletion deletion) onDelete;
Future<void> Function(TextEditingDeltaReplacement replacement) onReplace;
Future<void> Function(TextEditingDeltaNonTextUpdate nonTextUpdate)
onNonTextUpdate;
TextRange? composingTextRange;
void updateCaretPosition(Size size, Matrix4 transform, Rect rect);
/// Updates the [TextEditingValue] of the text currently being edited.
///
/// Note that if there are IME-related requirements,
/// please config `composing` value within [TextEditingValue]
void attach(TextEditingValue textEditingValue);
/// Applies insertion, deletion and replacement
/// to the text currently being edited.
///
/// For more information, please check [TextEditingDelta].
Future<void> apply(List<TextEditingDelta> deltas);
/// Closes the editing state of the text currently being edited.
void close();
}
class DeltaTextInputService extends TextInputService
implements DeltaTextInputClient {
DeltaTextInputService({
required super.onInsert,
required super.onDelete,
required super.onReplace,
required super.onNonTextUpdate,
});
TextInputConnection? textInputConnection;
@override
TextRange? composingTextRange;
@override
AutofillScope? get currentAutofillScope => throw UnimplementedError();
@override
TextEditingValue? get currentTextEditingValue => throw UnimplementedError();
@override
Future<void> apply(List<TextEditingDelta> deltas) async {
for (final delta in deltas) {
_updateComposing(delta);
if (delta is TextEditingDeltaInsertion) {
await onInsert(delta);
} else if (delta is TextEditingDeltaDeletion) {
await onDelete(delta);
} else if (delta is TextEditingDeltaReplacement) {
await onReplace(delta);
} else if (delta is TextEditingDeltaNonTextUpdate) {
await onNonTextUpdate(delta);
}
}
}
@override
void attach(TextEditingValue textEditingValue) {
if (textInputConnection == null || textInputConnection!.attached == false) {
textInputConnection = TextInput.attach(
this,
const TextInputConfiguration(
enableDeltaModel: true,
inputType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
),
);
}
textInputConnection!
..setEditingState(textEditingValue)
..show();
}
@override
void close() {
textInputConnection?.close();
textInputConnection = null;
}
@override
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
Log.input.debug(
textEditingDeltas.map((delta) => delta.toString()).toString(),
);
apply(textEditingDeltas);
}
// TODO: support IME in linux / windows / ios / android
// Only support macOS now.
@override
void updateCaretPosition(Size size, Matrix4 transform, Rect rect) {
textInputConnection
?..setEditableSizeAndTransform(size, transform)
..setCaretRect(rect);
}
@override
void connectionClosed() {}
@override
void insertTextPlaceholder(Size size) {}
@override
void performAction(TextInputAction action) {}
@override
void performPrivateCommand(String action, Map<String, dynamic> data) {}
@override
void removeTextPlaceholder() {}
@override
void showAutocorrectionPromptRect(int start, int end) {}
@override
void showToolbar() {}
@override
void updateEditingValue(TextEditingValue value) {}
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {}
void _updateComposing(TextEditingDelta delta) {
if (delta is! TextEditingDeltaNonTextUpdate) {
if (composingTextRange != null &&
composingTextRange!.start != -1 &&
delta.composing.end != -1) {
composingTextRange = TextRange(
start: composingTextRange!.start,
end: delta.composing.end,
);
} else {
composingTextRange = delta.composing;
}
}
}
@override
void didChangeInputControl(
TextInputControl? oldControl,
TextInputControl? newControl,
) {}
@override
void performSelector(String selectorName) {}
}

View file

@ -0,0 +1,87 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/block_component/delta_input_service.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class TextBlockComponentBuilder extends NodeWidgetBuilder<Node> {
TextBlockComponentBuilder({
this.padding = const EdgeInsets.all(0.0),
this.textStyle = const TextStyle(),
});
final EdgeInsets padding;
final TextStyle textStyle;
@override
Widget build(NodeWidgetContext<Node> context) {
return TextBlockComponentWidget(
key: context.node.key,
node: context.node,
padding: padding,
textStyle: textStyle,
);
}
@override
NodeValidator<Node> get nodeValidator => (node) {
// TODO: implement nodeValidator, delta...
return true;
};
}
class TextBlockComponentWidget extends StatefulWidget {
const TextBlockComponentWidget({
super.key,
required this.node,
this.padding = const EdgeInsets.all(0.0),
this.textStyle = const TextStyle(),
});
final Node node;
final EdgeInsets padding;
final TextStyle textStyle;
@override
State<TextBlockComponentWidget> createState() =>
_TextBlockComponentWidgetState();
}
class _TextBlockComponentWidgetState extends State<TextBlockComponentWidget>
with SelectableMixin, DefaultSelectable {
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
late final editorState = Provider.of<EditorState>(context, listen: false);
TextInputService? inputService;
@override
SelectableMixin<StatefulWidget> get forward =>
forwardKey.currentState as SelectableMixin;
@override
Offset get baseOffset => widget.padding.topLeft;
@override
GlobalKey<State<StatefulWidget>>? get iconKey => null;
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: widget.padding,
child: FlowyRichText(
key: forwardKey,
node: widget.node,
editorState: editorState,
),
);
}
}

View file

@ -0,0 +1,142 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class TodoListBlockKeys {
TodoListBlockKeys._();
/// The checked data of a todo list block.
///
/// The value is a boolean.
static const String checked = 'checked';
}
class TodoListBlockComponentBuilder extends NodeWidgetBuilder<Node> {
TodoListBlockComponentBuilder({
this.padding = const EdgeInsets.all(0.0),
this.textStyle = const TextStyle(),
this.icon,
});
/// The padding of the todo list block.
final EdgeInsets padding;
/// The text style of the todo list block.
final TextStyle textStyle;
/// The icon of the todo list block.
final Widget? Function(bool checked)? icon;
@override
Widget build(NodeWidgetContext<Node> context) {
return TodoListBlockComponentWidget(
key: context.node.key,
node: context.node,
padding: padding,
textStyle: textStyle,
icon: icon,
);
}
@override
NodeValidator<Node> get nodeValidator => (node) =>
node.delta != null &&
node.attributes.containsKey(
TodoListBlockKeys.checked,
);
}
class TodoListBlockComponentWidget extends StatefulWidget {
const TodoListBlockComponentWidget({
super.key,
required this.node,
this.padding = const EdgeInsets.all(0.0),
this.textStyle = const TextStyle(),
this.icon,
});
final Node node;
final EdgeInsets padding;
final TextStyle textStyle;
final Widget? Function(bool checked)? icon;
@override
State<TodoListBlockComponentWidget> createState() =>
_TodoListBlockComponentWidgetState();
}
class _TodoListBlockComponentWidgetState
extends State<TodoListBlockComponentWidget>
with SelectableMixin, DefaultSelectable {
@override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
late final editorState = Provider.of<EditorState>(context, listen: false);
bool get checked => widget.node.attributes[TodoListBlockKeys.checked];
@override
Widget build(BuildContext context) {
return Padding(
padding: widget.padding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_TodoListIcon(
checked: checked,
icon: widget.icon,
onTap: checkOrUncheck,
),
FlowyRichText(
key: forwardKey,
node: widget.node,
editorState: editorState,
),
],
),
);
}
Future<void> checkOrUncheck() async {
final transaction = editorState.transaction
..updateNode(widget.node, {
TodoListBlockKeys.checked: !checked,
});
return editorState.apply(transaction);
}
}
class _TodoListIcon extends StatelessWidget {
const _TodoListIcon({
required this.checked,
required this.icon,
required this.onTap,
});
final bool checked;
final Widget? Function(bool checked)? icon;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: icon?.call(checked) ?? defaultCheckboxIcon(),
),
);
}
FlowySvg defaultCheckboxIcon() {
return FlowySvg(
width: 22,
height: 22,
padding: const EdgeInsets.only(right: 5.0),
name: checked ? 'check' : 'uncheck',
);
}
}

View file

@ -7,6 +7,14 @@ import 'package:appflowy_editor/src/core/document/path.dart';
import 'package:appflowy_editor/src/core/document/text_delta.dart';
import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
/*
{
'type': string,
'data': Map<String, Object>,
'children': List<Node>,
}
*/
class Node extends ChangeNotifier with LinkedListEntry<Node> {
Node({
required this.type,
@ -93,16 +101,9 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
Path get path => _computePath();
void updateAttributes(Attributes attributes) {
final oldAttributes = this.attributes;
_attributes = composeAttributes(this.attributes, attributes) ?? {};
// Notifies the new attributes
// if attributes contains 'subtype', should notify parent to rebuild node
// else, just notify current node.
bool shouldNotifyParent =
this.attributes['subtype'] != oldAttributes['subtype'];
shouldNotifyParent ? parent?.notifyListeners() : notifyListeners();
notifyListeners();
}
Node? childAtIndex(int index) {
@ -170,6 +171,13 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
parent = null;
}
Delta? get delta {
if (attributes['delta'] is List) {
return Delta.fromJson(attributes['delta']);
}
return null;
}
Map<String, Object> toJson() {
var map = <String, Object>{
'type': type,
@ -239,6 +247,7 @@ class TextNode extends Node {
);
Delta _delta;
@override
Delta get delta => _delta;
set delta(Delta v) {
_delta = v;

View file

@ -1,16 +1,7 @@
import 'dart:async';
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/infra/log.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
import 'package:appflowy_editor/src/render/style/editor_style.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
import 'package:appflowy_editor/src/service/service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/core/document/document.dart';
import 'package:appflowy_editor/src/core/transform/operation.dart';
import 'package:appflowy_editor/src/core/transform/transaction.dart';
import 'package:appflowy_editor/src/history/undo_manager.dart';
class ApplyOptions {
@ -53,6 +44,8 @@ class EditorState {
// Service reference.
final service = FlowyService();
AppFlowySelectionService get selection => service.selectionService;
/// Configures log output parameters,
/// such as log level and log output callbacks,
/// with this variable.

View file

@ -48,18 +48,7 @@ class BulletedListTextNodeWidget extends BuiltInTextWidget {
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
@override
final iconKey = GlobalKey();
final _richTextKey = GlobalKey(debugLabel: 'bulleted_list_text');
@override
SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as SelectableMixin;
@override
Offset get baseOffset {
return super.baseOffset.translate(0, padding.top);
}
final forwardKey = GlobalKey(debugLabel: 'bulleted_list_text');
BulletedListPluginStyle get style =>
Theme.of(context).extensionOrNull<BulletedListPluginStyle>() ??
@ -88,19 +77,18 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
key: iconKey,
child: icon,
),
Flexible(
child: FlowyRichText(
key: _richTextKey,
key: forwardKey,
placeholderText: 'List',
textSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
lineHeight: widget.editorState.editorStyle.lineHeight,
textNode: widget.textNode,
node: widget.textNode,
editorState: widget.editorState,
),
)

View file

@ -39,18 +39,7 @@ class CheckboxNodeWidget extends BuiltInTextWidget {
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
@override
final iconKey = GlobalKey();
final _richTextKey = GlobalKey(debugLabel: 'checkbox_text');
@override
SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as SelectableMixin;
@override
Offset get baseOffset {
return super.baseOffset.translate(0, padding.top);
}
final forwardKey = GlobalKey(debugLabel: 'checkbox_text');
CheckboxPluginStyle get style =>
Theme.of(context).extensionOrNull<CheckboxPluginStyle>() ??
@ -80,7 +69,6 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
key: iconKey,
behavior: HitTestBehavior.opaque,
onTap: () async {
await widget.editorState.formatTextToCheckbox(
@ -92,10 +80,10 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
),
Flexible(
child: FlowyRichText(
key: _richTextKey,
key: forwardKey,
placeholderText: 'To-do',
lineHeight: widget.editorState.editorStyle.lineHeight,
textNode: widget.textNode,
node: widget.textNode,
textSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
placeholderTextSpanDecorator: (textSpan) =>

View file

@ -2,18 +2,19 @@ import 'package:appflowy_editor/src/core/location/position.dart';
import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
mixin DefaultSelectable {
SelectableMixin get forward;
GlobalKey get forwardKey;
GlobalKey? get iconKey;
SelectableMixin<StatefulWidget> get forward =>
forwardKey.currentState as SelectableMixin;
Offset get baseOffset {
if (iconKey != null) {
final renderBox = iconKey!.currentContext?.findRenderObject();
if (renderBox is RenderBox) {
return Offset(renderBox.size.width, 0);
}
final renderBox = forwardKey.currentContext?.findRenderObject();
final parentData = renderBox?.parentData;
if (parentData is BoxParentData) {
return parentData.offset;
}
return Offset.zero;
}

View file

@ -31,11 +31,11 @@ class FlowyRichText extends StatefulWidget {
this.textSpanDecorator,
this.placeholderText = ' ',
this.placeholderTextSpanDecorator,
required this.textNode,
required this.node,
required this.editorState,
}) : super(key: key);
final TextNode textNode;
final Node node;
final EditorState editorState;
final double? cursorHeight;
final double cursorWidth;
@ -75,12 +75,12 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
}
@override
Position start() => Position(path: widget.textNode.path, offset: 0);
Position start() => Position(path: widget.node.path, offset: 0);
@override
Position end() => Position(
path: widget.textNode.path,
offset: widget.textNode.toPlainText().length,
path: widget.node.path,
offset: widget.node.delta?.toPlainText().length ?? 0,
);
@override
@ -119,7 +119,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
Position getPositionInOffset(Offset start) {
final offset = _renderParagraph.globalToLocal(start);
final baseOffset = _renderParagraph.getPositionForOffset(offset).offset;
return Position(path: widget.textNode.path, offset: baseOffset);
return Position(path: widget.node.path, offset: baseOffset);
}
@override
@ -127,8 +127,8 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
final localOffset = _renderParagraph.globalToLocal(offset);
final textPosition = _renderParagraph.getPositionForOffset(localOffset);
final textRange = _renderParagraph.getWordBoundary(textPosition);
final start = Position(path: widget.textNode.path, offset: textRange.start);
final end = Position(path: widget.textNode.path, offset: textRange.end);
final start = Position(path: widget.node.path, offset: textRange.start);
final end = Position(path: widget.node.path, offset: textRange.end);
return Selection(start: start, end: end);
}
@ -136,15 +136,15 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
Selection? getWordBoundaryInPosition(Position position) {
final textPosition = TextPosition(offset: position.offset);
final textRange = _renderParagraph.getWordBoundary(textPosition);
final start = Position(path: widget.textNode.path, offset: textRange.start);
final end = Position(path: widget.textNode.path, offset: textRange.end);
final start = Position(path: widget.node.path, offset: textRange.start);
final end = Position(path: widget.node.path, offset: textRange.end);
return Selection(start: start, end: end);
}
@override
List<Rect> getRectsInSelection(Selection selection) {
assert(
selection.isSingle && selection.start.path.equals(widget.textNode.path),
selection.isSingle && selection.start.path.equals(widget.node.path),
);
final textSelection = TextSelection(
@ -171,7 +171,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
final baseOffset = _renderParagraph.getPositionForOffset(localStart).offset;
final extentOffset = _renderParagraph.getPositionForOffset(localEnd).offset;
return Selection.single(
path: widget.textNode.path,
path: widget.node.path,
startOffset: baseOffset,
endOffset: extentOffset,
);
@ -185,7 +185,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
Widget _buildRichText(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.text,
child: widget.textNode.toPlainText().isEmpty
child: widget.node.delta?.toPlainText().isEmpty ?? true
? Stack(
children: [
_buildPlaceholderText(context),
@ -241,7 +241,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
var offset = 0;
List<TextSpan> textSpans = [];
final style = widget.editorState.editorStyle;
final textInserts = widget.textNode.delta.whereType<TextInsert>();
final textInserts = widget.node.delta!.whereType<TextInsert>();
for (final textInsert in textInserts) {
var textStyle = style.textStyle!;
GestureRecognizer? recognizer;
@ -264,7 +264,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
recognizer = _buildTapHrefGestureRecognizer(
attributes.href!,
Selection.single(
path: widget.textNode.path,
path: widget.node.path,
startOffset: offset,
endOffset: offset + textInsert.length,
),
@ -296,7 +296,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
if (_kRichTextDebugMode) {
textSpans.add(
TextSpan(
text: '${widget.textNode.path}',
text: '${widget.node.path}',
style: const TextStyle(
backgroundColor: Colors.red,
fontSize: 16.0,

View file

@ -47,18 +47,7 @@ class HeadingTextNodeWidget extends BuiltInTextWidget {
class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
with SelectableMixin, DefaultSelectable {
@override
GlobalKey? get iconKey => null;
final _richTextKey = GlobalKey(debugLabel: 'heading_text');
@override
SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as SelectableMixin;
@override
Offset get baseOffset {
return padding.topLeft;
}
final forwardKey = GlobalKey(debugLabel: 'checkbox_text');
HeadingPluginStyle get style =>
Theme.of(context).extensionOrNull<HeadingPluginStyle>() ??
@ -79,13 +68,13 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
return Padding(
padding: padding,
child: FlowyRichText(
key: _richTextKey,
key: forwardKey,
placeholderText: 'Heading',
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
lineHeight: widget.editorState.editorStyle.lineHeight,
textNode: widget.textNode,
node: widget.textNode,
editorState: widget.editorState,
),
);

View file

@ -47,18 +47,7 @@ class NumberListTextNodeWidget extends BuiltInTextWidget {
class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
with SelectableMixin, DefaultSelectable {
@override
final iconKey = GlobalKey();
final _richTextKey = GlobalKey(debugLabel: 'number_list_text');
@override
SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as SelectableMixin;
@override
Offset get baseOffset {
return super.baseOffset.translate(0, padding.top);
}
final forwardKey = GlobalKey(debugLabel: 'checkbox_text');
NumberListPluginStyle get style =>
Theme.of(context).extensionOrNull<NumberListPluginStyle>() ??
@ -87,14 +76,13 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
key: iconKey,
child: icon,
),
Flexible(
child: FlowyRichText(
key: _richTextKey,
key: forwardKey,
placeholderText: 'List',
textNode: widget.textNode,
node: widget.textNode,
editorState: widget.editorState,
lineHeight: widget.editorState.editorStyle.lineHeight,
placeholderTextSpanDecorator: (textSpan) =>

View file

@ -47,18 +47,7 @@ class QuotedTextNodeWidget extends BuiltInTextWidget {
class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
with SelectableMixin, DefaultSelectable {
@override
final iconKey = GlobalKey();
final _richTextKey = GlobalKey(debugLabel: 'quoted_text');
@override
SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as SelectableMixin;
@override
Offset get baseOffset {
return super.baseOffset.translate(0, padding.top);
}
final forwardKey = GlobalKey(debugLabel: 'checkbox_text');
QuotedTextPluginStyle get style =>
Theme.of(context).extensionOrNull<QuotedTextPluginStyle>() ??
@ -88,14 +77,13 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
key: iconKey,
child: icon,
),
Flexible(
child: FlowyRichText(
key: _richTextKey,
key: forwardKey,
placeholderText: 'Quote',
textNode: widget.textNode,
node: widget.textNode,
textSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
placeholderTextSpanDecorator: (textSpan) =>

View file

@ -46,18 +46,7 @@ class RichTextNodeWidget extends BuiltInTextWidget {
class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
@override
GlobalKey? get iconKey => null;
final _richTextKey = GlobalKey(debugLabel: 'rich_text');
@override
SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as SelectableMixin;
@override
Offset get baseOffset {
return textPadding.topLeft;
}
final forwardKey = GlobalKey(debugLabel: 'checkbox_text');
EditorStyle get style => widget.editorState.editorStyle;
@ -70,8 +59,8 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
return Padding(
padding: textPadding,
child: FlowyRichText(
key: _richTextKey,
textNode: widget.textNode,
key: forwardKey,
node: widget.textNode,
textSpanDecorator: (textSpan) => textSpan,
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),

View file

@ -10,6 +10,7 @@ import 'package:appflowy_editor/src/render/rich_text/quoted_text.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text.dart';
import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
import 'package:provider/provider.dart';
NodeWidgetBuilders defaultBuilders = {
'editor': EditorEntryWidgetBuilder(),
@ -125,12 +126,15 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
Widget build(BuildContext context) {
services ??= _buildServices(context);
return Overlay(
initialEntries: [
OverlayEntry(
builder: (context) => services!,
),
],
return Provider.value(
value: editorState,
child: Overlay(
initialEntries: [
OverlayEntry(
builder: (context) => services!,
),
],
),
);
}

View file

@ -279,15 +279,14 @@ class _AppFlowyInputState extends State<AppFlowyInput>
}
void _onSelectionChange() {
final textNodes = _editorState.service.selectionService.currentSelectedNodes
.whereType<TextNode>();
final selection =
_editorState.service.selectionService.currentSelection.value;
// FIXME: upward and selection update.
if (textNodes.isNotEmpty && selection != null) {
final text = textNodes.fold<String>(
final editableNodes = _editorState.selection.currentSelectedNodes.where(
(element) => element.delta != null,
);
final selection = _editorState.selection.currentSelection.value;
if (editableNodes.isNotEmpty && selection != null) {
final text = editableNodes.fold<String>(
'',
(sum, textNode) => '$sum${textNode.toPlainText()}\n',
(sum, editableNode) => '$sum${editableNode.delta!.toPlainText()}\n',
);
attach(
TextEditingValue(
@ -296,11 +295,12 @@ class _AppFlowyInputState extends State<AppFlowyInput>
baseOffset: selection.start.offset,
extentOffset: selection.end.offset,
),
composing: _composingTextRange ?? const TextRange.collapsed(-1),
composing: _composingTextRange ??
TextRange.collapsed(selection.start.offset),
),
);
if (textNodes.length == 1) {
_updateCaretPosition(textNodes.first, selection);
if (editableNodes.length == 1) {
_updateCaretPosition(editableNodes.first, selection);
}
} else {
// https://github.com/flutter/flutter/issues/104944
@ -313,12 +313,12 @@ class _AppFlowyInputState extends State<AppFlowyInput>
// TODO: support IME in linux / windows / ios / android
// Only support macOS now.
void _updateCaretPosition(TextNode textNode, Selection selection) {
if (!selection.isCollapsed) {
void _updateCaretPosition(Node node, Selection selection) {
if (!selection.isCollapsed || node.delta == null) {
return;
}
final renderBox = textNode.renderBox;
final selectable = textNode.selectable;
final renderBox = node.renderBox;
final selectable = node.selectable;
if (renderBox != null && selectable != null) {
final size = renderBox.size;
final transform = renderBox.getTransformTo(null);

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
ShortcutEventHandler backspaceEventHandler = (editorState, event) {
return KeyEventResult.ignored;
var selection = editorState.service.selectionService.currentSelection.value;
if (selection == null) {
return KeyEventResult.ignored;