feat: implement to-do list component
This commit is contained in:
parent
0f303cbd8e
commit
a1ef08006b
20 changed files with 545 additions and 166 deletions
|
|
@ -3,24 +3,59 @@
|
||||||
"type": "editor",
|
"type": "editor",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "paragraph",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"subtype": "heading",
|
"delta": [
|
||||||
"heading": "h2"
|
{ "insert": "👋 " },
|
||||||
},
|
{ "insert": "Welcome to", "attributes": { "bold": true } },
|
||||||
"delta": [
|
{ "insert": " " },
|
||||||
{ "insert": "👋 " },
|
{
|
||||||
{ "insert": "Welcome to", "attributes": { "bold": true } },
|
"insert": "AppFlowy Editor",
|
||||||
{ "insert": " " },
|
"attributes": {
|
||||||
{
|
"href": "appflowy.io",
|
||||||
"insert": "AppFlowy Editor",
|
"italic": true,
|
||||||
"attributes": {
|
"bold": true
|
||||||
"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": [] },
|
{ "type": "text", "delta": [] },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,12 @@ class SimpleEditor extends StatelessWidget {
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
themeData: themeData,
|
themeData: themeData,
|
||||||
autoFocus: editorState.document.isEmpty,
|
autoFocus: editorState.document.isEmpty,
|
||||||
|
customBuilders: {
|
||||||
|
'paragraph': TextBlockComponentBuilder(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
),
|
||||||
|
'todo_list': TodoListBlockComponentBuilder(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return const Center(
|
return const Center(
|
||||||
|
|
|
||||||
|
|
@ -52,3 +52,5 @@ export 'src/extensions/extensions.dart';
|
||||||
export 'src/service/default_text_operations/format_rich_text_style.dart';
|
export 'src/service/default_text_operations/format_rich_text_style.dart';
|
||||||
export 'src/infra/html_converter.dart';
|
export 'src/infra/html_converter.dart';
|
||||||
export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
||||||
|
|
||||||
|
export 'src/block_component/block_component.dart';
|
||||||
|
|
|
||||||
5
lib/src/block_component/block_component.dart
Normal file
5
lib/src/block_component/block_component.dart
Normal 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';
|
||||||
164
lib/src/block_component/delta_input_service.dart
Normal file
164
lib/src/block_component/delta_input_service.dart
Normal 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) {}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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/document/text_delta.dart';
|
||||||
import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.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> {
|
class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
Node({
|
Node({
|
||||||
required this.type,
|
required this.type,
|
||||||
|
|
@ -93,16 +101,9 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
Path get path => _computePath();
|
Path get path => _computePath();
|
||||||
|
|
||||||
void updateAttributes(Attributes attributes) {
|
void updateAttributes(Attributes attributes) {
|
||||||
final oldAttributes = this.attributes;
|
|
||||||
|
|
||||||
_attributes = composeAttributes(this.attributes, attributes) ?? {};
|
_attributes = composeAttributes(this.attributes, attributes) ?? {};
|
||||||
|
|
||||||
// Notifies the new attributes
|
notifyListeners();
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Node? childAtIndex(int index) {
|
Node? childAtIndex(int index) {
|
||||||
|
|
@ -170,6 +171,13 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
parent = null;
|
parent = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Delta? get delta {
|
||||||
|
if (attributes['delta'] is List) {
|
||||||
|
return Delta.fromJson(attributes['delta']);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Object> toJson() {
|
Map<String, Object> toJson() {
|
||||||
var map = <String, Object>{
|
var map = <String, Object>{
|
||||||
'type': type,
|
'type': type,
|
||||||
|
|
@ -239,6 +247,7 @@ class TextNode extends Node {
|
||||||
);
|
);
|
||||||
|
|
||||||
Delta _delta;
|
Delta _delta;
|
||||||
|
@override
|
||||||
Delta get delta => _delta;
|
Delta get delta => _delta;
|
||||||
set delta(Delta v) {
|
set delta(Delta v) {
|
||||||
_delta = v;
|
_delta = v;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
import 'package:appflowy_editor/appflowy_editor.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:flutter/material.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';
|
import 'package:appflowy_editor/src/history/undo_manager.dart';
|
||||||
|
|
||||||
class ApplyOptions {
|
class ApplyOptions {
|
||||||
|
|
@ -53,6 +44,8 @@ class EditorState {
|
||||||
// Service reference.
|
// Service reference.
|
||||||
final service = FlowyService();
|
final service = FlowyService();
|
||||||
|
|
||||||
|
AppFlowySelectionService get selection => service.selectionService;
|
||||||
|
|
||||||
/// Configures log output parameters,
|
/// Configures log output parameters,
|
||||||
/// such as log level and log output callbacks,
|
/// such as log level and log output callbacks,
|
||||||
/// with this variable.
|
/// with this variable.
|
||||||
|
|
|
||||||
|
|
@ -48,18 +48,7 @@ class BulletedListTextNodeWidget extends BuiltInTextWidget {
|
||||||
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
||||||
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
|
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
|
||||||
@override
|
@override
|
||||||
final iconKey = GlobalKey();
|
final forwardKey = GlobalKey(debugLabel: 'bulleted_list_text');
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
BulletedListPluginStyle get style =>
|
BulletedListPluginStyle get style =>
|
||||||
Theme.of(context).extensionOrNull<BulletedListPluginStyle>() ??
|
Theme.of(context).extensionOrNull<BulletedListPluginStyle>() ??
|
||||||
|
|
@ -88,19 +77,18 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
key: iconKey,
|
|
||||||
child: icon,
|
child: icon,
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: FlowyRichText(
|
child: FlowyRichText(
|
||||||
key: _richTextKey,
|
key: forwardKey,
|
||||||
placeholderText: 'List',
|
placeholderText: 'List',
|
||||||
textSpanDecorator: (textSpan) =>
|
textSpanDecorator: (textSpan) =>
|
||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
placeholderTextSpanDecorator: (textSpan) =>
|
placeholderTextSpanDecorator: (textSpan) =>
|
||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
lineHeight: widget.editorState.editorStyle.lineHeight,
|
lineHeight: widget.editorState.editorStyle.lineHeight,
|
||||||
textNode: widget.textNode,
|
node: widget.textNode,
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,18 +39,7 @@ class CheckboxNodeWidget extends BuiltInTextWidget {
|
||||||
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
||||||
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
|
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
|
||||||
@override
|
@override
|
||||||
final iconKey = GlobalKey();
|
final forwardKey = GlobalKey(debugLabel: 'checkbox_text');
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckboxPluginStyle get style =>
|
CheckboxPluginStyle get style =>
|
||||||
Theme.of(context).extensionOrNull<CheckboxPluginStyle>() ??
|
Theme.of(context).extensionOrNull<CheckboxPluginStyle>() ??
|
||||||
|
|
@ -80,7 +69,6 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
key: iconKey,
|
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await widget.editorState.formatTextToCheckbox(
|
await widget.editorState.formatTextToCheckbox(
|
||||||
|
|
@ -92,10 +80,10 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: FlowyRichText(
|
child: FlowyRichText(
|
||||||
key: _richTextKey,
|
key: forwardKey,
|
||||||
placeholderText: 'To-do',
|
placeholderText: 'To-do',
|
||||||
lineHeight: widget.editorState.editorStyle.lineHeight,
|
lineHeight: widget.editorState.editorStyle.lineHeight,
|
||||||
textNode: widget.textNode,
|
node: widget.textNode,
|
||||||
textSpanDecorator: (textSpan) =>
|
textSpanDecorator: (textSpan) =>
|
||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
placeholderTextSpanDecorator: (textSpan) =>
|
placeholderTextSpanDecorator: (textSpan) =>
|
||||||
|
|
|
||||||
|
|
@ -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/core/location/selection.dart';
|
||||||
import 'package:appflowy_editor/src/render/selection/selectable.dart';
|
import 'package:appflowy_editor/src/render/selection/selectable.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
mixin DefaultSelectable {
|
mixin DefaultSelectable {
|
||||||
SelectableMixin get forward;
|
GlobalKey get forwardKey;
|
||||||
|
|
||||||
GlobalKey? get iconKey;
|
SelectableMixin<StatefulWidget> get forward =>
|
||||||
|
forwardKey.currentState as SelectableMixin;
|
||||||
|
|
||||||
Offset get baseOffset {
|
Offset get baseOffset {
|
||||||
if (iconKey != null) {
|
final renderBox = forwardKey.currentContext?.findRenderObject();
|
||||||
final renderBox = iconKey!.currentContext?.findRenderObject();
|
final parentData = renderBox?.parentData;
|
||||||
if (renderBox is RenderBox) {
|
if (parentData is BoxParentData) {
|
||||||
return Offset(renderBox.size.width, 0);
|
return parentData.offset;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Offset.zero;
|
return Offset.zero;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,11 @@ class FlowyRichText extends StatefulWidget {
|
||||||
this.textSpanDecorator,
|
this.textSpanDecorator,
|
||||||
this.placeholderText = ' ',
|
this.placeholderText = ' ',
|
||||||
this.placeholderTextSpanDecorator,
|
this.placeholderTextSpanDecorator,
|
||||||
required this.textNode,
|
required this.node,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final TextNode textNode;
|
final Node node;
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final double? cursorHeight;
|
final double? cursorHeight;
|
||||||
final double cursorWidth;
|
final double cursorWidth;
|
||||||
|
|
@ -75,12 +75,12 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Position start() => Position(path: widget.textNode.path, offset: 0);
|
Position start() => Position(path: widget.node.path, offset: 0);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Position end() => Position(
|
Position end() => Position(
|
||||||
path: widget.textNode.path,
|
path: widget.node.path,
|
||||||
offset: widget.textNode.toPlainText().length,
|
offset: widget.node.delta?.toPlainText().length ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -119,7 +119,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
||||||
Position getPositionInOffset(Offset start) {
|
Position getPositionInOffset(Offset start) {
|
||||||
final offset = _renderParagraph.globalToLocal(start);
|
final offset = _renderParagraph.globalToLocal(start);
|
||||||
final baseOffset = _renderParagraph.getPositionForOffset(offset).offset;
|
final baseOffset = _renderParagraph.getPositionForOffset(offset).offset;
|
||||||
return Position(path: widget.textNode.path, offset: baseOffset);
|
return Position(path: widget.node.path, offset: baseOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -127,8 +127,8 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
||||||
final localOffset = _renderParagraph.globalToLocal(offset);
|
final localOffset = _renderParagraph.globalToLocal(offset);
|
||||||
final textPosition = _renderParagraph.getPositionForOffset(localOffset);
|
final textPosition = _renderParagraph.getPositionForOffset(localOffset);
|
||||||
final textRange = _renderParagraph.getWordBoundary(textPosition);
|
final textRange = _renderParagraph.getWordBoundary(textPosition);
|
||||||
final start = Position(path: widget.textNode.path, offset: textRange.start);
|
final start = Position(path: widget.node.path, offset: textRange.start);
|
||||||
final end = Position(path: widget.textNode.path, offset: textRange.end);
|
final end = Position(path: widget.node.path, offset: textRange.end);
|
||||||
return Selection(start: start, end: end);
|
return Selection(start: start, end: end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,15 +136,15 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
||||||
Selection? getWordBoundaryInPosition(Position position) {
|
Selection? getWordBoundaryInPosition(Position position) {
|
||||||
final textPosition = TextPosition(offset: position.offset);
|
final textPosition = TextPosition(offset: position.offset);
|
||||||
final textRange = _renderParagraph.getWordBoundary(textPosition);
|
final textRange = _renderParagraph.getWordBoundary(textPosition);
|
||||||
final start = Position(path: widget.textNode.path, offset: textRange.start);
|
final start = Position(path: widget.node.path, offset: textRange.start);
|
||||||
final end = Position(path: widget.textNode.path, offset: textRange.end);
|
final end = Position(path: widget.node.path, offset: textRange.end);
|
||||||
return Selection(start: start, end: end);
|
return Selection(start: start, end: end);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Rect> getRectsInSelection(Selection selection) {
|
List<Rect> getRectsInSelection(Selection selection) {
|
||||||
assert(
|
assert(
|
||||||
selection.isSingle && selection.start.path.equals(widget.textNode.path),
|
selection.isSingle && selection.start.path.equals(widget.node.path),
|
||||||
);
|
);
|
||||||
|
|
||||||
final textSelection = TextSelection(
|
final textSelection = TextSelection(
|
||||||
|
|
@ -171,7 +171,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
||||||
final baseOffset = _renderParagraph.getPositionForOffset(localStart).offset;
|
final baseOffset = _renderParagraph.getPositionForOffset(localStart).offset;
|
||||||
final extentOffset = _renderParagraph.getPositionForOffset(localEnd).offset;
|
final extentOffset = _renderParagraph.getPositionForOffset(localEnd).offset;
|
||||||
return Selection.single(
|
return Selection.single(
|
||||||
path: widget.textNode.path,
|
path: widget.node.path,
|
||||||
startOffset: baseOffset,
|
startOffset: baseOffset,
|
||||||
endOffset: extentOffset,
|
endOffset: extentOffset,
|
||||||
);
|
);
|
||||||
|
|
@ -185,7 +185,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
||||||
Widget _buildRichText(BuildContext context) {
|
Widget _buildRichText(BuildContext context) {
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
cursor: SystemMouseCursors.text,
|
cursor: SystemMouseCursors.text,
|
||||||
child: widget.textNode.toPlainText().isEmpty
|
child: widget.node.delta?.toPlainText().isEmpty ?? true
|
||||||
? Stack(
|
? Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildPlaceholderText(context),
|
_buildPlaceholderText(context),
|
||||||
|
|
@ -241,7 +241,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
List<TextSpan> textSpans = [];
|
List<TextSpan> textSpans = [];
|
||||||
final style = widget.editorState.editorStyle;
|
final style = widget.editorState.editorStyle;
|
||||||
final textInserts = widget.textNode.delta.whereType<TextInsert>();
|
final textInserts = widget.node.delta!.whereType<TextInsert>();
|
||||||
for (final textInsert in textInserts) {
|
for (final textInsert in textInserts) {
|
||||||
var textStyle = style.textStyle!;
|
var textStyle = style.textStyle!;
|
||||||
GestureRecognizer? recognizer;
|
GestureRecognizer? recognizer;
|
||||||
|
|
@ -264,7 +264,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
||||||
recognizer = _buildTapHrefGestureRecognizer(
|
recognizer = _buildTapHrefGestureRecognizer(
|
||||||
attributes.href!,
|
attributes.href!,
|
||||||
Selection.single(
|
Selection.single(
|
||||||
path: widget.textNode.path,
|
path: widget.node.path,
|
||||||
startOffset: offset,
|
startOffset: offset,
|
||||||
endOffset: offset + textInsert.length,
|
endOffset: offset + textInsert.length,
|
||||||
),
|
),
|
||||||
|
|
@ -296,7 +296,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
||||||
if (_kRichTextDebugMode) {
|
if (_kRichTextDebugMode) {
|
||||||
textSpans.add(
|
textSpans.add(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '${widget.textNode.path}',
|
text: '${widget.node.path}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
fontSize: 16.0,
|
fontSize: 16.0,
|
||||||
|
|
|
||||||
|
|
@ -47,18 +47,7 @@ class HeadingTextNodeWidget extends BuiltInTextWidget {
|
||||||
class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
|
class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
|
||||||
with SelectableMixin, DefaultSelectable {
|
with SelectableMixin, DefaultSelectable {
|
||||||
@override
|
@override
|
||||||
GlobalKey? get iconKey => null;
|
final forwardKey = GlobalKey(debugLabel: 'checkbox_text');
|
||||||
|
|
||||||
final _richTextKey = GlobalKey(debugLabel: 'heading_text');
|
|
||||||
|
|
||||||
@override
|
|
||||||
SelectableMixin<StatefulWidget> get forward =>
|
|
||||||
_richTextKey.currentState as SelectableMixin;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Offset get baseOffset {
|
|
||||||
return padding.topLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
HeadingPluginStyle get style =>
|
HeadingPluginStyle get style =>
|
||||||
Theme.of(context).extensionOrNull<HeadingPluginStyle>() ??
|
Theme.of(context).extensionOrNull<HeadingPluginStyle>() ??
|
||||||
|
|
@ -79,13 +68,13 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: FlowyRichText(
|
child: FlowyRichText(
|
||||||
key: _richTextKey,
|
key: forwardKey,
|
||||||
placeholderText: 'Heading',
|
placeholderText: 'Heading',
|
||||||
placeholderTextSpanDecorator: (textSpan) =>
|
placeholderTextSpanDecorator: (textSpan) =>
|
||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
|
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
|
||||||
lineHeight: widget.editorState.editorStyle.lineHeight,
|
lineHeight: widget.editorState.editorStyle.lineHeight,
|
||||||
textNode: widget.textNode,
|
node: widget.textNode,
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -47,18 +47,7 @@ class NumberListTextNodeWidget extends BuiltInTextWidget {
|
||||||
class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
|
class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
|
||||||
with SelectableMixin, DefaultSelectable {
|
with SelectableMixin, DefaultSelectable {
|
||||||
@override
|
@override
|
||||||
final iconKey = GlobalKey();
|
final forwardKey = GlobalKey(debugLabel: 'checkbox_text');
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberListPluginStyle get style =>
|
NumberListPluginStyle get style =>
|
||||||
Theme.of(context).extensionOrNull<NumberListPluginStyle>() ??
|
Theme.of(context).extensionOrNull<NumberListPluginStyle>() ??
|
||||||
|
|
@ -87,14 +76,13 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
key: iconKey,
|
|
||||||
child: icon,
|
child: icon,
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: FlowyRichText(
|
child: FlowyRichText(
|
||||||
key: _richTextKey,
|
key: forwardKey,
|
||||||
placeholderText: 'List',
|
placeholderText: 'List',
|
||||||
textNode: widget.textNode,
|
node: widget.textNode,
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
lineHeight: widget.editorState.editorStyle.lineHeight,
|
lineHeight: widget.editorState.editorStyle.lineHeight,
|
||||||
placeholderTextSpanDecorator: (textSpan) =>
|
placeholderTextSpanDecorator: (textSpan) =>
|
||||||
|
|
|
||||||
|
|
@ -47,18 +47,7 @@ class QuotedTextNodeWidget extends BuiltInTextWidget {
|
||||||
class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
|
class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
|
||||||
with SelectableMixin, DefaultSelectable {
|
with SelectableMixin, DefaultSelectable {
|
||||||
@override
|
@override
|
||||||
final iconKey = GlobalKey();
|
final forwardKey = GlobalKey(debugLabel: 'checkbox_text');
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
QuotedTextPluginStyle get style =>
|
QuotedTextPluginStyle get style =>
|
||||||
Theme.of(context).extensionOrNull<QuotedTextPluginStyle>() ??
|
Theme.of(context).extensionOrNull<QuotedTextPluginStyle>() ??
|
||||||
|
|
@ -88,14 +77,13 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
key: iconKey,
|
|
||||||
child: icon,
|
child: icon,
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: FlowyRichText(
|
child: FlowyRichText(
|
||||||
key: _richTextKey,
|
key: forwardKey,
|
||||||
placeholderText: 'Quote',
|
placeholderText: 'Quote',
|
||||||
textNode: widget.textNode,
|
node: widget.textNode,
|
||||||
textSpanDecorator: (textSpan) =>
|
textSpanDecorator: (textSpan) =>
|
||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
placeholderTextSpanDecorator: (textSpan) =>
|
placeholderTextSpanDecorator: (textSpan) =>
|
||||||
|
|
|
||||||
|
|
@ -46,18 +46,7 @@ class RichTextNodeWidget extends BuiltInTextWidget {
|
||||||
class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
|
class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
|
||||||
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
|
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
|
||||||
@override
|
@override
|
||||||
GlobalKey? get iconKey => null;
|
final forwardKey = GlobalKey(debugLabel: 'checkbox_text');
|
||||||
|
|
||||||
final _richTextKey = GlobalKey(debugLabel: 'rich_text');
|
|
||||||
|
|
||||||
@override
|
|
||||||
SelectableMixin<StatefulWidget> get forward =>
|
|
||||||
_richTextKey.currentState as SelectableMixin;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Offset get baseOffset {
|
|
||||||
return textPadding.topLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorStyle get style => widget.editorState.editorStyle;
|
EditorStyle get style => widget.editorState.editorStyle;
|
||||||
|
|
||||||
|
|
@ -70,8 +59,8 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: textPadding,
|
padding: textPadding,
|
||||||
child: FlowyRichText(
|
child: FlowyRichText(
|
||||||
key: _richTextKey,
|
key: forwardKey,
|
||||||
textNode: widget.textNode,
|
node: widget.textNode,
|
||||||
textSpanDecorator: (textSpan) => textSpan,
|
textSpanDecorator: (textSpan) => textSpan,
|
||||||
placeholderTextSpanDecorator: (textSpan) =>
|
placeholderTextSpanDecorator: (textSpan) =>
|
||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
|
|
|
||||||
|
|
@ -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/render/rich_text/rich_text.dart';
|
||||||
import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
|
import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
|
||||||
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
NodeWidgetBuilders defaultBuilders = {
|
NodeWidgetBuilders defaultBuilders = {
|
||||||
'editor': EditorEntryWidgetBuilder(),
|
'editor': EditorEntryWidgetBuilder(),
|
||||||
|
|
@ -125,12 +126,15 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
services ??= _buildServices(context);
|
services ??= _buildServices(context);
|
||||||
|
|
||||||
return Overlay(
|
return Provider.value(
|
||||||
initialEntries: [
|
value: editorState,
|
||||||
OverlayEntry(
|
child: Overlay(
|
||||||
builder: (context) => services!,
|
initialEntries: [
|
||||||
),
|
OverlayEntry(
|
||||||
],
|
builder: (context) => services!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -279,15 +279,14 @@ class _AppFlowyInputState extends State<AppFlowyInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionChange() {
|
void _onSelectionChange() {
|
||||||
final textNodes = _editorState.service.selectionService.currentSelectedNodes
|
final editableNodes = _editorState.selection.currentSelectedNodes.where(
|
||||||
.whereType<TextNode>();
|
(element) => element.delta != null,
|
||||||
final selection =
|
);
|
||||||
_editorState.service.selectionService.currentSelection.value;
|
final selection = _editorState.selection.currentSelection.value;
|
||||||
// FIXME: upward and selection update.
|
if (editableNodes.isNotEmpty && selection != null) {
|
||||||
if (textNodes.isNotEmpty && selection != null) {
|
final text = editableNodes.fold<String>(
|
||||||
final text = textNodes.fold<String>(
|
|
||||||
'',
|
'',
|
||||||
(sum, textNode) => '$sum${textNode.toPlainText()}\n',
|
(sum, editableNode) => '$sum${editableNode.delta!.toPlainText()}\n',
|
||||||
);
|
);
|
||||||
attach(
|
attach(
|
||||||
TextEditingValue(
|
TextEditingValue(
|
||||||
|
|
@ -296,11 +295,12 @@ class _AppFlowyInputState extends State<AppFlowyInput>
|
||||||
baseOffset: selection.start.offset,
|
baseOffset: selection.start.offset,
|
||||||
extentOffset: selection.end.offset,
|
extentOffset: selection.end.offset,
|
||||||
),
|
),
|
||||||
composing: _composingTextRange ?? const TextRange.collapsed(-1),
|
composing: _composingTextRange ??
|
||||||
|
TextRange.collapsed(selection.start.offset),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (textNodes.length == 1) {
|
if (editableNodes.length == 1) {
|
||||||
_updateCaretPosition(textNodes.first, selection);
|
_updateCaretPosition(editableNodes.first, selection);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// https://github.com/flutter/flutter/issues/104944
|
// https://github.com/flutter/flutter/issues/104944
|
||||||
|
|
@ -313,12 +313,12 @@ class _AppFlowyInputState extends State<AppFlowyInput>
|
||||||
|
|
||||||
// TODO: support IME in linux / windows / ios / android
|
// TODO: support IME in linux / windows / ios / android
|
||||||
// Only support macOS now.
|
// Only support macOS now.
|
||||||
void _updateCaretPosition(TextNode textNode, Selection selection) {
|
void _updateCaretPosition(Node node, Selection selection) {
|
||||||
if (!selection.isCollapsed) {
|
if (!selection.isCollapsed || node.delta == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final renderBox = textNode.renderBox;
|
final renderBox = node.renderBox;
|
||||||
final selectable = textNode.selectable;
|
final selectable = node.selectable;
|
||||||
if (renderBox != null && selectable != null) {
|
if (renderBox != null && selectable != null) {
|
||||||
final size = renderBox.size;
|
final size = renderBox.size;
|
||||||
final transform = renderBox.getTransformTo(null);
|
final transform = renderBox.getTransformTo(null);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
ShortcutEventHandler backspaceEventHandler = (editorState, event) {
|
ShortcutEventHandler backspaceEventHandler = (editorState, event) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
var selection = editorState.service.selectionService.currentSelection.value;
|
var selection = editorState.service.selectionService.currentSelection.value;
|
||||||
if (selection == null) {
|
if (selection == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue