feat: customize href (#293)
This commit is contained in:
parent
c5b5e641fe
commit
46158d9e26
61 changed files with 766 additions and 1635 deletions
12
README.md
12
README.md
|
|
@ -56,7 +56,7 @@ Start by creating a new empty AppFlowyEditor object.
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
final editorState = EditorState.blank(withInitialText: true); // with an empty paragraph
|
final editorState = EditorState.blank(withInitialText: true); // with an empty paragraph
|
||||||
final editor = AppFlowyEditor.standard(
|
final editor = AppFlowyEditor(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
@ -66,7 +66,7 @@ You can also create an editor from a JSON object in order to configure your init
|
||||||
```dart
|
```dart
|
||||||
final json = jsonDecode('YOUR INPUT JSON STRING');
|
final json = jsonDecode('YOUR INPUT JSON STRING');
|
||||||
final editorState = EditorState(document: Document.fromJson(json));
|
final editorState = EditorState(document: Document.fromJson(json));
|
||||||
final editor = AppFlowyEditor.standard(
|
final editor = AppFlowyEditor(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
@ -90,6 +90,12 @@ flutter run
|
||||||
|
|
||||||
## Customizing Your Editor
|
## Customizing Your Editor
|
||||||
|
|
||||||
|
### Customizing theme
|
||||||
|
|
||||||
|
Please refer to our documentation on customizing AppFlowy for a detailed discussion about [customizing theme](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/documentation/customizing.md#customizing-a-theme).
|
||||||
|
|
||||||
|
* See further examples of [how AppFlowy custom the theme](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart)
|
||||||
|
|
||||||
### Customizing Block Components
|
### Customizing Block Components
|
||||||
|
|
||||||
Please refer to our documentation on customizing AppFlowy for a detailed discussion about [customizing components](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/documentation/customizing.md#customize-a-component).
|
Please refer to our documentation on customizing AppFlowy for a detailed discussion about [customizing components](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/documentation/customizing.md#customize-a-component).
|
||||||
|
|
@ -98,7 +104,7 @@ Below are some examples of component customizations:
|
||||||
|
|
||||||
* [Todo List Block Component](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/lib/src/editor/block_component/todo_list_block_component/todo_list_block_component.dart) demonstrates how to extend new styles based on existing rich text components
|
* [Todo List Block Component](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/lib/src/editor/block_component/todo_list_block_component/todo_list_block_component.dart) demonstrates how to extend new styles based on existing rich text components
|
||||||
* [Divider Block Component](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/lib/src/editor/block_component/divider_block_component/divider_block_component.dart) demonstrates how to extend a new block component and render it
|
* [Divider Block Component](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/lib/src/editor/block_component/divider_block_component/divider_block_component.dart) demonstrates how to extend a new block component and render it
|
||||||
* See further examples of [Rich-Text Plugins](https://github.com/AppFlowy-IO/appflowy-editor/tree/main/lib/src/editor/block_component)
|
* See further examples of [AppFlowy](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart)
|
||||||
|
|
||||||
### Customizing Shortcut Events
|
### Customizing Shortcut Events
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,175 +89,7 @@ CharacterShortcutEvent underScoreToItalicEvent = CharacterShortcutEvent(
|
||||||
Check out the [complete code](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/example/lib/samples/underscore_to_italic.dart) file of this example.
|
Check out the [complete code](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/example/lib/samples/underscore_to_italic.dart) file of this example.
|
||||||
|
|
||||||
|
|
||||||
## Customizing a Component
|
## Customizing a Theme
|
||||||
|
|
||||||
> ⚠️ Notes: The content below is outdated.
|
|
||||||
|
|
||||||
We will use a simple example to show how to quickly add a custom component.
|
|
||||||
|
|
||||||
In this example we will render an image from the network.
|
|
||||||
|
|
||||||
Let's start with a blank document:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Container(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
child: AppFlowyEditor(
|
|
||||||
editorState: EditorState.empty(),
|
|
||||||
shortcutEvents: const [],
|
|
||||||
customBuilders: const {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, we will choose a unique string for your custom node's type.
|
|
||||||
|
|
||||||
We'll use `network_image` in this case. And we add `network_image_src` to the `attributes` to describe the link of the image.
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"type": "network_image",
|
|
||||||
"data": {
|
|
||||||
"network_image_src": "https://docs.flutter.dev/assets/images/dash/dash-fainting.gif"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, we create a class that inherits [NodeWidgetBuilder](../lib/src/service/render_plugin_service.dart). As shown in the autoprompt, we need to implement two functions:
|
|
||||||
1. one returns a widget
|
|
||||||
2. the other verifies the correctness of the [Node](../lib/src/core/document/node.dart).
|
|
||||||
|
|
||||||
|
|
||||||
```dart
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
|
|
||||||
@override
|
|
||||||
Widget build(NodeWidgetContext<Node> context) {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
NodeValidator<Node> get nodeValidator => throw UnimplementedError();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, let's implement a simple image widget based on `Image`.
|
|
||||||
|
|
||||||
Note that the `State` object that is returned by the `Widget` must implement [Selectable](../lib/src/render/selection/selectable.dart) using the `with` keyword.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
class _NetworkImageNodeWidget extends StatefulWidget {
|
|
||||||
const _NetworkImageNodeWidget({
|
|
||||||
Key? key,
|
|
||||||
required this.node,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final Node node;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_NetworkImageNodeWidget> createState() =>
|
|
||||||
__NetworkImageNodeWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class __NetworkImageNodeWidgetState extends State<_NetworkImageNodeWidget>
|
|
||||||
with SelectableMixin {
|
|
||||||
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Image.network(
|
|
||||||
widget.node.attributes['network_image_src'],
|
|
||||||
height: 200,
|
|
||||||
loadingBuilder: (context, child, loadingProgress) =>
|
|
||||||
loadingProgress == null ? child : const CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Position start() => Position(path: widget.node.path, offset: 0);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Position end() => Position(path: widget.node.path, offset: 1);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Position getPositionInOffset(Offset start) => end();
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Rect> getRectsInSelection(Selection selection) =>
|
|
||||||
[Offset.zero & _renderBox.size];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
|
|
||||||
path: widget.node.path,
|
|
||||||
startOffset: 0,
|
|
||||||
endOffset: 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally, we return `_NetworkImageNodeWidget` in the `build` function of `NetworkImageNodeWidgetBuilder`...
|
|
||||||
|
|
||||||
```dart
|
|
||||||
class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
|
|
||||||
@override
|
|
||||||
Widget build(NodeWidgetContext<Node> context) {
|
|
||||||
return _NetworkImageNodeWidget(
|
|
||||||
key: context.node.key,
|
|
||||||
node: context.node,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
NodeValidator<Node> get nodeValidator => (node) {
|
|
||||||
return node.type == 'network_image' &&
|
|
||||||
node.attributes['network_image_src'] is String;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
... and register `NetworkImageNodeWidgetBuilder` in the `AppFlowyEditor`.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
final editorState = EditorState(
|
|
||||||
document: StateTree.empty()
|
|
||||||
..insert(
|
|
||||||
[0],
|
|
||||||
[
|
|
||||||
TextNode.empty(),
|
|
||||||
Node.fromJson({
|
|
||||||
'type': 'network_image',
|
|
||||||
'attributes': {
|
|
||||||
'network_image_src':
|
|
||||||
'https://docs.flutter.dev/assets/images/dash/dash-fainting.gif'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return AppFlowyEditor(
|
|
||||||
editorState: editorState,
|
|
||||||
shortcutEvents: const [],
|
|
||||||
customBuilders: {
|
|
||||||
'network_image': NetworkImageNodeWidgetBuilder(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Check out the [complete code](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/example/lib/plugin/network_image_node_widget.dart) file of this example.
|
|
||||||
|
|
||||||
## Customizing a Theme (New Feature in 0.0.7)
|
|
||||||
|
|
||||||
We will use a simple example to illustrate how to quickly customize a theme.
|
We will use a simple example to illustrate how to quickly customize a theme.
|
||||||
|
|
||||||
|
|
@ -270,9 +102,7 @@ Widget build(BuildContext context) {
|
||||||
body: Container(
|
body: Container(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: AppFlowyEditor(
|
child: AppFlowyEditor(
|
||||||
editorState: EditorState.empty(),
|
editorState: EditorState.blank(),
|
||||||
shortcutEvents: const [],
|
|
||||||
customBuilders: const {},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -283,44 +113,118 @@ At this point, the editor looks like ...
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
Next, we will customize the `EditorStyle`.
|
Next, we will customize the `EditorStyle` and the block style with `BlockComponentConfiguration`.
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
ThemeData customizeEditorTheme(BuildContext context) {
|
EditorStyle customizeEditorStyle() {
|
||||||
final dark = EditorStyle.dark;
|
return EditorStyle(
|
||||||
final editorStyle = dark.copyWith(
|
padding: PlatformExtension.isDesktopOrWeb
|
||||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 150),
|
? const EdgeInsets.only(left: 100, right: 100, top: 20)
|
||||||
cursorColor: Colors.red.shade600,
|
: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
selectionColor: Colors.yellow.shade600.withOpacity(0.5),
|
cursorColor: Colors.green,
|
||||||
textStyle: GoogleFonts.poppins().copyWith(
|
selectionColor: Colors.green,
|
||||||
fontSize: 14,
|
textStyleConfiguration: TextStyleConfiguration(
|
||||||
color: Colors.white,
|
text: const TextStyle(
|
||||||
|
fontSize: 18.0,
|
||||||
|
color: Colors.white54,
|
||||||
|
),
|
||||||
|
bold: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
href: TextStyle(
|
||||||
|
color: Colors.amber,
|
||||||
|
decoration: TextDecoration.combine(
|
||||||
|
[
|
||||||
|
TextDecoration.overline,
|
||||||
|
TextDecoration.underline,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
code: const TextStyle(
|
||||||
|
fontSize: 14.0,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: Colors.blue,
|
||||||
|
backgroundColor: Colors.black12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
placeholderTextStyle: GoogleFonts.poppins().copyWith(
|
textSpanDecorator: (context, node, index, text, textSpan) {
|
||||||
fontSize: 14,
|
final attributes = text.attributes;
|
||||||
color: Colors.grey.shade400,
|
final href = attributes?[AppFlowyRichTextKeys.href];
|
||||||
),
|
if (href != null) {
|
||||||
code: dark.code?.copyWith(
|
return TextSpan(
|
||||||
backgroundColor: Colors.lightBlue.shade200,
|
text: text.text,
|
||||||
fontStyle: FontStyle.italic,
|
style: textSpan.style,
|
||||||
),
|
recognizer: TapGestureRecognizer()
|
||||||
highlightColorHex: '0x60FF0000', // red
|
..onTap = () {
|
||||||
|
debugPrint('onTap: $href');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return textSpan;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, BlockComponentBuilder> customBuilder() {
|
||||||
|
final configuration = BlockComponentConfiguration(
|
||||||
|
padding: (node) {
|
||||||
|
if (HeadingBlockKeys.type == node.type) {
|
||||||
|
return const EdgeInsets.symmetric(vertical: 30);
|
||||||
|
}
|
||||||
|
return const EdgeInsets.symmetric(vertical: 10);
|
||||||
|
},
|
||||||
|
textStyle: (node) {
|
||||||
|
if (HeadingBlockKeys.type == node.type) {
|
||||||
|
return const TextStyle(color: Colors.yellow);
|
||||||
|
}
|
||||||
|
return const TextStyle();
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final quote = QuotedTextPluginStyle.dark.copyWith(
|
// customize heading block style
|
||||||
textStyle: (_, __) => GoogleFonts.poppins().copyWith(
|
return {
|
||||||
fontSize: 14,
|
...standardBlockComponentBuilderMap,
|
||||||
color: Colors.blue.shade400,
|
// heading block
|
||||||
fontStyle: FontStyle.italic,
|
HeadingBlockKeys.type: HeadingBlockComponentBuilder(
|
||||||
fontWeight: FontWeight.w700,
|
configuration: configuration,
|
||||||
),
|
),
|
||||||
);
|
// todo-list block
|
||||||
|
TodoListBlockKeys.type: TodoListBlockComponentBuilder(
|
||||||
return Theme.of(context).copyWith(extensions: [
|
configuration: configuration,
|
||||||
editorStyle,
|
iconBuilder: (context, node) {
|
||||||
...darkPlguinStyleExtension,
|
final checked = node.attributes[TodoListBlockKeys.checked] as bool;
|
||||||
quote,
|
return Icon(
|
||||||
]);
|
checked ? Icons.check_box : Icons.check_box_outline_blank,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.white,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// bulleted list block
|
||||||
|
BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
|
||||||
|
configuration: configuration,
|
||||||
|
iconBuilder: (context, node) {
|
||||||
|
return const Icon(
|
||||||
|
Icons.circle,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.green,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// quote block
|
||||||
|
QuoteBlockKeys.type: QuoteBlockComponentBuilder(
|
||||||
|
configuration: configuration,
|
||||||
|
iconBuilder: (context, node) {
|
||||||
|
return const EditorSvg(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
padding: EdgeInsets.only(right: 5.0),
|
||||||
|
name: 'quote',
|
||||||
|
color: Colors.pink,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -333,72 +237,13 @@ Widget build(BuildContext context) {
|
||||||
body: Container(
|
body: Container(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: AppFlowyEditor(
|
child: AppFlowyEditor(
|
||||||
editorState: EditorState.empty(),
|
editorState: EditorState.blank(),
|
||||||
themeData: customizeEditorTheme(context),
|
editorStyle: customizeEditorStyle(),
|
||||||
shortcutEvents: const [],
|
blockComponentBuilders: customBuilder(),
|
||||||
customBuilders: const {},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Note:
|
|
||||||
|
|
||||||
`themeData` has since been depreciated, and you should now use `textStyleConfiguration`. If you would like to use dark mode, the following code will set the text colour to white:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
editorStyle: const EditorStyle.desktop().copyWith(
|
|
||||||
textStyleConfiguration: TextStyleConfiguration(
|
|
||||||
text: TextStyle(
|
|
||||||
color: Theme.of(context).primaryColorLight,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
The above example of `customizeEditorTheme` would turn into the following, which is how AppFlowy customises its editor style:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
EditorStyle desktop() {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final fontSize = context
|
|
||||||
.read<DocumentAppearanceCubit>()
|
|
||||||
.state
|
|
||||||
.fontSize;
|
|
||||||
return EditorStyle.desktop(
|
|
||||||
padding: padding,
|
|
||||||
backgroundColor: theme.colorScheme.surface,
|
|
||||||
cursorColor: theme.colorScheme.primary,
|
|
||||||
textStyleConfiguration: TextStyleConfiguration(
|
|
||||||
text: TextStyle(
|
|
||||||
fontFamily: 'Poppins',
|
|
||||||
fontSize: fontSize,
|
|
||||||
color: theme.colorScheme.onBackground,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
bold: const TextStyle(
|
|
||||||
fontFamily: 'Poppins-Bold',
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
italic: const TextStyle(fontStyle: FontStyle.italic),
|
|
||||||
underline: const TextStyle(decoration: TextDecoration.underline),
|
|
||||||
strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
|
|
||||||
href: TextStyle(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
code: GoogleFonts.robotoMono(
|
|
||||||
textStyle: TextStyle(
|
|
||||||
fontSize: fontSize,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
color: Colors.red,
|
|
||||||
backgroundColor: theme.colorScheme.inverseSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 607 KiB |
|
|
@ -128,8 +128,8 @@
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"italic": true,
|
"italic": true,
|
||||||
"bold": true,
|
"bold": true,
|
||||||
"textColor": "0xffD70040",
|
"font_color": "0xffD70040",
|
||||||
"highlightColor": "0x6000BCF0"
|
"bg_color": "0x6000BCF0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:example/pages/simple_editor.dart';
|
import 'package:example/pages/customize_theme_for_editor.dart';
|
||||||
|
import 'package:example/pages/editor.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -40,6 +41,7 @@ class HomePage extends StatefulWidget {
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage> {
|
class _HomePageState extends State<HomePage> {
|
||||||
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
late WidgetBuilder _widgetBuilder;
|
late WidgetBuilder _widgetBuilder;
|
||||||
late EditorState _editorState;
|
late EditorState _editorState;
|
||||||
late Future<String> _jsonString;
|
late Future<String> _jsonString;
|
||||||
|
|
@ -49,7 +51,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_jsonString = rootBundle.loadString('assets/example.json');
|
_jsonString = rootBundle.loadString('assets/example.json');
|
||||||
_widgetBuilder = (context) => SimpleEditor(
|
_widgetBuilder = (context) => Editor(
|
||||||
jsonString: _jsonString,
|
jsonString: _jsonString,
|
||||||
onEditorStateChange: (editorState) {
|
onEditorStateChange: (editorState) {
|
||||||
_editorState = editorState;
|
_editorState = editorState;
|
||||||
|
|
@ -61,7 +63,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
void reassemble() {
|
void reassemble() {
|
||||||
super.reassemble();
|
super.reassemble();
|
||||||
|
|
||||||
_widgetBuilder = (context) => SimpleEditor(
|
_widgetBuilder = (context) => Editor(
|
||||||
jsonString: _jsonString,
|
jsonString: _jsonString,
|
||||||
onEditorStateChange: (editorState) {
|
onEditorStateChange: (editorState) {
|
||||||
_editorState = editorState;
|
_editorState = editorState;
|
||||||
|
|
@ -130,6 +132,17 @@ class _HomePageState extends State<HomePage> {
|
||||||
_loadEditor(context, jsonString);
|
_loadEditor(context, jsonString);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Theme Demo
|
||||||
|
_buildSeparator(context, 'Theme Demo'),
|
||||||
|
_buildListTile(context, 'Custom Theme', () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const CustomizeThemeForEditor(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
// Encoder Demo
|
// Encoder Demo
|
||||||
_buildSeparator(context, 'Export To X Demo'),
|
_buildSeparator(context, 'Export To X Demo'),
|
||||||
_buildListTile(context, 'Export To JSON', () {
|
_buildListTile(context, 'Export To JSON', () {
|
||||||
|
|
@ -150,23 +163,6 @@ class _HomePageState extends State<HomePage> {
|
||||||
_buildListTile(context, 'Import From Quill Delta', () {
|
_buildListTile(context, 'Import From Quill Delta', () {
|
||||||
_importFile(ExportFileType.delta);
|
_importFile(ExportFileType.delta);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Theme Demo
|
|
||||||
_buildSeparator(context, 'Theme Demo'),
|
|
||||||
_buildListTile(context, 'Built In Dark Mode', () {
|
|
||||||
_jsonString = Future<String>.value(
|
|
||||||
jsonEncode(_editorState.document.toJson()).toString(),
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
}),
|
|
||||||
_buildListTile(context, 'Custom Theme', () {
|
|
||||||
_jsonString = Future<String>.value(
|
|
||||||
jsonEncode(_editorState.document.toJson()).toString(),
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
// todo: implement it
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -220,7 +216,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
_jsonString = jsonString;
|
_jsonString = jsonString;
|
||||||
setState(
|
setState(
|
||||||
() {
|
() {
|
||||||
_widgetBuilder = (context) => SimpleEditor(
|
_widgetBuilder = (context) => Editor(
|
||||||
jsonString: _jsonString,
|
jsonString: _jsonString,
|
||||||
onEditorStateChange: (editorState) {
|
onEditorStateChange: (editorState) {
|
||||||
_editorState = editorState;
|
_editorState = editorState;
|
||||||
|
|
|
||||||
179
example/lib/pages/customize_theme_for_editor.dart
Normal file
179
example/lib/pages/customize_theme_for_editor.dart
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class CustomizeThemeForEditor extends StatefulWidget {
|
||||||
|
const CustomizeThemeForEditor({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CustomizeThemeForEditor> createState() =>
|
||||||
|
_CustomizeThemeForEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomizeThemeForEditorState extends State<CustomizeThemeForEditor> {
|
||||||
|
late final Future<EditorState> editorState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
editorState = rootBundle.loadString('assets/example.json').then((value) {
|
||||||
|
return EditorState(
|
||||||
|
document: Document.fromJson(
|
||||||
|
Map<String, Object>.from(
|
||||||
|
json.decode(value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
title: const Text('Custom Theme For Editor'),
|
||||||
|
titleTextStyle: const TextStyle(color: Colors.white),
|
||||||
|
iconTheme: const IconThemeData(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: FutureBuilder(
|
||||||
|
future: editorState,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return !snapshot.hasData
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: buildEditor(snapshot.data!);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildEditor(EditorState editorState) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.black,
|
||||||
|
child: AppFlowyEditor(
|
||||||
|
editorState: editorState,
|
||||||
|
editorStyle: customizeEditorStyle(),
|
||||||
|
blockComponentBuilders: customBuilder(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// custom the block style
|
||||||
|
Map<String, BlockComponentBuilder> customBuilder() {
|
||||||
|
final configuration = BlockComponentConfiguration(
|
||||||
|
padding: (node) {
|
||||||
|
if (HeadingBlockKeys.type == node.type) {
|
||||||
|
return const EdgeInsets.symmetric(vertical: 30);
|
||||||
|
}
|
||||||
|
return const EdgeInsets.symmetric(vertical: 10);
|
||||||
|
},
|
||||||
|
textStyle: (node) {
|
||||||
|
if (HeadingBlockKeys.type == node.type) {
|
||||||
|
return const TextStyle(color: Colors.yellow);
|
||||||
|
}
|
||||||
|
return const TextStyle();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// customize heading block style
|
||||||
|
return {
|
||||||
|
...standardBlockComponentBuilderMap,
|
||||||
|
// heading block
|
||||||
|
HeadingBlockKeys.type: HeadingBlockComponentBuilder(
|
||||||
|
configuration: configuration,
|
||||||
|
),
|
||||||
|
// todo-list block
|
||||||
|
TodoListBlockKeys.type: TodoListBlockComponentBuilder(
|
||||||
|
configuration: configuration,
|
||||||
|
iconBuilder: (context, node) {
|
||||||
|
final checked = node.attributes[TodoListBlockKeys.checked] as bool;
|
||||||
|
return Icon(
|
||||||
|
checked ? Icons.check_box : Icons.check_box_outline_blank,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.white,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// bulleted list block
|
||||||
|
BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
|
||||||
|
configuration: configuration,
|
||||||
|
iconBuilder: (context, node) {
|
||||||
|
return const Icon(
|
||||||
|
Icons.circle,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.green,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// quote block
|
||||||
|
QuoteBlockKeys.type: QuoteBlockComponentBuilder(
|
||||||
|
configuration: configuration,
|
||||||
|
iconBuilder: (context, node) {
|
||||||
|
return const EditorSvg(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
padding: EdgeInsets.only(right: 5.0),
|
||||||
|
name: 'quote',
|
||||||
|
color: Colors.pink,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// custom the text style
|
||||||
|
EditorStyle customizeEditorStyle() {
|
||||||
|
return EditorStyle(
|
||||||
|
padding: PlatformExtension.isDesktopOrWeb
|
||||||
|
? const EdgeInsets.only(left: 100, right: 100, top: 20)
|
||||||
|
: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
cursorColor: Colors.green,
|
||||||
|
selectionColor: Colors.green,
|
||||||
|
textStyleConfiguration: TextStyleConfiguration(
|
||||||
|
text: const TextStyle(
|
||||||
|
fontSize: 18.0,
|
||||||
|
color: Colors.white54,
|
||||||
|
),
|
||||||
|
bold: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
href: TextStyle(
|
||||||
|
color: Colors.amber,
|
||||||
|
decoration: TextDecoration.combine(
|
||||||
|
[
|
||||||
|
TextDecoration.overline,
|
||||||
|
TextDecoration.underline,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
code: const TextStyle(
|
||||||
|
fontSize: 14.0,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: Colors.blue,
|
||||||
|
backgroundColor: Colors.black12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textSpanDecorator: (context, node, index, text, textSpan) {
|
||||||
|
final attributes = text.attributes;
|
||||||
|
final href = attributes?[AppFlowyRichTextKeys.href];
|
||||||
|
if (href != null) {
|
||||||
|
return TextSpan(
|
||||||
|
text: text.text,
|
||||||
|
style: textSpan.style,
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
debugPrint('onTap: $href');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return textSpan;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
130
example/lib/pages/editor.dart
Normal file
130
example/lib/pages/editor.dart
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class Editor extends StatelessWidget {
|
||||||
|
const Editor({
|
||||||
|
super.key,
|
||||||
|
required this.jsonString,
|
||||||
|
required this.onEditorStateChange,
|
||||||
|
this.editorStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Future<String> jsonString;
|
||||||
|
final EditorStyle? editorStyle;
|
||||||
|
final void Function(EditorState editorState) onEditorStateChange;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<String>(
|
||||||
|
future: jsonString,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData &&
|
||||||
|
snapshot.connectionState == ConnectionState.done) {
|
||||||
|
final editorState = EditorState(
|
||||||
|
document: Document.fromJson(
|
||||||
|
Map<String, Object>.from(
|
||||||
|
json.decode(snapshot.data!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
editorState.logConfiguration
|
||||||
|
..handler = debugPrint
|
||||||
|
..level = LogLevel.off;
|
||||||
|
onEditorStateChange(editorState);
|
||||||
|
final scrollController = ScrollController();
|
||||||
|
if (PlatformExtension.isDesktopOrWeb) {
|
||||||
|
return FloatingToolbar(
|
||||||
|
items: [
|
||||||
|
paragraphItem,
|
||||||
|
...headingItems,
|
||||||
|
...markdownFormatItems,
|
||||||
|
quoteItem,
|
||||||
|
bulletedListItem,
|
||||||
|
numberedListItem,
|
||||||
|
linkItem,
|
||||||
|
buildTextColorItem(),
|
||||||
|
buildHighlightColorItem()
|
||||||
|
],
|
||||||
|
editorState: editorState,
|
||||||
|
scrollController: scrollController,
|
||||||
|
child: _buildDesktopEditor(
|
||||||
|
context,
|
||||||
|
editorState,
|
||||||
|
scrollController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (PlatformExtension.isMobile) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildMobileEditor(
|
||||||
|
context,
|
||||||
|
editorState,
|
||||||
|
scrollController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MobileToolbar(
|
||||||
|
editorState: editorState,
|
||||||
|
toolbarItems: [
|
||||||
|
textDecorationMobileToolbarItem,
|
||||||
|
buildTextAndBackgroundColorMobileToolbarItem(),
|
||||||
|
headingMobileToolbarItem,
|
||||||
|
todoListMobileToolbarItem,
|
||||||
|
listMobileToolbarItem,
|
||||||
|
linkMobileToolbarItem,
|
||||||
|
quoteMobileToolbarItem,
|
||||||
|
dividerMobileToolbarItem,
|
||||||
|
codeMobileToolbarItem,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMobileEditor(
|
||||||
|
BuildContext context,
|
||||||
|
EditorState editorState,
|
||||||
|
ScrollController? scrollController,
|
||||||
|
) {
|
||||||
|
return AppFlowyEditor(
|
||||||
|
editorStyle: const EditorStyle.mobile(),
|
||||||
|
editorState: editorState,
|
||||||
|
scrollController: scrollController,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDesktopEditor(
|
||||||
|
BuildContext context,
|
||||||
|
EditorState editorState,
|
||||||
|
ScrollController? scrollController,
|
||||||
|
) {
|
||||||
|
final customBlockComponentBuilders = {
|
||||||
|
...standardBlockComponentBuilderMap,
|
||||||
|
ImageBlockKeys.type: ImageBlockComponentBuilder(
|
||||||
|
showMenu: true,
|
||||||
|
menuBuilder: (node, _) {
|
||||||
|
return const Positioned(
|
||||||
|
right: 10,
|
||||||
|
child: Text('Sample Menu'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
return AppFlowyEditor(
|
||||||
|
editorState: editorState,
|
||||||
|
scrollController: scrollController,
|
||||||
|
blockComponentBuilders: customBlockComponentBuilders,
|
||||||
|
commandShortcutEvents: standardCommandShortcutEvents,
|
||||||
|
characterShortcutEvents: standardCharacterShortcutEvents,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,9 +18,9 @@ export 'src/l10n/l10n.dart';
|
||||||
// plugins part, including decoder and encoder.
|
// plugins part, including decoder and encoder.
|
||||||
export 'src/plugins/plugins.dart';
|
export 'src/plugins/plugins.dart';
|
||||||
// legacy
|
// legacy
|
||||||
export 'src/render/rich_text/default_selectable.dart';
|
export 'src/editor/block_component/rich_text/default_selectable_mixin.dart';
|
||||||
export 'src/render/rich_text/flowy_rich_text.dart';
|
export 'src/editor/block_component/rich_text/appflowy_rich_text.dart';
|
||||||
export 'src/render/rich_text/flowy_rich_text_keys.dart';
|
export 'src/editor/block_component/rich_text/appflowy_rich_text_keys.dart';
|
||||||
export 'src/render/selection/selectable.dart';
|
export 'src/render/selection/selectable.dart';
|
||||||
export 'src/render/toolbar/toolbar_item.dart';
|
export 'src/render/toolbar/toolbar_item.dart';
|
||||||
export 'src/service/shortcut_event/key_mapping.dart';
|
export 'src/service/shortcut_event/key_mapping.dart';
|
||||||
|
|
|
||||||
|
|
@ -681,7 +681,7 @@ extension on Delta {
|
||||||
final attributes = slice(index - 1, index).first.attributes;
|
final attributes = slice(index - 1, index).first.attributes;
|
||||||
if (attributes == null ||
|
if (attributes == null ||
|
||||||
!attributes.keys.every(
|
!attributes.keys.every(
|
||||||
(element) => FlowyRichTextKeys.supportSliced.contains(element),
|
(element) => AppFlowyRichTextKeys.supportSliced.contains(element),
|
||||||
)) {
|
)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,25 @@ class TextStyleConfiguration {
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// default text style
|
||||||
final TextStyle text;
|
final TextStyle text;
|
||||||
|
|
||||||
|
/// bold text style
|
||||||
final TextStyle bold;
|
final TextStyle bold;
|
||||||
|
|
||||||
|
/// italic text style
|
||||||
final TextStyle italic;
|
final TextStyle italic;
|
||||||
|
|
||||||
|
/// underline text style
|
||||||
final TextStyle underline;
|
final TextStyle underline;
|
||||||
|
|
||||||
|
/// strikethrough text style
|
||||||
final TextStyle strikethrough;
|
final TextStyle strikethrough;
|
||||||
|
|
||||||
|
/// href text style
|
||||||
final TextStyle href;
|
final TextStyle href;
|
||||||
|
|
||||||
|
/// code text style
|
||||||
final TextStyle code;
|
final TextStyle code;
|
||||||
|
|
||||||
TextStyleConfiguration copyWith({
|
TextStyleConfiguration copyWith({
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ export 'base_component/outdent_command.dart';
|
||||||
export 'base_component/widget/full_screen_overlay_entry.dart';
|
export 'base_component/widget/full_screen_overlay_entry.dart';
|
||||||
export 'base_component/widget/ignore_parent_pointer.dart';
|
export 'base_component/widget/ignore_parent_pointer.dart';
|
||||||
|
|
||||||
|
// rich text
|
||||||
|
export 'rich_text/default_selectable_mixin.dart';
|
||||||
|
export 'rich_text/appflowy_rich_text.dart';
|
||||||
|
export 'rich_text/appflowy_rich_text_keys.dart';
|
||||||
|
|
||||||
export 'base_component/block_component_configuration.dart';
|
export 'base_component/block_component_configuration.dart';
|
||||||
export 'base_component/text_style_configuration.dart';
|
export 'base_component/text_style_configuration.dart';
|
||||||
export 'base_component/background_color_mixin.dart';
|
export 'base_component/background_color_mixin.dart';
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ class _BulletedListBlockComponentWidgetState
|
||||||
extends State<BulletedListBlockComponentWidget>
|
extends State<BulletedListBlockComponentWidget>
|
||||||
with
|
with
|
||||||
SelectableMixin,
|
SelectableMixin,
|
||||||
DefaultSelectable,
|
DefaultSelectableMixin,
|
||||||
BlockComponentConfigurable,
|
BlockComponentConfigurable,
|
||||||
BackgroundColorMixin,
|
BackgroundColorMixin,
|
||||||
NestedBlockComponentStatefulWidgetMixin {
|
NestedBlockComponentStatefulWidgetMixin {
|
||||||
|
|
@ -107,7 +107,7 @@ class _BulletedListBlockComponentWidgetState
|
||||||
textStyle: textStyle,
|
textStyle: textStyle,
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: FlowyRichText(
|
child: AppFlowyRichText(
|
||||||
key: forwardKey,
|
key: forwardKey,
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ class _HeadingBlockComponentWidgetState
|
||||||
extends State<HeadingBlockComponentWidget>
|
extends State<HeadingBlockComponentWidget>
|
||||||
with
|
with
|
||||||
SelectableMixin,
|
SelectableMixin,
|
||||||
DefaultSelectable,
|
DefaultSelectableMixin,
|
||||||
BlockComponentConfigurable,
|
BlockComponentConfigurable,
|
||||||
BackgroundColorMixin {
|
BackgroundColorMixin {
|
||||||
@override
|
@override
|
||||||
|
|
@ -113,7 +113,7 @@ class _HeadingBlockComponentWidgetState
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget child = Container(
|
Widget child = Container(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
child: FlowyRichText(
|
child: AppFlowyRichText(
|
||||||
key: forwardKey,
|
key: forwardKey,
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ class _NumberedListBlockComponentWidgetState
|
||||||
extends State<NumberedListBlockComponentWidget>
|
extends State<NumberedListBlockComponentWidget>
|
||||||
with
|
with
|
||||||
SelectableMixin,
|
SelectableMixin,
|
||||||
DefaultSelectable,
|
DefaultSelectableMixin,
|
||||||
BlockComponentConfigurable,
|
BlockComponentConfigurable,
|
||||||
BackgroundColorMixin,
|
BackgroundColorMixin,
|
||||||
NestedBlockComponentStatefulWidgetMixin {
|
NestedBlockComponentStatefulWidgetMixin {
|
||||||
|
|
@ -114,7 +114,7 @@ class _NumberedListBlockComponentWidgetState
|
||||||
textStyle: textStyle,
|
textStyle: textStyle,
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: FlowyRichText(
|
child: AppFlowyRichText(
|
||||||
key: forwardKey,
|
key: forwardKey,
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ class QuoteBlockComponentWidget extends BlockComponentStatefulWidget {
|
||||||
class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
||||||
with
|
with
|
||||||
SelectableMixin,
|
SelectableMixin,
|
||||||
DefaultSelectable,
|
DefaultSelectableMixin,
|
||||||
BlockComponentConfigurable,
|
BlockComponentConfigurable,
|
||||||
BackgroundColorMixin {
|
BackgroundColorMixin {
|
||||||
@override
|
@override
|
||||||
|
|
@ -106,7 +106,7 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
||||||
? widget.iconBuilder!(context, node)
|
? widget.iconBuilder!(context, node)
|
||||||
: const _QuoteIcon(),
|
: const _QuoteIcon(),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: FlowyRichText(
|
child: AppFlowyRichText(
|
||||||
key: forwardKey,
|
key: forwardKey,
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
|
|
||||||
394
lib/src/editor/block_component/rich_text/appflowy_rich_text.dart
Normal file
394
lib/src/editor/block_component/rich_text/appflowy_rich_text.dart
Normal file
|
|
@ -0,0 +1,394 @@
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/src/core/document/attributes.dart';
|
||||||
|
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||||
|
import 'package:appflowy_editor/src/core/document/text_delta.dart';
|
||||||
|
import 'package:appflowy_editor/src/core/location/position.dart';
|
||||||
|
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor_state.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart';
|
||||||
|
import 'package:appflowy_editor/src/render/selection/selectable.dart';
|
||||||
|
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/util/color_util.dart';
|
||||||
|
import 'package:appflowy_editor/src/core/document/path.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
typedef TextSpanDecoratorForAttribute = InlineSpan Function(
|
||||||
|
BuildContext context,
|
||||||
|
Node node,
|
||||||
|
int index,
|
||||||
|
TextInsert text,
|
||||||
|
TextSpan textSpan,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef AppFlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan);
|
||||||
|
|
||||||
|
class AppFlowyRichText extends StatefulWidget {
|
||||||
|
const AppFlowyRichText({
|
||||||
|
super.key,
|
||||||
|
this.cursorHeight,
|
||||||
|
this.cursorWidth = 1.5,
|
||||||
|
this.lineHeight,
|
||||||
|
this.textSpanDecorator,
|
||||||
|
this.placeholderText = ' ',
|
||||||
|
this.placeholderTextSpanDecorator,
|
||||||
|
this.textSpanDecoratorForCustomAttributes,
|
||||||
|
required this.node,
|
||||||
|
required this.editorState,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The node of the rich text.
|
||||||
|
final Node node;
|
||||||
|
|
||||||
|
/// The editor state.
|
||||||
|
final EditorState editorState;
|
||||||
|
|
||||||
|
/// The height of the cursor.
|
||||||
|
///
|
||||||
|
/// If this is null, the height of the cursor will be calculated automatically.
|
||||||
|
final double? cursorHeight;
|
||||||
|
|
||||||
|
/// The width of the cursor.
|
||||||
|
final double cursorWidth;
|
||||||
|
|
||||||
|
/// The height of each line.
|
||||||
|
final double? lineHeight;
|
||||||
|
|
||||||
|
/// customize the text span for rich text
|
||||||
|
final AppFlowyTextSpanDecorator? textSpanDecorator;
|
||||||
|
|
||||||
|
/// The placeholder text when the rich text is empty.
|
||||||
|
final String placeholderText;
|
||||||
|
|
||||||
|
/// customize the text span for placeholder text
|
||||||
|
final AppFlowyTextSpanDecorator? placeholderTextSpanDecorator;
|
||||||
|
|
||||||
|
/// customize the text span for custom attributes
|
||||||
|
///
|
||||||
|
/// You can use this to customize the text span for custom attributes
|
||||||
|
/// or override the existing one.
|
||||||
|
final TextSpanDecoratorForAttribute? textSpanDecoratorForCustomAttributes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppFlowyRichText> createState() => _AppFlowyRichTextState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
||||||
|
with SelectableMixin {
|
||||||
|
final textKey = GlobalKey();
|
||||||
|
final placeholderTextKey = GlobalKey();
|
||||||
|
|
||||||
|
RenderParagraph get _renderParagraph =>
|
||||||
|
textKey.currentContext?.findRenderObject() as RenderParagraph;
|
||||||
|
|
||||||
|
RenderParagraph? get _placeholderRenderParagraph =>
|
||||||
|
placeholderTextKey.currentContext?.findRenderObject() as RenderParagraph?;
|
||||||
|
|
||||||
|
TextSpanDecoratorForAttribute? get textSpanDecoratorForAttribute =>
|
||||||
|
widget.textSpanDecoratorForCustomAttributes ??
|
||||||
|
widget.editorState.editorStyle.textSpanDecorator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.text,
|
||||||
|
child: widget.node.delta?.toPlainText().isEmpty ?? true
|
||||||
|
? Stack(
|
||||||
|
children: [
|
||||||
|
_buildPlaceholderText(context),
|
||||||
|
_buildRichText(context),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: _buildRichText(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Position start() => Position(path: widget.node.path, offset: 0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Position end() => Position(
|
||||||
|
path: widget.node.path,
|
||||||
|
offset: widget.node.delta?.toPlainText().length ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Rect? getCursorRectInPosition(Position position) {
|
||||||
|
final textPosition = TextPosition(offset: position.offset);
|
||||||
|
|
||||||
|
var cursorHeight = _renderParagraph.getFullHeightForCaret(textPosition);
|
||||||
|
var cursorOffset =
|
||||||
|
_renderParagraph.getOffsetForCaret(textPosition, Rect.zero);
|
||||||
|
if (cursorHeight == null) {
|
||||||
|
cursorHeight =
|
||||||
|
_placeholderRenderParagraph?.getFullHeightForCaret(textPosition);
|
||||||
|
cursorOffset = _placeholderRenderParagraph?.getOffsetForCaret(
|
||||||
|
textPosition,
|
||||||
|
Rect.zero,
|
||||||
|
) ??
|
||||||
|
Offset.zero;
|
||||||
|
}
|
||||||
|
if (widget.cursorHeight != null && cursorHeight != null) {
|
||||||
|
cursorOffset = Offset(
|
||||||
|
cursorOffset.dx,
|
||||||
|
cursorOffset.dy + (cursorHeight - widget.cursorHeight!) / 2,
|
||||||
|
);
|
||||||
|
cursorHeight = widget.cursorHeight;
|
||||||
|
}
|
||||||
|
final rect = Rect.fromLTWH(
|
||||||
|
cursorOffset.dx - (widget.cursorWidth / 2.0),
|
||||||
|
cursorOffset.dy,
|
||||||
|
widget.cursorWidth,
|
||||||
|
cursorHeight ?? 16.0,
|
||||||
|
);
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Position getPositionInOffset(Offset start) {
|
||||||
|
final offset = _renderParagraph.globalToLocal(start);
|
||||||
|
final baseOffset = _renderParagraph.getPositionForOffset(offset).offset;
|
||||||
|
return Position(path: widget.node.path, offset: baseOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Selection? getWordBoundaryInOffset(Offset offset) {
|
||||||
|
final localOffset = _renderParagraph.globalToLocal(offset);
|
||||||
|
final textPosition = _renderParagraph.getPositionForOffset(localOffset);
|
||||||
|
final textRange = _renderParagraph.getWordBoundary(textPosition);
|
||||||
|
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
|
||||||
|
Selection? getWordBoundaryInPosition(Position position) {
|
||||||
|
final textPosition = TextPosition(offset: position.offset);
|
||||||
|
final textRange = _renderParagraph.getWordBoundary(textPosition);
|
||||||
|
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) {
|
||||||
|
final textSelection = textSelectionFromEditorSelection(selection);
|
||||||
|
if (textSelection == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final rects = _renderParagraph
|
||||||
|
.getBoxesForSelection(textSelection, boxHeightStyle: BoxHeightStyle.max)
|
||||||
|
.map((box) => box.toRect())
|
||||||
|
.toList(growable: false);
|
||||||
|
if (rects.isEmpty) {
|
||||||
|
// If the rich text widget does not contain any text,
|
||||||
|
// there will be no selection boxes,
|
||||||
|
// so we need to return to the default selection.
|
||||||
|
return [Rect.fromLTWH(0, 0, 0, _renderParagraph.size.height)];
|
||||||
|
}
|
||||||
|
return rects;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Selection getSelectionInRange(Offset start, Offset end) {
|
||||||
|
final localStart = _renderParagraph.globalToLocal(start);
|
||||||
|
final localEnd = _renderParagraph.globalToLocal(end);
|
||||||
|
final baseOffset = _renderParagraph.getPositionForOffset(localStart).offset;
|
||||||
|
final extentOffset = _renderParagraph.getPositionForOffset(localEnd).offset;
|
||||||
|
return Selection.single(
|
||||||
|
path: widget.node.path,
|
||||||
|
startOffset: baseOffset,
|
||||||
|
endOffset: extentOffset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Offset localToGlobal(Offset offset) {
|
||||||
|
return _renderParagraph.localToGlobal(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPlaceholderText(BuildContext context) {
|
||||||
|
final textSpan = getPlaceholderTextSpan();
|
||||||
|
return RichText(
|
||||||
|
key: placeholderTextKey,
|
||||||
|
textHeightBehavior: const TextHeightBehavior(
|
||||||
|
applyHeightToFirstAscent: false,
|
||||||
|
applyHeightToLastDescent: false,
|
||||||
|
),
|
||||||
|
text: widget.placeholderTextSpanDecorator != null
|
||||||
|
? widget.placeholderTextSpanDecorator!(textSpan)
|
||||||
|
: textSpan,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRichText(BuildContext context) {
|
||||||
|
final textSpan = getTextSpan();
|
||||||
|
return RichText(
|
||||||
|
key: textKey,
|
||||||
|
textHeightBehavior: const TextHeightBehavior(
|
||||||
|
applyHeightToFirstAscent: false,
|
||||||
|
applyHeightToLastDescent: false,
|
||||||
|
),
|
||||||
|
text: widget.textSpanDecorator != null
|
||||||
|
? widget.textSpanDecorator!(textSpan)
|
||||||
|
: textSpan,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextSpan getPlaceholderTextSpan() {
|
||||||
|
final style = widget.editorState.editorStyle.textStyleConfiguration;
|
||||||
|
return TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: widget.placeholderText,
|
||||||
|
style: style.text.copyWith(height: widget.lineHeight),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextSpan getTextSpan() {
|
||||||
|
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;
|
||||||
|
if (attributes != null) {
|
||||||
|
if (attributes.bold == true) {
|
||||||
|
textStyle = textStyle.combine(style.bold);
|
||||||
|
}
|
||||||
|
if (attributes.italic == true) {
|
||||||
|
textStyle = textStyle.combine(style.italic);
|
||||||
|
}
|
||||||
|
if (attributes.underline == true) {
|
||||||
|
textStyle = textStyle.combine(style.underline);
|
||||||
|
}
|
||||||
|
if (attributes.strikethrough == true) {
|
||||||
|
textStyle = textStyle.combine(style.strikethrough);
|
||||||
|
}
|
||||||
|
if (attributes.href != null) {
|
||||||
|
textStyle = textStyle.combine(style.href);
|
||||||
|
}
|
||||||
|
if (attributes.code == true) {
|
||||||
|
textStyle = textStyle.combine(style.code);
|
||||||
|
}
|
||||||
|
if (attributes.backgroundColor != null) {
|
||||||
|
textStyle = textStyle.combine(
|
||||||
|
TextStyle(backgroundColor: attributes.backgroundColor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (attributes.color != null) {
|
||||||
|
textStyle = textStyle.combine(
|
||||||
|
TextStyle(color: attributes.color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final textSpan = TextSpan(
|
||||||
|
text: textInsert.text,
|
||||||
|
style: textStyle,
|
||||||
|
);
|
||||||
|
textSpans.add(
|
||||||
|
textSpanDecoratorForAttribute != null
|
||||||
|
? textSpanDecoratorForAttribute!(
|
||||||
|
context,
|
||||||
|
widget.node,
|
||||||
|
offset,
|
||||||
|
textInsert,
|
||||||
|
textSpan,
|
||||||
|
)
|
||||||
|
: textSpan,
|
||||||
|
);
|
||||||
|
offset += textInsert.length;
|
||||||
|
}
|
||||||
|
return TextSpan(
|
||||||
|
children: textSpans,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextSelection? textSelectionFromEditorSelection(Selection? selection) {
|
||||||
|
if (selection == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalized = selection.normalized;
|
||||||
|
final path = widget.node.path;
|
||||||
|
if (path < normalized.start.path || path > normalized.end.path) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final length = widget.node.delta?.length;
|
||||||
|
if (length == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextSelection? textSelection;
|
||||||
|
|
||||||
|
if (normalized.isSingle) {
|
||||||
|
if (path.equals(normalized.start.path)) {
|
||||||
|
if (normalized.isCollapsed) {
|
||||||
|
textSelection = TextSelection.collapsed(
|
||||||
|
offset: normalized.startIndex,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
textSelection = TextSelection(
|
||||||
|
baseOffset: normalized.startIndex,
|
||||||
|
extentOffset: normalized.endIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (path.equals(normalized.start.path)) {
|
||||||
|
textSelection = TextSelection(
|
||||||
|
baseOffset: normalized.startIndex,
|
||||||
|
extentOffset: length,
|
||||||
|
);
|
||||||
|
} else if (path.equals(normalized.end.path)) {
|
||||||
|
textSelection = TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: normalized.endIndex,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
textSelection = TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return textSelection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FlowyRichTextAttributes on Attributes {
|
||||||
|
bool get bold => this[AppFlowyRichTextKeys.bold] == true;
|
||||||
|
|
||||||
|
bool get italic => this[AppFlowyRichTextKeys.italic] == true;
|
||||||
|
|
||||||
|
bool get underline => this[AppFlowyRichTextKeys.underline] == true;
|
||||||
|
|
||||||
|
bool get code => this[AppFlowyRichTextKeys.code] == true;
|
||||||
|
|
||||||
|
bool get strikethrough {
|
||||||
|
return (containsKey(AppFlowyRichTextKeys.strikethrough) &&
|
||||||
|
this[AppFlowyRichTextKeys.strikethrough] == true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color? get color {
|
||||||
|
final textColor = this[AppFlowyRichTextKeys.textColor] as String?;
|
||||||
|
return textColor?.toColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
Color? get backgroundColor {
|
||||||
|
final highlightColor = this[AppFlowyRichTextKeys.highlightColor] as String?;
|
||||||
|
return highlightColor?.toColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get href {
|
||||||
|
if (this[AppFlowyRichTextKeys.href] is String) {
|
||||||
|
return this[AppFlowyRichTextKeys.href];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
class AppFlowyRichTextKeys {
|
||||||
|
static String bold = 'bold';
|
||||||
|
static String italic = 'italic';
|
||||||
|
static String underline = 'underline';
|
||||||
|
static String strikethrough = 'strikethrough';
|
||||||
|
static String textColor = 'font_color';
|
||||||
|
static String highlightColor = 'bg_color';
|
||||||
|
static String code = 'code';
|
||||||
|
static String href = 'href';
|
||||||
|
|
||||||
|
static List<String> supportSliced = [
|
||||||
|
bold,
|
||||||
|
italic,
|
||||||
|
underline,
|
||||||
|
strikethrough,
|
||||||
|
textColor,
|
||||||
|
highlightColor,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
mixin DefaultSelectableMixin {
|
||||||
|
GlobalKey get forwardKey;
|
||||||
|
GlobalKey get containerKey;
|
||||||
|
|
||||||
|
SelectableMixin<StatefulWidget> get forward =>
|
||||||
|
forwardKey.currentState as SelectableMixin;
|
||||||
|
|
||||||
|
Offset get baseOffset {
|
||||||
|
final parentBox = containerKey.currentContext?.findRenderObject();
|
||||||
|
final childBox = forwardKey.currentContext?.findRenderObject();
|
||||||
|
if (parentBox is RenderBox && childBox is RenderBox) {
|
||||||
|
return childBox.localToGlobal(Offset.zero, ancestor: parentBox);
|
||||||
|
}
|
||||||
|
return Offset.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
Position getPositionInOffset(Offset start) =>
|
||||||
|
forward.getPositionInOffset(start);
|
||||||
|
|
||||||
|
Rect? getCursorRectInPosition(Position position) =>
|
||||||
|
forward.getCursorRectInPosition(position)?.shift(baseOffset);
|
||||||
|
|
||||||
|
List<Rect> getRectsInSelection(Selection selection) => forward
|
||||||
|
.getRectsInSelection(selection)
|
||||||
|
.map((rect) => rect.shift(baseOffset))
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
Selection getSelectionInRange(Offset start, Offset end) =>
|
||||||
|
forward.getSelectionInRange(start, end);
|
||||||
|
|
||||||
|
Offset localToGlobal(Offset offset) =>
|
||||||
|
forward.localToGlobal(offset) - baseOffset;
|
||||||
|
|
||||||
|
Selection? getWordBoundaryInOffset(Offset offset) =>
|
||||||
|
forward.getWordBoundaryInOffset(offset);
|
||||||
|
|
||||||
|
Selection? getWordBoundaryInPosition(Position position) =>
|
||||||
|
forward.getWordBoundaryInPosition(position);
|
||||||
|
|
||||||
|
Position start() => forward.start();
|
||||||
|
|
||||||
|
Position end() => forward.end();
|
||||||
|
}
|
||||||
|
|
@ -75,7 +75,7 @@ class TextBlockComponentWidget extends BlockComponentStatefulWidget {
|
||||||
class _TextBlockComponentWidgetState extends State<TextBlockComponentWidget>
|
class _TextBlockComponentWidgetState extends State<TextBlockComponentWidget>
|
||||||
with
|
with
|
||||||
SelectableMixin,
|
SelectableMixin,
|
||||||
DefaultSelectable,
|
DefaultSelectableMixin,
|
||||||
BlockComponentConfigurable,
|
BlockComponentConfigurable,
|
||||||
BackgroundColorMixin,
|
BackgroundColorMixin,
|
||||||
NestedBlockComponentStatefulWidgetMixin {
|
NestedBlockComponentStatefulWidgetMixin {
|
||||||
|
|
@ -95,7 +95,7 @@ class _TextBlockComponentWidgetState extends State<TextBlockComponentWidget>
|
||||||
Widget buildComponent(BuildContext context) {
|
Widget buildComponent(BuildContext context) {
|
||||||
Widget child = Container(
|
Widget child = Container(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
child: FlowyRichText(
|
child: AppFlowyRichText(
|
||||||
key: forwardKey,
|
key: forwardKey,
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ class _TodoListBlockComponentWidgetState
|
||||||
extends State<TodoListBlockComponentWidget>
|
extends State<TodoListBlockComponentWidget>
|
||||||
with
|
with
|
||||||
SelectableMixin,
|
SelectableMixin,
|
||||||
DefaultSelectable,
|
DefaultSelectableMixin,
|
||||||
BlockComponentConfigurable,
|
BlockComponentConfigurable,
|
||||||
BackgroundColorMixin,
|
BackgroundColorMixin,
|
||||||
NestedBlockComponentStatefulWidgetMixin {
|
NestedBlockComponentStatefulWidgetMixin {
|
||||||
|
|
@ -126,7 +126,7 @@ class _TodoListBlockComponentWidgetState
|
||||||
onTap: checkOrUncheck,
|
onTap: checkOrUncheck,
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: FlowyRichText(
|
child: AppFlowyRichText(
|
||||||
key: forwardKey,
|
key: forwardKey,
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue