diff --git a/README.md b/README.md index a65fd937d..1cab4caa2 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Start by creating a new empty AppFlowyEditor object. ```dart final editorState = EditorState.blank(withInitialText: true); // with an empty paragraph -final editor = AppFlowyEditor.standard( +final editor = AppFlowyEditor( editorState: editorState, ); ``` @@ -66,7 +66,7 @@ You can also create an editor from a JSON object in order to configure your init ```dart final json = jsonDecode('YOUR INPUT JSON STRING'); final editorState = EditorState(document: Document.fromJson(json)); -final editor = AppFlowyEditor.standard( +final editor = AppFlowyEditor( editorState: editorState, ); ``` @@ -90,6 +90,12 @@ flutter run ## 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 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 * [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 diff --git a/documentation/customizing.md b/documentation/customizing.md index 95df046c3..0cadda9ef 100644 --- a/documentation/customizing.md +++ b/documentation/customizing.md @@ -89,13 +89,9 @@ 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. -## 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. +We will use a simple example to illustrate how to quickly customize a theme. Let's start with a blank document: @@ -106,221 +102,129 @@ Widget build(BuildContext context) { body: Container( alignment: Alignment.topCenter, child: AppFlowyEditor( - editorState: EditorState.empty(), - shortcutEvents: const [], - customBuilders: const {}, + editorState: EditorState.blank(), ), ), ); } ``` -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 context) { - throw UnimplementedError(); - } - - @override - NodeValidator 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 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 context) { - return _NetworkImageNodeWidget( - key: context.node.key, - node: context.node, - ); - } - - @override - NodeValidator 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(), - }, -); -``` - -![Whew!](./images/customize_a_component.gif) - -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) +At this point, the editor looks like ... +![Before](./images/customizing_a_theme_before.png) -We will use a simple example to illustrate how to quickly customize a theme. -Let's start with a blank document: +Next, we will customize the `EditorStyle` and the block style with `BlockComponentConfiguration`. ```dart -@override -Widget build(BuildContext context) { - return Scaffold( - body: Container( - alignment: Alignment.topCenter, - child: AppFlowyEditor( - editorState: EditorState.empty(), - shortcutEvents: const [], - customBuilders: const {}, +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; + }, ); } -``` -At this point, the editor looks like ... -![Before](./images/customizing_a_theme_before.png) - - -Next, we will customize the `EditorStyle`. +Map 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(); + }, + ); -```dart -ThemeData customizeEditorTheme(BuildContext context) { - final dark = EditorStyle.dark; - final editorStyle = dark.copyWith( - padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 150), - cursorColor: Colors.red.shade600, - selectionColor: Colors.yellow.shade600.withOpacity(0.5), - textStyle: GoogleFonts.poppins().copyWith( - fontSize: 14, - color: Colors.white, + // customize heading block style + return { + ...standardBlockComponentBuilderMap, + // heading block + HeadingBlockKeys.type: HeadingBlockComponentBuilder( + configuration: configuration, ), - placeholderTextStyle: GoogleFonts.poppins().copyWith( - fontSize: 14, - color: Colors.grey.shade400, + // 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, + ); + }, ), - code: dark.code?.copyWith( - backgroundColor: Colors.lightBlue.shade200, - fontStyle: FontStyle.italic, + // bulleted list block + BulletedListBlockKeys.type: BulletedListBlockComponentBuilder( + configuration: configuration, + iconBuilder: (context, node) { + return const Icon( + Icons.circle, + size: 20, + color: Colors.green, + ); + }, ), - highlightColorHex: '0x60FF0000', // red - ); - - final quote = QuotedTextPluginStyle.dark.copyWith( - textStyle: (_, __) => GoogleFonts.poppins().copyWith( - fontSize: 14, - color: Colors.blue.shade400, - fontStyle: FontStyle.italic, - fontWeight: FontWeight.w700, + // 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, + ); + }, ), - ); - - return Theme.of(context).copyWith(extensions: [ - editorStyle, - ...darkPlguinStyleExtension, - quote, - ]); + }; } ``` @@ -333,72 +237,13 @@ Widget build(BuildContext context) { body: Container( alignment: Alignment.topCenter, child: AppFlowyEditor( - editorState: EditorState.empty(), - themeData: customizeEditorTheme(context), - shortcutEvents: const [], - customBuilders: const {}, + editorState: EditorState.blank(), + editorStyle: customizeEditorStyle(), + blockComponentBuilders: customBuilder(), ), ), ); } ``` -![After](./images/customizing_a_theme_after.png) - -### 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() - .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, - ), - ), - ), - ); -} -``` +![After](./images/customizing_a_theme_after.png) \ No newline at end of file diff --git a/documentation/images/customizing_a_theme_after.png b/documentation/images/customizing_a_theme_after.png index 389082922..56b4a5c4b 100644 Binary files a/documentation/images/customizing_a_theme_after.png and b/documentation/images/customizing_a_theme_after.png differ diff --git a/example/assets/example.json b/example/assets/example.json index dac41c33f..64291636f 100644 --- a/example/assets/example.json +++ b/example/assets/example.json @@ -128,8 +128,8 @@ "attributes": { "italic": true, "bold": true, - "textColor": "0xffD70040", - "highlightColor": "0x6000BCF0" + "font_color": "0xffD70040", + "bg_color": "0x6000BCF0" } }, { diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index 84e8bdbe2..f00b8bcb1 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -3,7 +3,8 @@ import 'dart:convert'; import 'dart:io'; 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:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -40,6 +41,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { final _scaffoldKey = GlobalKey(); + late WidgetBuilder _widgetBuilder; late EditorState _editorState; late Future _jsonString; @@ -49,7 +51,7 @@ class _HomePageState extends State { super.initState(); _jsonString = rootBundle.loadString('assets/example.json'); - _widgetBuilder = (context) => SimpleEditor( + _widgetBuilder = (context) => Editor( jsonString: _jsonString, onEditorStateChange: (editorState) { _editorState = editorState; @@ -61,7 +63,7 @@ class _HomePageState extends State { void reassemble() { super.reassemble(); - _widgetBuilder = (context) => SimpleEditor( + _widgetBuilder = (context) => Editor( jsonString: _jsonString, onEditorStateChange: (editorState) { _editorState = editorState; @@ -130,6 +132,17 @@ class _HomePageState extends State { _loadEditor(context, jsonString); }), + // Theme Demo + _buildSeparator(context, 'Theme Demo'), + _buildListTile(context, 'Custom Theme', () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CustomizeThemeForEditor(), + ), + ); + }), + // Encoder Demo _buildSeparator(context, 'Export To X Demo'), _buildListTile(context, 'Export To JSON', () { @@ -150,23 +163,6 @@ class _HomePageState extends State { _buildListTile(context, 'Import From Quill Delta', () { _importFile(ExportFileType.delta); }), - - // Theme Demo - _buildSeparator(context, 'Theme Demo'), - _buildListTile(context, 'Built In Dark Mode', () { - _jsonString = Future.value( - jsonEncode(_editorState.document.toJson()).toString(), - ); - setState(() {}); - }), - _buildListTile(context, 'Custom Theme', () { - _jsonString = Future.value( - jsonEncode(_editorState.document.toJson()).toString(), - ); - setState(() { - // todo: implement it - }); - }), ], ), ); @@ -220,7 +216,7 @@ class _HomePageState extends State { _jsonString = jsonString; setState( () { - _widgetBuilder = (context) => SimpleEditor( + _widgetBuilder = (context) => Editor( jsonString: _jsonString, onEditorStateChange: (editorState) { _editorState = editorState; diff --git a/example/lib/pages/customize_theme_for_editor.dart b/example/lib/pages/customize_theme_for_editor.dart new file mode 100644 index 000000000..fab9c3e13 --- /dev/null +++ b/example/lib/pages/customize_theme_for_editor.dart @@ -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 createState() => + _CustomizeThemeForEditorState(); +} + +class _CustomizeThemeForEditorState extends State { + late final Future editorState; + + @override + void initState() { + super.initState(); + + editorState = rootBundle.loadString('assets/example.json').then((value) { + return EditorState( + document: Document.fromJson( + Map.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 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; + }, + ); + } +} diff --git a/example/lib/pages/simple_editor.dart b/example/lib/pages/editor.dart similarity index 96% rename from example/lib/pages/simple_editor.dart rename to example/lib/pages/editor.dart index e8ae6f3fc..3e3da6bcf 100644 --- a/example/lib/pages/simple_editor.dart +++ b/example/lib/pages/editor.dart @@ -3,8 +3,8 @@ import 'dart:convert'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; -class SimpleEditor extends StatelessWidget { - const SimpleEditor({ +class Editor extends StatelessWidget { + const Editor({ super.key, required this.jsonString, required this.onEditorStateChange, @@ -95,7 +95,7 @@ class SimpleEditor extends StatelessWidget { EditorState editorState, ScrollController? scrollController, ) { - return AppFlowyEditor.standard( + return AppFlowyEditor( editorStyle: const EditorStyle.mobile(), editorState: editorState, scrollController: scrollController, @@ -119,7 +119,7 @@ class SimpleEditor extends StatelessWidget { }, ) }; - return AppFlowyEditor.custom( + return AppFlowyEditor( editorState: editorState, scrollController: scrollController, blockComponentBuilders: customBlockComponentBuilders, diff --git a/lib/appflowy_editor.dart b/lib/appflowy_editor.dart index 066ec5fc8..459e8ceb6 100644 --- a/lib/appflowy_editor.dart +++ b/lib/appflowy_editor.dart @@ -18,9 +18,9 @@ export 'src/l10n/l10n.dart'; // plugins part, including decoder and encoder. export 'src/plugins/plugins.dart'; // legacy -export 'src/render/rich_text/default_selectable.dart'; -export 'src/render/rich_text/flowy_rich_text.dart'; -export 'src/render/rich_text/flowy_rich_text_keys.dart'; +export 'src/editor/block_component/rich_text/default_selectable_mixin.dart'; +export 'src/editor/block_component/rich_text/appflowy_rich_text.dart'; +export 'src/editor/block_component/rich_text/appflowy_rich_text_keys.dart'; export 'src/render/selection/selectable.dart'; export 'src/render/toolbar/toolbar_item.dart'; export 'src/service/shortcut_event/key_mapping.dart'; diff --git a/lib/src/core/transform/transaction.dart b/lib/src/core/transform/transaction.dart index 164483679..8d54b840d 100644 --- a/lib/src/core/transform/transaction.dart +++ b/lib/src/core/transform/transaction.dart @@ -681,7 +681,7 @@ extension on Delta { final attributes = slice(index - 1, index).first.attributes; if (attributes == null || !attributes.keys.every( - (element) => FlowyRichTextKeys.supportSliced.contains(element), + (element) => AppFlowyRichTextKeys.supportSliced.contains(element), )) { return null; } diff --git a/lib/src/editor/block_component/base_component/text_style_configuration.dart b/lib/src/editor/block_component/base_component/text_style_configuration.dart index b2da813f4..3dfaa4e86 100644 --- a/lib/src/editor/block_component/base_component/text_style_configuration.dart +++ b/lib/src/editor/block_component/base_component/text_style_configuration.dart @@ -22,12 +22,25 @@ class TextStyleConfiguration { ), }); + /// default text style final TextStyle text; + + /// bold text style final TextStyle bold; + + /// italic text style final TextStyle italic; + + /// underline text style final TextStyle underline; + + /// strikethrough text style final TextStyle strikethrough; + + /// href text style final TextStyle href; + + /// code text style final TextStyle code; TextStyleConfiguration copyWith({ diff --git a/lib/src/editor/block_component/block_component.dart b/lib/src/editor/block_component/block_component.dart index 764fddf85..c7fd90058 100644 --- a/lib/src/editor/block_component/block_component.dart +++ b/lib/src/editor/block_component/block_component.dart @@ -40,6 +40,11 @@ export 'base_component/outdent_command.dart'; export 'base_component/widget/full_screen_overlay_entry.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/text_style_configuration.dart'; export 'base_component/background_color_mixin.dart'; diff --git a/lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart b/lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart index 080f6cdfa..3a9fa0069 100644 --- a/lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart +++ b/lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart @@ -75,7 +75,7 @@ class _BulletedListBlockComponentWidgetState extends State with SelectableMixin, - DefaultSelectable, + DefaultSelectableMixin, BlockComponentConfigurable, BackgroundColorMixin, NestedBlockComponentStatefulWidgetMixin { @@ -107,7 +107,7 @@ class _BulletedListBlockComponentWidgetState textStyle: textStyle, ), Flexible( - child: FlowyRichText( + child: AppFlowyRichText( key: forwardKey, node: widget.node, editorState: editorState, diff --git a/lib/src/editor/block_component/heading_block_component/heading_block_component.dart b/lib/src/editor/block_component/heading_block_component/heading_block_component.dart index 8a13d4553..645a9cabc 100644 --- a/lib/src/editor/block_component/heading_block_component/heading_block_component.dart +++ b/lib/src/editor/block_component/heading_block_component/heading_block_component.dart @@ -90,7 +90,7 @@ class _HeadingBlockComponentWidgetState extends State with SelectableMixin, - DefaultSelectable, + DefaultSelectableMixin, BlockComponentConfigurable, BackgroundColorMixin { @override @@ -113,7 +113,7 @@ class _HeadingBlockComponentWidgetState Widget build(BuildContext context) { Widget child = Container( color: backgroundColor, - child: FlowyRichText( + child: AppFlowyRichText( key: forwardKey, node: widget.node, editorState: editorState, diff --git a/lib/src/editor/block_component/numbered_list_block_component/numbered_list_block_component.dart b/lib/src/editor/block_component/numbered_list_block_component/numbered_list_block_component.dart index ddc32d968..fd929774e 100644 --- a/lib/src/editor/block_component/numbered_list_block_component/numbered_list_block_component.dart +++ b/lib/src/editor/block_component/numbered_list_block_component/numbered_list_block_component.dart @@ -82,7 +82,7 @@ class _NumberedListBlockComponentWidgetState extends State with SelectableMixin, - DefaultSelectable, + DefaultSelectableMixin, BlockComponentConfigurable, BackgroundColorMixin, NestedBlockComponentStatefulWidgetMixin { @@ -114,7 +114,7 @@ class _NumberedListBlockComponentWidgetState textStyle: textStyle, ), Flexible( - child: FlowyRichText( + child: AppFlowyRichText( key: forwardKey, node: widget.node, editorState: editorState, diff --git a/lib/src/editor/block_component/quote_block_component/quote_block_component.dart b/lib/src/editor/block_component/quote_block_component/quote_block_component.dart index 412f009ab..8ed5f91db 100644 --- a/lib/src/editor/block_component/quote_block_component/quote_block_component.dart +++ b/lib/src/editor/block_component/quote_block_component/quote_block_component.dart @@ -75,7 +75,7 @@ class QuoteBlockComponentWidget extends BlockComponentStatefulWidget { class _QuoteBlockComponentWidgetState extends State with SelectableMixin, - DefaultSelectable, + DefaultSelectableMixin, BlockComponentConfigurable, BackgroundColorMixin { @override @@ -106,7 +106,7 @@ class _QuoteBlockComponentWidgetState extends State ? widget.iconBuilder!(context, node) : const _QuoteIcon(), Flexible( - child: FlowyRichText( + child: AppFlowyRichText( key: forwardKey, node: widget.node, editorState: editorState, diff --git a/lib/src/render/rich_text/flowy_rich_text.dart b/lib/src/editor/block_component/rich_text/appflowy_rich_text.dart similarity index 70% rename from lib/src/render/rich_text/flowy_rich_text.dart rename to lib/src/editor/block_component/rich_text/appflowy_rich_text.dart index 6fe016aa0..0dbd72ecd 100644 --- a/lib/src/render/rich_text/flowy_rich_text.dart +++ b/lib/src/editor/block_component/rich_text/appflowy_rich_text.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:ui'; import 'package:appflowy_editor/src/core/document/attributes.dart'; @@ -6,30 +5,28 @@ 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/toolbar/toolbar.dart'; import 'package:appflowy_editor/src/editor_state.dart'; -import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; -import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text_keys.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/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -typedef TextSpanDecoratorForCustomAttributes = InlineSpan Function( +typedef TextSpanDecoratorForAttribute = InlineSpan Function( + BuildContext context, Node node, int index, TextInsert text, TextSpan textSpan, ); -typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); +typedef AppFlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); -class FlowyRichText extends StatefulWidget { - const FlowyRichText({ - Key? key, +class AppFlowyRichText extends StatefulWidget { + const AppFlowyRichText({ + super.key, this.cursorHeight, this.cursorWidth = 1.5, this.lineHeight, @@ -39,53 +36,72 @@ class FlowyRichText extends StatefulWidget { this.textSpanDecoratorForCustomAttributes, required this.node, required this.editorState, - }) : super(key: key); + }); + /// 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; - final FlowyTextSpanDecorator? textSpanDecorator; + + /// customize the text span for rich text + final AppFlowyTextSpanDecorator? textSpanDecorator; + + /// The placeholder text when the rich text is empty. final String placeholderText; - final FlowyTextSpanDecorator? placeholderTextSpanDecorator; - final TextSpanDecoratorForCustomAttributes? - textSpanDecoratorForCustomAttributes; + + /// 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 createState() => _FlowyRichTextState(); + State createState() => _AppFlowyRichTextState(); } -class _FlowyRichTextState extends State with SelectableMixin { - var _textKey = GlobalKey(); - final _placeholderTextKey = GlobalKey(); +class _AppFlowyRichTextState extends State + with SelectableMixin { + final textKey = GlobalKey(); + final placeholderTextKey = GlobalKey(); RenderParagraph get _renderParagraph => - _textKey.currentContext?.findRenderObject() as RenderParagraph; + textKey.currentContext?.findRenderObject() as RenderParagraph; RenderParagraph? get _placeholderRenderParagraph => - _placeholderTextKey.currentContext?.findRenderObject() - as RenderParagraph?; - - TextSpanDecoratorForCustomAttributes? - get textSpanDecoratorForCustomAttributes => - widget.textSpanDecoratorForCustomAttributes ?? - widget.editorState.editorStyle.textSpanDecorator; - - @override - void didUpdateWidget(covariant FlowyRichText oldWidget) { - super.didUpdateWidget(oldWidget); + placeholderTextKey.currentContext?.findRenderObject() as RenderParagraph?; - // https://github.com/flutter/flutter/issues/110342 - if (_textKey.currentWidget is RichText) { - // Force refresh the RichText widget. - _textKey = GlobalKey(); - } - } + TextSpanDecoratorForAttribute? get textSpanDecoratorForAttribute => + widget.textSpanDecoratorForCustomAttributes ?? + widget.editorState.editorStyle.textSpanDecorator; @override Widget build(BuildContext context) { - return _buildRichText(context); + return MouseRegion( + cursor: SystemMouseCursors.text, + child: widget.node.delta?.toPlainText().isEmpty ?? true + ? Stack( + children: [ + _buildPlaceholderText(context), + _buildRichText(context), + ], + ) + : _buildRichText(context), + ); } @override @@ -192,24 +208,10 @@ class _FlowyRichTextState extends State with SelectableMixin { return _renderParagraph.localToGlobal(offset); } - Widget _buildRichText(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.text, - child: widget.node.delta?.toPlainText().isEmpty ?? true - ? Stack( - children: [ - _buildPlaceholderText(context), - _buildSingleRichText(context), - ], - ) - : _buildSingleRichText(context), - ); - } - Widget _buildPlaceholderText(BuildContext context) { - final textSpan = _placeholderTextSpan; + final textSpan = getPlaceholderTextSpan(); return RichText( - key: _placeholderTextKey, + key: placeholderTextKey, textHeightBehavior: const TextHeightBehavior( applyHeightToFirstAscent: false, applyHeightToLastDescent: false, @@ -220,10 +222,10 @@ class _FlowyRichTextState extends State with SelectableMixin { ); } - Widget _buildSingleRichText(BuildContext context) { - final textSpan = _textSpan; + Widget _buildRichText(BuildContext context) { + final textSpan = getTextSpan(); return RichText( - key: _textKey, + key: textKey, textHeightBehavior: const TextHeightBehavior( applyHeightToFirstAscent: false, applyHeightToLastDescent: false, @@ -234,7 +236,7 @@ class _FlowyRichTextState extends State with SelectableMixin { ); } - TextSpan get _placeholderTextSpan { + TextSpan getPlaceholderTextSpan() { final style = widget.editorState.editorStyle.textStyleConfiguration; return TextSpan( children: [ @@ -246,14 +248,13 @@ class _FlowyRichTextState extends State with SelectableMixin { ); } - TextSpan get _textSpan { - var offset = 0; + TextSpan getTextSpan() { + int offset = 0; List textSpans = []; final style = widget.editorState.editorStyle.textStyleConfiguration; final textInserts = widget.node.delta!.whereType(); for (final textInsert in textInserts) { - var textStyle = style.text.copyWith(height: widget.lineHeight); - GestureRecognizer? recognizer; + TextStyle textStyle = style.text.copyWith(height: widget.lineHeight); final attributes = textInsert.attributes; if (attributes != null) { if (attributes.bold == true) { @@ -270,14 +271,6 @@ class _FlowyRichTextState extends State with SelectableMixin { } if (attributes.href != null) { textStyle = textStyle.combine(style.href); - recognizer = _buildTapHrefGestureRecognizer( - attributes.href!, - Selection.single( - path: widget.node.path, - startOffset: offset, - endOffset: offset + textInsert.length, - ), - ); } if (attributes.code == true) { textStyle = textStyle.combine(style.code); @@ -296,11 +289,11 @@ class _FlowyRichTextState extends State with SelectableMixin { final textSpan = TextSpan( text: textInsert.text, style: textStyle, - recognizer: recognizer, ); textSpans.add( - textSpanDecoratorForCustomAttributes != null - ? textSpanDecoratorForCustomAttributes!( + textSpanDecoratorForAttribute != null + ? textSpanDecoratorForAttribute!( + context, widget.node, offset, textInsert, @@ -315,36 +308,6 @@ class _FlowyRichTextState extends State with SelectableMixin { ); } - GestureRecognizer _buildTapHrefGestureRecognizer( - String href, - Selection selection, - ) { - Timer? timer; - var tapCount = 0; - final tapGestureRecognizer = TapGestureRecognizer() - ..onTap = () async { - // implement a simple double tap logic - tapCount += 1; - timer?.cancel(); - - if (tapCount == 2 || !widget.editorState.editable) { - tapCount = 0; - safeLaunchUrl(href); - return; - } - - timer = Timer(const Duration(milliseconds: 200), () { - tapCount = 0; - widget.editorState.service.selectionService - .updateSelection(selection); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - showLinkMenu(context, widget.editorState, selection, true); - }); - }); - }; - return tapGestureRecognizer; - } - TextSelection? textSelectionFromEditorSelection(Selection? selection) { if (selection == null) { return null; @@ -399,32 +362,32 @@ class _FlowyRichTextState extends State with SelectableMixin { } extension FlowyRichTextAttributes on Attributes { - bool get bold => this[FlowyRichTextKeys.bold] == true; + bool get bold => this[AppFlowyRichTextKeys.bold] == true; - bool get italic => this[FlowyRichTextKeys.italic] == true; + bool get italic => this[AppFlowyRichTextKeys.italic] == true; - bool get underline => this[FlowyRichTextKeys.underline] == true; + bool get underline => this[AppFlowyRichTextKeys.underline] == true; - bool get code => this[FlowyRichTextKeys.code] == true; + bool get code => this[AppFlowyRichTextKeys.code] == true; bool get strikethrough { - return (containsKey(FlowyRichTextKeys.strikethrough) && - this[FlowyRichTextKeys.strikethrough] == true); + return (containsKey(AppFlowyRichTextKeys.strikethrough) && + this[AppFlowyRichTextKeys.strikethrough] == true); } Color? get color { - final textColor = this[FlowyRichTextKeys.textColor] as String?; + final textColor = this[AppFlowyRichTextKeys.textColor] as String?; return textColor?.toColor(); } Color? get backgroundColor { - final highlightColor = this[FlowyRichTextKeys.highlightColor] as String?; + final highlightColor = this[AppFlowyRichTextKeys.highlightColor] as String?; return highlightColor?.toColor(); } String? get href { - if (this[FlowyRichTextKeys.href] is String) { - return this[FlowyRichTextKeys.href]; + if (this[AppFlowyRichTextKeys.href] is String) { + return this[AppFlowyRichTextKeys.href]; } return null; } diff --git a/lib/src/render/rich_text/flowy_rich_text_keys.dart b/lib/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart similarity index 93% rename from lib/src/render/rich_text/flowy_rich_text_keys.dart rename to lib/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart index 18e9b8100..76c9fb2ae 100644 --- a/lib/src/render/rich_text/flowy_rich_text_keys.dart +++ b/lib/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart @@ -1,4 +1,4 @@ -class FlowyRichTextKeys { +class AppFlowyRichTextKeys { static String bold = 'bold'; static String italic = 'italic'; static String underline = 'underline'; diff --git a/lib/src/render/rich_text/default_selectable.dart b/lib/src/editor/block_component/rich_text/default_selectable_mixin.dart similarity index 98% rename from lib/src/render/rich_text/default_selectable.dart rename to lib/src/editor/block_component/rich_text/default_selectable_mixin.dart index bceafe84a..466f1947f 100644 --- a/lib/src/render/rich_text/default_selectable.dart +++ b/lib/src/editor/block_component/rich_text/default_selectable_mixin.dart @@ -3,7 +3,7 @@ import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; -mixin DefaultSelectable { +mixin DefaultSelectableMixin { GlobalKey get forwardKey; GlobalKey get containerKey; diff --git a/lib/src/editor/block_component/text_block_component/text_block_component.dart b/lib/src/editor/block_component/text_block_component/text_block_component.dart index 1682da66c..843d0b16e 100644 --- a/lib/src/editor/block_component/text_block_component/text_block_component.dart +++ b/lib/src/editor/block_component/text_block_component/text_block_component.dart @@ -75,7 +75,7 @@ class TextBlockComponentWidget extends BlockComponentStatefulWidget { class _TextBlockComponentWidgetState extends State with SelectableMixin, - DefaultSelectable, + DefaultSelectableMixin, BlockComponentConfigurable, BackgroundColorMixin, NestedBlockComponentStatefulWidgetMixin { @@ -95,7 +95,7 @@ class _TextBlockComponentWidgetState extends State Widget buildComponent(BuildContext context) { Widget child = Container( color: backgroundColor, - child: FlowyRichText( + child: AppFlowyRichText( key: forwardKey, node: widget.node, editorState: editorState, diff --git a/lib/src/editor/block_component/todo_list_block_component/todo_list_block_component.dart b/lib/src/editor/block_component/todo_list_block_component/todo_list_block_component.dart index 887a9787e..79f00b16e 100644 --- a/lib/src/editor/block_component/todo_list_block_component/todo_list_block_component.dart +++ b/lib/src/editor/block_component/todo_list_block_component/todo_list_block_component.dart @@ -92,7 +92,7 @@ class _TodoListBlockComponentWidgetState extends State with SelectableMixin, - DefaultSelectable, + DefaultSelectableMixin, BlockComponentConfigurable, BackgroundColorMixin, NestedBlockComponentStatefulWidgetMixin { @@ -126,7 +126,7 @@ class _TodoListBlockComponentWidgetState onTap: checkOrUncheck, ), Flexible( - child: FlowyRichText( + child: AppFlowyRichText( key: forwardKey, node: widget.node, editorState: editorState, diff --git a/lib/src/editor/editor_component/service/editor.dart b/lib/src/editor/editor_component/service/editor.dart index 59be225b0..e2f110d95 100644 --- a/lib/src/editor/editor_component/service/editor.dart +++ b/lib/src/editor/editor_component/service/editor.dart @@ -13,102 +13,98 @@ import 'package:provider/provider.dart'; ValueNotifier keepEditorFocusNotifier = ValueNotifier(0); class AppFlowyEditor extends StatefulWidget { - @Deprecated('Use AppFlowyEditor.custom or AppFlowyEditor.standard instead') - const AppFlowyEditor({ + AppFlowyEditor({ super.key, required this.editorState, - this.blockComponentBuilders = const {}, - this.characterShortcutEvents = const [], - this.commandShortcutEvents = const [], - this.selectionMenuItems = const [], - this.toolbarItems = const [], + Map? blockComponentBuilders, + List? characterShortcutEvents, + List? commandShortcutEvents, this.editable = true, this.autoFocus = false, this.focusedSelection, this.shrinkWrap = false, this.scrollController, - this.themeData, this.editorStyle = const EditorStyle.desktop(), this.header, this.footer, this.focusNode, - }); - - const AppFlowyEditor.custom({ - Key? key, - required EditorState editorState, - ScrollController? scrollController, - bool editable = true, - bool autoFocus = false, - Selection? focusedSelection, - EditorStyle? editorStyle, - Map blockComponentBuilders = const {}, - List characterShortcutEvents = const [], - List commandShortcutEvents = const [], - List selectionMenuItems = const [], - Widget? header, - Widget? footer, - FocusNode? focusNode, - bool shrinkWrap = false, - }) : this( - key: key, - editorState: editorState, - scrollController: scrollController, - editable: editable, - autoFocus: autoFocus, - focusedSelection: focusedSelection, - blockComponentBuilders: blockComponentBuilders, - characterShortcutEvents: characterShortcutEvents, - commandShortcutEvents: commandShortcutEvents, - selectionMenuItems: selectionMenuItems, - editorStyle: editorStyle ?? const EditorStyle.desktop(), - header: header, - footer: footer, - focusNode: focusNode, - shrinkWrap: shrinkWrap, - ); - - AppFlowyEditor.standard({ - Key? key, - required EditorState editorState, - ScrollController? scrollController, - bool editable = true, - bool autoFocus = false, - Selection? focusedSelection, - EditorStyle? editorStyle, - Widget? header, - Widget? footer, - FocusNode? focusNode, - bool shrinkWrap = false, - }) : this( - key: key, - editorState: editorState, - scrollController: scrollController, - editable: editable, - autoFocus: autoFocus, - focusedSelection: focusedSelection, - blockComponentBuilders: standardBlockComponentBuilderMap, - characterShortcutEvents: standardCharacterShortcutEvents, - commandShortcutEvents: standardCommandShortcutEvents, - editorStyle: editorStyle ?? const EditorStyle.desktop(), - header: header, - footer: footer, - focusNode: focusNode, - shrinkWrap: shrinkWrap, - ); + }) : blockComponentBuilders = + blockComponentBuilders ?? standardBlockComponentBuilderMap, + characterShortcutEvents = + characterShortcutEvents ?? standardCharacterShortcutEvents, + commandShortcutEvents = + commandShortcutEvents ?? standardCommandShortcutEvents; final EditorState editorState; final EditorStyle editorStyle; + /// Block component builders + /// + /// Pass the [standardBlockComponentBuilderMap] as well + /// if you simply want to extend it with a new one. + /// + /// For example, if you want to add a new block component: + /// + /// ```dart + /// AppFlowyEditor( + /// blockComponentBuilders: { + /// ...standardBlockComponentBuilderMap, + /// 'my_block_component': MyBlockComponentBuilder(), + /// }, + /// ); + /// ``` + /// + /// Also, you can override the standard block component: + /// + /// ```dart + /// AppFlowyEditor( + /// blockComponentBuilders: { + /// ...standardBlockComponentBuilderMap, + /// 'paragraph': MyParagraphBlockComponentBuilder(), + /// }, + /// ); + /// ``` final Map blockComponentBuilders; /// Character event handlers + /// + /// Pass the [standardCharacterShortcutEvents] as well + /// if you simply want to extend it with a new one. + /// + /// For example, if you want to add a new character shortcut event: + /// + /// ```dart + /// AppFlowyEditor( + /// characterShortcutEvents: [ + /// ...standardCharacterShortcutEvents, + /// [YOUR_SHORTCUT_EVENT], + /// ], + /// ); + /// ``` final List characterShortcutEvents; - // Command event handlers + /// Command event handlers + /// + /// Pass the [standardCommandShortcutEvents] as well + /// if you simply want to extend it with a new one. + /// + /// For example, if you want to add a new command shortcut event: + /// + /// ```dart + /// AppFlowyEditor( + /// commandShortcutEvents: [ + /// ...standardCommandShortcutEvents, + /// [YOUR_SHORTCUT_EVENT], + /// ], + /// ); + /// ``` final List commandShortcutEvents; + /// Provide a scrollController to control the scroll behavior + /// if you need to custom the scroll behavior. + /// + /// Otherwise, the editor will create a scrollController inside. final ScrollController? scrollController; /// Set the value to false to disable editing. @@ -117,25 +113,26 @@ class AppFlowyEditor extends StatefulWidget { /// Set the value to true to focus the editor on the start of the document. final bool autoFocus; + /// Set the value to focus the editor on the specified selection. + /// + /// only works when [autoFocus] is true. final Selection? focusedSelection; + final FocusNode? focusNode; + + /// AppFlowy Editor use column as the root widget. + /// + /// You can provide a header and/or a footer to the editor. final Widget? header; final Widget? footer; - final FocusNode? focusNode; - /// if true, the editor will be sized to its contents. + /// + /// You should wrap the editor with a sized widget if you set this value to true. + /// + /// Notes: Must provide a scrollController when shrinkWrap is true. final bool shrinkWrap; - @Deprecated('Use FloatingToolbar or MobileToolbar instead.') - final List toolbarItems; - - @Deprecated('Customize the style that block component provides instead.') - final ThemeData? themeData; - - @Deprecated('Use customSlashCommand instead') - final List selectionMenuItems; - @override State createState() => _AppFlowyEditorState(); } @@ -156,7 +153,6 @@ class _AppFlowyEditorState extends State { } editorState.editorStyle = widget.editorStyle; - editorState.selectionMenuItems = widget.selectionMenuItems; editorState.renderer = _renderer; editorState.editable = widget.editable; @@ -166,6 +162,11 @@ class _AppFlowyEditorState extends State { }); } + @override + void dispose() { + super.dispose(); + } + @override void didUpdateWidget(covariant AppFlowyEditor oldWidget) { super.didUpdateWidget(oldWidget); @@ -174,7 +175,6 @@ class _AppFlowyEditorState extends State { editorState.editable = widget.editable; if (editorState.service != oldWidget.editorState.service) { - editorState.selectionMenuItems = widget.selectionMenuItems; editorState.renderer = _renderer; } @@ -192,7 +192,7 @@ class _AppFlowyEditorState extends State { initialEntries: [ OverlayEntry( builder: (context) => services!, - ), + ) ], ), ), diff --git a/lib/src/editor/editor_component/service/editor_service.dart b/lib/src/editor/editor_component/service/editor_service.dart index c5a58e019..80546f137 100644 --- a/lib/src/editor/editor_component/service/editor_service.dart +++ b/lib/src/editor/editor_component/service/editor_service.dart @@ -1,7 +1,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/editor_component/service/toolbar_service.dart'; - -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Overlay, OverlayEntry, OverlayState; class EditorService { // selection service diff --git a/lib/src/editor/editor_component/service/scroll_service_widget.dart b/lib/src/editor/editor_component/service/scroll_service_widget.dart index e7dfb8ac3..99bb8d99c 100644 --- a/lib/src/editor/editor_component/service/scroll_service_widget.dart +++ b/lib/src/editor/editor_component/service/scroll_service_widget.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scrollable_widget.dart'; import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scroller.dart'; @@ -70,7 +68,7 @@ class _ScrollServiceWidgetState extends State builder: ((context, autoScroller) { if (PlatformExtension.isDesktopOrWeb) { return _buildDesktopScrollService(context, autoScroller); - } else if (Platform.isIOS || Platform.isAndroid) { + } else if (PlatformExtension.isMobile) { return _buildMobileScrollService(context, autoScroller); } throw UnimplementedError(); diff --git a/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart b/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart index d76db1c0d..39cf61dc9 100644 --- a/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart +++ b/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart @@ -479,7 +479,10 @@ class _DesktopSelectionServiceWidgetState } } - Overlay.of(context)?.insertAll(_selectionAreas); + final overlay = Overlay.of(context); + overlay?.insertAll( + _selectionAreas, + ); } void _updateCursorAreas(Position position) { diff --git a/lib/src/editor/editor_component/service/selection/mobile_selection_service.dart b/lib/src/editor/editor_component/service/selection/mobile_selection_service.dart index 77d840b46..3f21e29fb 100644 --- a/lib/src/editor/editor_component/service/selection/mobile_selection_service.dart +++ b/lib/src/editor/editor_component/service/selection/mobile_selection_service.dart @@ -451,7 +451,10 @@ class _MobileSelectionServiceWidgetState } } - Overlay.of(context)?.insertAll(_selectionAreas); + final overlay = Overlay.of(context); + overlay?.insertAll( + _selectionAreas, + ); } void _updateCursorAreas(Position position) { diff --git a/lib/src/editor/editor_component/service/toolbar_service.dart b/lib/src/editor/editor_component/service/toolbar_service.dart index 3e6e59ace..9b77189ca 100644 --- a/lib/src/editor/editor_component/service/toolbar_service.dart +++ b/lib/src/editor/editor_component/service/toolbar_service.dart @@ -1,9 +1,5 @@ -import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:flutter/material.dart' hide Overlay, OverlayEntry; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; - abstract class AppFlowyToolbarService { /// Show the toolbar widget beside the offset. void showInOffset( @@ -18,111 +14,3 @@ abstract class AppFlowyToolbarService { /// Trigger the specified handler. bool triggerHandler(String id); } - -class FlowyToolbar extends StatefulWidget { - const FlowyToolbar({ - Key? key, - required this.editorState, - required this.child, - required this.showDefaultToolbar, - }) : super(key: key); - final bool showDefaultToolbar; - final EditorState editorState; - final Widget child; - - @override - State createState() => _FlowyToolbarState(); -} - -class _FlowyToolbarState extends State - implements AppFlowyToolbarService { - OverlayEntry? _toolbarOverlay; - final _toolbarWidgetKey = GlobalKey(debugLabel: '_toolbar_widget'); - late final List toolbarItems; - - @override - void initState() { - super.initState(); - - toolbarItems = widget.showDefaultToolbar - ? [...defaultToolbarItems, ...widget.editorState.toolbarItems] - : [...widget.editorState.toolbarItems] - ..sort((a, b) => a.type.compareTo(b.type)); - } - - @override - void showInOffset( - Offset offset, - Alignment alignment, - LayerLink layerLink, - ) { - hide(); - final items = _filterItems(toolbarItems); - if (items.isEmpty) { - return; - } - _toolbarOverlay = OverlayEntry( - builder: (context) => ToolbarWidget( - key: _toolbarWidgetKey, - editorState: widget.editorState, - layerLink: layerLink, - offset: offset, - items: items, - alignment: alignment, - ), - ); - Overlay.of(context)?.insert(_toolbarOverlay!); - } - - @override - void hide() { - _toolbarWidgetKey.currentState?.unwrapOrNull()?.hide(); - _toolbarOverlay?.remove(); - _toolbarOverlay = null; - } - - @override - bool triggerHandler(String id) { - final items = toolbarItems.where((item) => item.id == id); - if (items.length != 1) { - assert(items.length == 1, 'The toolbar item\'s id must be unique'); - return false; - } - items.first.handler?.call(widget.editorState, context); - return true; - } - - @override - Widget build(BuildContext context) { - return Container( - child: widget.child, - ); - } - - @override - void dispose() { - hide(); - - super.dispose(); - } - - // Filter items that should not be displayed, sort according to type, - // and insert dividers between different types. - List _filterItems(List items) { - final filterItems = items - .where((item) => item.validator?.call(widget.editorState) ?? false) - .toList(growable: false) - ..sort((a, b) => a.type.compareTo(b.type)); - if (filterItems.isEmpty) { - return []; - } - final List dividedItems = [filterItems.first]; - for (var i = 1; i < filterItems.length; i++) { - if (filterItems[i].type != filterItems[i - 1].type) { - dividedItems.add(ToolbarItem.divider()); - } - dividedItems.add(filterItems[i]); - } - return dividedItems; - } -} diff --git a/lib/src/editor/editor_component/style/editor_style.dart b/lib/src/editor/editor_component/style/editor_style.dart index 336d60bd4..da9766c56 100644 --- a/lib/src/editor/editor_component/style/editor_style.dart +++ b/lib/src/editor/editor_component/style/editor_style.dart @@ -1,115 +1,58 @@ +import 'dart:async'; + import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; - -Iterable> get lightEditorStyleExtension => [ - EditorStyle.light, - ]; - -Iterable> get darkEditorStyleExtension => [ - EditorStyle.dark, - ]; - -class EditorStyle extends ThemeExtension { - // Editor styles - final EdgeInsets? padding; - - final Color cursorColor; - final Color selectionColor; - final TextStyleConfiguration textStyleConfiguration; - final TextSpanDecoratorForCustomAttributes? textSpanDecorator; - - // @Deprecated('customize the editor\'s background color directly') - final Color? backgroundColor; - - // Text styles - @Deprecated('customize the block component directly') - final EdgeInsets? textPadding; - @Deprecated('customize the block component directly') - final TextStyle? textStyle; - @Deprecated('customize the block component directly') - final TextStyle? placeholderTextStyle; - @Deprecated('customize the block component directly') - final double lineHeight; - - // Rich text styles - @Deprecated('customize the text style configuration directly') - final TextStyle? bold; - @Deprecated('customize the text style configuration directly') - final TextStyle? italic; - @Deprecated('customize the text style configuration directly') - final TextStyle? underline; - @Deprecated('customize the text style configuration directly') - final TextStyle? strikethrough; - @Deprecated('customize the text style configuration directly') - final TextStyle? href; - @Deprecated('customize the text style configuration directly') - final TextStyle? code; - @Deprecated('customize the text style configuration directly') - final String? highlightColorHex; - - // Selection menu styles - @Deprecated('customize the selection menu directly') - final Color? selectionMenuBackgroundColor; - @Deprecated('customize the selection menu directly') - final Color? selectionMenuItemTextColor; - @Deprecated('customize the selection menu directly') - final Color? selectionMenuItemIconColor; - @Deprecated('customize the selection menu directly') - final Color? selectionMenuItemSelectedTextColor; - @Deprecated('customize the selection menu directly') - final Color? selectionMenuItemSelectedIconColor; - @Deprecated('customize the selection menu directly') - final Color? selectionMenuItemSelectedColor; - @Deprecated('customize the selection menu directly') - final Color? toolbarColor; - @Deprecated('customize the selection menu directly') - final double toolbarElevation; - - // Item's pop up menu styles - final Color? popupMenuFGColor; - final Color? popupMenuHoverColor; - +import 'package:provider/provider.dart'; + +/// The style of the editor. +/// +/// You can customize the style of the editor by passing the [EditorStyle] to +/// the [AppFlowyEditor]. +/// +class EditorStyle { const EditorStyle({ required this.padding, required this.cursorColor, required this.selectionColor, required this.textStyleConfiguration, - required this.backgroundColor, - required this.selectionMenuBackgroundColor, - required this.selectionMenuItemTextColor, - required this.selectionMenuItemIconColor, - required this.selectionMenuItemSelectedTextColor, - required this.selectionMenuItemSelectedIconColor, - required this.selectionMenuItemSelectedColor, - required this.toolbarColor, - required this.toolbarElevation, - required this.textPadding, - required this.textStyle, - required this.placeholderTextStyle, - required this.bold, - required this.italic, - required this.underline, - required this.strikethrough, - required this.href, - required this.code, - required this.highlightColorHex, - required this.lineHeight, - required this.popupMenuFGColor, - required this.popupMenuHoverColor, required this.textSpanDecorator, }); + /// The padding of the editor. + final EdgeInsets padding; + + /// The cursor color + final Color cursorColor; + + /// The selection color + final Color selectionColor; + + /// Customize the text style of the editor. + /// + /// All the text-based components will use this configuration to build their + /// text style. + /// + /// Notes, this configuration is only for the common config of text style and + /// it maybe override if the text block has its own [BlockComponentConfiguration]. + final TextStyleConfiguration textStyleConfiguration; + + /// Customize the built-in or custom text span. + /// + /// For example, you can add a custom text span for the mention text + /// or override the built-in text span. + final TextSpanDecoratorForAttribute? textSpanDecorator; + const EditorStyle.desktop({ EdgeInsets? padding, Color? backgroundColor, Color? cursorColor, Color? selectionColor, TextStyleConfiguration? textStyleConfiguration, - TextSpanDecoratorForCustomAttributes? textSpanDecorator, + TextSpanDecoratorForAttribute? textSpanDecorator, }) : this( padding: padding ?? const EdgeInsets.symmetric(horizontal: 100), - backgroundColor: backgroundColor ?? Colors.white, cursorColor: cursorColor ?? const Color(0xFF00BCF0), selectionColor: selectionColor ?? const Color.fromARGB(53, 111, 201, 231), @@ -117,28 +60,8 @@ class EditorStyle extends ThemeExtension { const TextStyleConfiguration( text: TextStyle(fontSize: 16, color: Colors.black), ), - textSpanDecorator: textSpanDecorator, - selectionMenuBackgroundColor: null, - selectionMenuItemTextColor: null, - selectionMenuItemIconColor: null, - selectionMenuItemSelectedTextColor: null, - selectionMenuItemSelectedIconColor: null, - selectionMenuItemSelectedColor: null, - toolbarColor: null, - toolbarElevation: 0, - textPadding: null, - textStyle: null, - placeholderTextStyle: null, - bold: null, - italic: null, - underline: null, - strikethrough: null, - href: null, - code: null, - highlightColorHex: null, - lineHeight: 0, - popupMenuFGColor: null, - popupMenuHoverColor: null, + textSpanDecorator: + textSpanDecorator ?? defaultTextSpanDecoratorForAttribute, ); const EditorStyle.mobile({ @@ -147,10 +70,9 @@ class EditorStyle extends ThemeExtension { Color? cursorColor, Color? selectionColor, TextStyleConfiguration? textStyleConfiguration, - TextSpanDecoratorForCustomAttributes? textSpanDecorator, + TextSpanDecoratorForAttribute? textSpanDecorator, }) : this( padding: padding ?? const EdgeInsets.symmetric(horizontal: 20), - backgroundColor: backgroundColor ?? Colors.white, cursorColor: cursorColor ?? const Color(0xFF00BCF0), selectionColor: selectionColor ?? const Color.fromARGB(53, 111, 201, 231), @@ -159,226 +81,78 @@ class EditorStyle extends ThemeExtension { text: TextStyle(fontSize: 16, color: Colors.black), ), textSpanDecorator: textSpanDecorator, - selectionMenuBackgroundColor: null, - selectionMenuItemTextColor: null, - selectionMenuItemIconColor: null, - selectionMenuItemSelectedTextColor: null, - selectionMenuItemSelectedIconColor: null, - selectionMenuItemSelectedColor: null, - toolbarColor: null, - toolbarElevation: 0, - textPadding: null, - textStyle: null, - placeholderTextStyle: null, - bold: null, - italic: null, - underline: null, - strikethrough: null, - href: null, - code: null, - highlightColorHex: null, - lineHeight: 0, - popupMenuFGColor: null, - popupMenuHoverColor: null, ); - @override EditorStyle copyWith({ EdgeInsets? padding, Color? backgroundColor, Color? cursorColor, Color? selectionColor, TextStyleConfiguration? textStyleConfiguration, - TextSpanDecoratorForCustomAttributes? textSpanDecorator, - Color? selectionMenuBackgroundColor, - Color? selectionMenuItemTextColor, - Color? selectionMenuItemIconColor, - Color? selectionMenuItemSelectedTextColor, - Color? selectionMenuItemSelectedIconColor, - Color? selectionMenuItemSelectedColor, - Color? toolbarColor, - double? toolbarElevation, - TextStyle? textStyle, - TextStyle? placeholderTextStyle, - TextStyle? bold, - TextStyle? italic, - TextStyle? underline, - TextStyle? strikethrough, - TextStyle? href, - TextStyle? code, - String? highlightColorHex, - double? lineHeight, - Color? popupMenuFGColor, - Color? popupMenuHoverColor, - EdgeInsets? textPadding, + TextSpanDecoratorForAttribute? textSpanDecorator, }) { return EditorStyle( padding: padding ?? this.padding, - backgroundColor: backgroundColor ?? this.backgroundColor, cursorColor: cursorColor ?? this.cursorColor, selectionColor: selectionColor ?? this.selectionColor, textStyleConfiguration: textStyleConfiguration ?? this.textStyleConfiguration, textSpanDecorator: textSpanDecorator ?? this.textSpanDecorator, - selectionMenuBackgroundColor: - selectionMenuBackgroundColor ?? this.selectionMenuBackgroundColor, - selectionMenuItemTextColor: - selectionMenuItemTextColor ?? this.selectionMenuItemTextColor, - selectionMenuItemIconColor: - selectionMenuItemIconColor ?? this.selectionMenuItemIconColor, - selectionMenuItemSelectedTextColor: selectionMenuItemSelectedTextColor ?? - this.selectionMenuItemSelectedTextColor, - selectionMenuItemSelectedIconColor: selectionMenuItemSelectedIconColor ?? - this.selectionMenuItemSelectedIconColor, - selectionMenuItemSelectedColor: - selectionMenuItemSelectedColor ?? this.selectionMenuItemSelectedColor, - toolbarColor: toolbarColor ?? this.toolbarColor, - toolbarElevation: toolbarElevation ?? this.toolbarElevation, - textPadding: textPadding ?? this.textPadding, - textStyle: textStyle ?? this.textStyle, - placeholderTextStyle: placeholderTextStyle ?? this.placeholderTextStyle, - bold: bold ?? this.bold, - italic: italic ?? this.italic, - underline: underline ?? this.underline, - strikethrough: strikethrough ?? this.strikethrough, - href: href ?? this.href, - code: code ?? this.code, - highlightColorHex: highlightColorHex ?? this.highlightColorHex, - lineHeight: lineHeight ?? this.lineHeight, - popupMenuFGColor: popupMenuFGColor ?? this.popupMenuFGColor, - popupMenuHoverColor: popupMenuHoverColor ?? this.popupMenuHoverColor, ); } +} - @override - ThemeExtension lerp( - ThemeExtension? other, - double t, - ) { - if (other == null || other is! EditorStyle) { - return this; - } - return EditorStyle( - padding: EdgeInsets.lerp(padding, other.padding, t), - backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t), - cursorColor: Color.lerp(cursorColor, other.cursorColor, t)!, - textPadding: EdgeInsets.lerp(textPadding, other.textPadding, t), - textStyleConfiguration: other.textStyleConfiguration, - textSpanDecorator: other.textSpanDecorator, - selectionColor: Color.lerp(selectionColor, other.selectionColor, t)!, - selectionMenuBackgroundColor: Color.lerp( - selectionMenuBackgroundColor, - other.selectionMenuBackgroundColor, - t, - ), - selectionMenuItemTextColor: Color.lerp( - selectionMenuItemTextColor, - other.selectionMenuItemTextColor, - t, - ), - selectionMenuItemIconColor: Color.lerp( - selectionMenuItemIconColor, - other.selectionMenuItemIconColor, - t, - ), - selectionMenuItemSelectedTextColor: Color.lerp( - selectionMenuItemSelectedTextColor, - other.selectionMenuItemSelectedTextColor, - t, - ), - selectionMenuItemSelectedIconColor: Color.lerp( - selectionMenuItemSelectedIconColor, - other.selectionMenuItemSelectedIconColor, - t, - ), - selectionMenuItemSelectedColor: Color.lerp( - selectionMenuItemSelectedColor, - other.selectionMenuItemSelectedColor, - t, - ), - toolbarColor: Color.lerp(toolbarColor, other.toolbarColor, t), - toolbarElevation: toolbarElevation, - textStyle: TextStyle.lerp(textStyle, other.textStyle, t), - placeholderTextStyle: - TextStyle.lerp(placeholderTextStyle, other.placeholderTextStyle, t), - bold: TextStyle.lerp(bold, other.bold, t), - italic: TextStyle.lerp(italic, other.italic, t), - underline: TextStyle.lerp(underline, other.underline, t), - strikethrough: TextStyle.lerp(strikethrough, other.strikethrough, t), - href: TextStyle.lerp(href, other.href, t), - code: TextStyle.lerp(code, other.code, t), - highlightColorHex: highlightColorHex, - lineHeight: lineHeight, - popupMenuFGColor: Color.lerp(popupMenuFGColor, other.popupMenuFGColor, t), - popupMenuHoverColor: - Color.lerp(popupMenuHoverColor, other.popupMenuHoverColor, t), - ); +/// Supports +/// +/// - customize the href text span +TextSpan defaultTextSpanDecoratorForAttribute( + BuildContext context, + Node node, + int index, + TextInsert text, + TextSpan textSpan, +) { + final attributes = text.attributes; + if (attributes == null) { + return textSpan; } - - static EditorStyle? of(BuildContext context) { - return Theme.of(context).extension(); + final editorState = context.read(); + final href = attributes[AppFlowyRichTextKeys.href] as String?; + if (href != null) { + // add a tap gesture recognizer to the text span + Timer? timer; + int tapCount = 0; + final tapGestureRecognizer = TapGestureRecognizer() + ..onTap = () async { + // implement a simple double tap logic + tapCount += 1; + timer?.cancel(); + if (tapCount == 2 || !editorState.editable) { + tapCount = 0; + safeLaunchUrl(href); + return; + } + timer = Timer(const Duration(milliseconds: 200), () { + tapCount = 0; + final selection = Selection.single( + path: node.path, + startOffset: index, + endOffset: index + text.text.length, + ); + editorState.updateSelectionWithReason( + selection, + reason: SelectionUpdateReason.uiEvent, + ); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + showLinkMenu(context, editorState, selection, true); + }); + }); + }; + return TextSpan( + style: textSpan.style, + text: text.text, + recognizer: tapGestureRecognizer, + ); } - - static final light = EditorStyle( - padding: PlatformExtension.isMobile - ? const EdgeInsets.symmetric(horizontal: 20) - : const EdgeInsets.symmetric(horizontal: 200), - backgroundColor: Colors.white, - cursorColor: const Color(0xFF00BCF0), - textStyleConfiguration: const TextStyleConfiguration( - text: TextStyle(fontSize: 16, color: Colors.black), - ), - textSpanDecorator: (_, __, ___, textSpan) => textSpan, - selectionColor: const Color.fromARGB(53, 111, 201, 231), - selectionMenuBackgroundColor: const Color(0xFFFFFFFF), - selectionMenuItemTextColor: const Color(0xFF333333), - selectionMenuItemIconColor: const Color(0xFF333333), - selectionMenuItemSelectedTextColor: const Color.fromARGB(255, 56, 91, 247), - selectionMenuItemSelectedIconColor: const Color.fromARGB(255, 56, 91, 247), - selectionMenuItemSelectedColor: const Color(0xFFE0F8FF), - toolbarColor: const Color(0xFF333333), - toolbarElevation: 0.0, - textPadding: const EdgeInsets.symmetric(vertical: 8.0), - textStyle: const TextStyle(fontSize: 16.0, color: Colors.black), - placeholderTextStyle: const TextStyle(fontSize: 16.0, color: Colors.grey), - bold: const TextStyle(fontWeight: FontWeight.bold), - italic: const TextStyle(fontStyle: FontStyle.italic), - underline: const TextStyle(decoration: TextDecoration.underline), - strikethrough: const TextStyle(decoration: TextDecoration.lineThrough), - href: const TextStyle( - color: Colors.blue, - decoration: TextDecoration.underline, - ), - code: const TextStyle( - fontFamily: 'monospace', - color: Color(0xFF00BCF0), - backgroundColor: Color(0xFFE0F8FF), - ), - highlightColorHex: '0x6000BCF0', - lineHeight: 1.5, - popupMenuFGColor: const Color(0xFF333333), - popupMenuHoverColor: const Color(0xFFE0F8FF), - ); - - @Deprecated( - 'EditorStyle.dark has been deprecated. You can now customize the editor style by utilizing the TextStyleConfiguration.', - ) - static final dark = light.copyWith( - backgroundColor: Colors.black, - textStyle: const TextStyle(fontSize: 16.0, color: Colors.white), - placeholderTextStyle: TextStyle( - fontSize: 16.0, - color: Colors.white.withOpacity(0.3), - ), - selectionMenuBackgroundColor: const Color(0xFF282E3A), - selectionMenuItemTextColor: const Color(0xFFBBC3CD), - selectionMenuItemIconColor: const Color(0xFFBBC3CD), - selectionMenuItemSelectedTextColor: const Color(0xFF131720), - selectionMenuItemSelectedIconColor: const Color(0xFF131720), - selectionMenuItemSelectedColor: const Color(0xFF00BCF0), - toolbarColor: const Color(0xFF131720), - toolbarElevation: 0.0, - popupMenuFGColor: Colors.white, - popupMenuHoverColor: const Color(0xFF00BCF0), - ); + return textSpan; } diff --git a/lib/src/editor/toolbar/desktop/items/color/color_picker.dart b/lib/src/editor/toolbar/desktop/items/color/color_picker.dart index 3ba42ff2a..4d25bdfd6 100644 --- a/lib/src/editor/toolbar/desktop/items/color/color_picker.dart +++ b/lib/src/editor/toolbar/desktop/items/color/color_picker.dart @@ -173,7 +173,7 @@ class ResetTextColorButton extends StatelessWidget { onPressed: () { final selection = editorState.selection!; editorState - .formatDelta(selection, {FlowyRichTextKeys.textColor: null}); + .formatDelta(selection, {AppFlowyRichTextKeys.textColor: null}); dismissOverlay(); }, icon: EditorSvg( @@ -225,7 +225,7 @@ class ClearHighlightColorButton extends StatelessWidget { final selection = editorState.selection!; editorState.formatDelta( selection, - {FlowyRichTextKeys.highlightColor: null}, + {AppFlowyRichTextKeys.highlightColor: null}, ); dismissOverlay(); }, diff --git a/lib/src/editor/toolbar/desktop/items/color/highlight_color_toolbar_item.dart b/lib/src/editor/toolbar/desktop/items/color/highlight_color_toolbar_item.dart index b7c09e984..286f3e840 100644 --- a/lib/src/editor/toolbar/desktop/items/color/highlight_color_toolbar_item.dart +++ b/lib/src/editor/toolbar/desktop/items/color/highlight_color_toolbar_item.dart @@ -13,7 +13,7 @@ ToolbarItem buildHighlightColorItem({List? colorOptions}) { final nodes = editorState.getNodesInSelection(selection); final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { return delta.everyAttributes((attributes) { - highlightColorHex = attributes[FlowyRichTextKeys.highlightColor]; + highlightColorHex = attributes[AppFlowyRichTextKeys.highlightColor]; return highlightColorHex != null; }); }); diff --git a/lib/src/editor/toolbar/desktop/items/color/text_color_toolbar_item.dart b/lib/src/editor/toolbar/desktop/items/color/text_color_toolbar_item.dart index 54dfb7e14..ce1925122 100644 --- a/lib/src/editor/toolbar/desktop/items/color/text_color_toolbar_item.dart +++ b/lib/src/editor/toolbar/desktop/items/color/text_color_toolbar_item.dart @@ -14,7 +14,7 @@ ToolbarItem buildTextColorItem({ final nodes = editorState.getNodesInSelection(selection); final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { return delta.everyAttributes((attributes) { - textColorHex = attributes[FlowyRichTextKeys.textColor]; + textColorHex = attributes[AppFlowyRichTextKeys.textColor]; return (textColorHex != null); }); }); diff --git a/lib/src/editor/toolbar/desktop/items/link/link_toolbar_item.dart b/lib/src/editor/toolbar/desktop/items/link/link_toolbar_item.dart index e9694f063..6505d8044 100644 --- a/lib/src/editor/toolbar/desktop/items/link/link_toolbar_item.dart +++ b/lib/src/editor/toolbar/desktop/items/link/link_toolbar_item.dart @@ -12,7 +12,7 @@ final linkItem = ToolbarItem( final nodes = editorState.getNodesInSelection(selection); final isHref = nodes.allSatisfyInSelection(selection, (delta) { return delta.everyAttributes( - (attributes) => attributes[FlowyRichTextKeys.href] != null, + (attributes) => attributes[AppFlowyRichTextKeys.href] != null, ); }); diff --git a/lib/src/editor/toolbar/mobile/mobile_toolbar.dart b/lib/src/editor/toolbar/mobile/mobile_toolbar.dart index d8d5387a7..985c580ca 100644 --- a/lib/src/editor/toolbar/mobile/mobile_toolbar.dart +++ b/lib/src/editor/toolbar/mobile/mobile_toolbar.dart @@ -188,11 +188,11 @@ class _ToolbarItemListView extends StatelessWidget { Widget build(BuildContext context) { return ListView.builder( itemBuilder: (context, index) { - final toobarItem = toolbarItems[index]; + final toolbarItem = toolbarItems[index]; return IconButton( - icon: toobarItem.itemIcon, + icon: toolbarItem.itemIcon, onPressed: () { - if (toobarItem.hasMenu) { + if (toolbarItem.hasMenu) { // open /close current item menu through its parent widget(MobileToolbarWidget) itemOnPressed.call(index); } else { diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/code_mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/code_mobile_toolbar_item.dart index add75049a..9b487f4cc 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/code_mobile_toolbar_item.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/code_mobile_toolbar_item.dart @@ -3,5 +3,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; final codeMobileToolbarItem = MobileToolbarItem.action( itemIcon: const AFMobileIcon(afMobileIcons: AFMobileIcons.code), actionHandler: (editorState, selection) => - editorState.toggleAttribute(FlowyRichTextKeys.code), + editorState.toggleAttribute(AppFlowyRichTextKeys.code), ); diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/color/background_color_options_widgets.dart b/lib/src/editor/toolbar/mobile/toolbar_items/color/background_color_options_widgets.dart index 1cdc24ba0..a26caed81 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/color/background_color_options_widgets.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/color/background_color_options_widgets.dart @@ -29,7 +29,7 @@ class _BackgroundColorOptionsWidgetsState final nodes = widget.editorState.getNodesInSelection(selection); final hasTextColor = nodes.allSatisfyInSelection(selection, (delta) { return delta.everyAttributes( - (attributes) => attributes[FlowyRichTextKeys.highlightColor] != null, + (attributes) => attributes[AppFlowyRichTextKeys.highlightColor] != null, ); }); @@ -48,7 +48,7 @@ class _BackgroundColorOptionsWidgetsState setState(() { widget.editorState.formatDelta( selection, - {FlowyRichTextKeys.highlightColor: null}, + {AppFlowyRichTextKeys.highlightColor: null}, ); }); } @@ -60,7 +60,8 @@ class _BackgroundColorOptionsWidgetsState final isSelected = nodes.allSatisfyInSelection(selection, (delta) { return delta.everyAttributes( (attributes) => - attributes[FlowyRichTextKeys.highlightColor] == e.colorHex, + attributes[AppFlowyRichTextKeys.highlightColor] == + e.colorHex, ); }); return ColorButton( diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/color/text_color_options_widgets.dart b/lib/src/editor/toolbar/mobile/toolbar_items/color/text_color_options_widgets.dart index 9d1b538b9..64812af55 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/color/text_color_options_widgets.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/color/text_color_options_widgets.dart @@ -27,7 +27,7 @@ class _TextColorOptionsWidgetsState extends State { final nodes = widget.editorState.getNodesInSelection(selection); final hasTextColor = nodes.allSatisfyInSelection(selection, (delta) { return delta.everyAttributes( - (attributes) => attributes[FlowyRichTextKeys.textColor] != null, + (attributes) => attributes[AppFlowyRichTextKeys.textColor] != null, ); }); @@ -48,7 +48,7 @@ class _TextColorOptionsWidgetsState extends State { setState(() { widget.editorState.formatDelta( selection, - {FlowyRichTextKeys.textColor: null}, + {AppFlowyRichTextKeys.textColor: null}, ); }); } @@ -60,7 +60,7 @@ class _TextColorOptionsWidgetsState extends State { final isSelected = nodes.allSatisfyInSelection(selection, (delta) { return delta.everyAttributes( (attributes) => - attributes[FlowyRichTextKeys.textColor] == e.colorHex, + attributes[AppFlowyRichTextKeys.textColor] == e.colorHex, ); }); return ColorButton( diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/link_mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/link_mobile_toolbar_item.dart index 4bc1dd0ad..0d7050999 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/link_mobile_toolbar_item.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/link_mobile_toolbar_item.dart @@ -7,7 +7,7 @@ final linkMobileToolbarItem = MobileToolbarItem.withMenu( ), itemMenuBuilder: (editorState, selection, service) { final String? linkText = editorState.getDeltaAttributeValueInSelection( - FlowyRichTextKeys.href, + AppFlowyRichTextKeys.href, selection, ); @@ -17,7 +17,7 @@ final linkMobileToolbarItem = MobileToolbarItem.withMenu( onSubmitted: (value) async { if (value.isNotEmpty) { await editorState.formatDelta(selection, { - FlowyRichTextKeys.href: value, + AppFlowyRichTextKeys.href: value, }); } service.closeItemMenu(); diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item.dart index b3922d9b9..6c4db49b9 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item.dart @@ -27,22 +27,22 @@ class _TextDecorationMenuState extends State<_TextDecorationMenu> { TextDecorationUnit( icon: AFMobileIcons.bold, label: AppFlowyEditorLocalizations.current.bold, - name: FlowyRichTextKeys.bold, + name: AppFlowyRichTextKeys.bold, ), TextDecorationUnit( icon: AFMobileIcons.italic, label: AppFlowyEditorLocalizations.current.italic, - name: FlowyRichTextKeys.italic, + name: AppFlowyRichTextKeys.italic, ), TextDecorationUnit( icon: AFMobileIcons.underline, label: AppFlowyEditorLocalizations.current.underline, - name: FlowyRichTextKeys.underline, + name: AppFlowyRichTextKeys.underline, ), TextDecorationUnit( icon: AFMobileIcons.strikethrough, label: AppFlowyEditorLocalizations.current.strikethrough, - name: FlowyRichTextKeys.strikethrough, + name: AppFlowyRichTextKeys.strikethrough, ), ]; @override diff --git a/lib/src/editor/toolbar/utils/format_color.dart b/lib/src/editor/toolbar/utils/format_color.dart index f368076fc..51f7763be 100644 --- a/lib/src/editor/toolbar/utils/format_color.dart +++ b/lib/src/editor/toolbar/utils/format_color.dart @@ -3,13 +3,13 @@ import 'package:appflowy_editor/appflowy_editor.dart'; void formatHighlightColor(EditorState editorState, String color) { editorState.formatDelta( editorState.selection, - {FlowyRichTextKeys.highlightColor: color}, + {AppFlowyRichTextKeys.highlightColor: color}, ); } void formatFontColor(EditorState editorState, String color) { editorState.formatDelta( editorState.selection, - {FlowyRichTextKeys.textColor: color}, + {AppFlowyRichTextKeys.textColor: color}, ); } diff --git a/lib/src/flutter/overlay.dart b/lib/src/flutter/overlay.dart index 901f86b72..c578d164a 100644 --- a/lib/src/flutter/overlay.dart +++ b/lib/src/flutter/overlay.dart @@ -334,6 +334,9 @@ class Overlay extends StatefulWidget { class OverlayState extends State with TickerProviderStateMixin { final List _entries = []; + UnmodifiableListView get entries => + UnmodifiableListView(_entries); + @override void initState() { super.initState(); diff --git a/lib/src/plugins/html/html_document_decoder.dart b/lib/src/plugins/html/html_document_decoder.dart index 24ea0e174..82c402915 100644 --- a/lib/src/plugins/html/html_document_decoder.dart +++ b/lib/src/plugins/html/html_document_decoder.dart @@ -97,19 +97,19 @@ class DocumentHTMLDecoder extends Converter { Attributes attributes = {}; switch (localName) { case HTMLTags.bold || HTMLTags.strong: - attributes = {FlowyRichTextKeys.bold: true}; + attributes = {AppFlowyRichTextKeys.bold: true}; break; case HTMLTags.italic || HTMLTags.em: - attributes = {FlowyRichTextKeys.italic: true}; + attributes = {AppFlowyRichTextKeys.italic: true}; break; case HTMLTags.underline: - attributes = {FlowyRichTextKeys.underline: true}; + attributes = {AppFlowyRichTextKeys.underline: true}; break; case HTMLTags.del: - attributes = {FlowyRichTextKeys.strikethrough: true}; + attributes = {AppFlowyRichTextKeys.strikethrough: true}; break; case HTMLTags.code: - attributes = {FlowyRichTextKeys.code: true}; + attributes = {AppFlowyRichTextKeys.code: true}; case HTMLTags.span: final deltaAttributes = _getDeltaAttributesFromHTMLAttributes( element.attributes, @@ -120,7 +120,7 @@ class DocumentHTMLDecoder extends Converter { case HTMLTags.anchor: final href = element.attributes['href']; if (href != null) { - attributes = {FlowyRichTextKeys.href: href}; + attributes = {AppFlowyRichTextKeys.href: href}; } break; default: @@ -235,11 +235,11 @@ class DocumentHTMLDecoder extends Converter { final fontWeight = css['font-weight']; if (fontWeight != null) { if (fontWeight == 'bold') { - attributes[FlowyRichTextKeys.bold] = true; + attributes[AppFlowyRichTextKeys.bold] = true; } else { final weight = int.tryParse(fontWeight); if (weight != null && weight >= 500) { - attributes[FlowyRichTextKeys.bold] = true; + attributes[AppFlowyRichTextKeys.bold] = true; } } } @@ -251,10 +251,10 @@ class DocumentHTMLDecoder extends Converter { for (final decoration in decorations) { switch (decoration) { case 'underline': - attributes[FlowyRichTextKeys.underline] = true; + attributes[AppFlowyRichTextKeys.underline] = true; break; case 'line-through': - attributes[FlowyRichTextKeys.strikethrough] = true; + attributes[AppFlowyRichTextKeys.strikethrough] = true; break; default: break; @@ -267,14 +267,14 @@ class DocumentHTMLDecoder extends Converter { if (backgroundColor != null) { final highlightColor = backgroundColor.tryToColor()?.toHex(); if (highlightColor != null) { - attributes[FlowyRichTextKeys.highlightColor] = highlightColor; + attributes[AppFlowyRichTextKeys.highlightColor] = highlightColor; } } // italic final fontStyle = css['font-style']; if (fontStyle == 'italic') { - attributes[FlowyRichTextKeys.italic] = true; + attributes[AppFlowyRichTextKeys.italic] = true; } return attributes.isEmpty ? null : attributes; diff --git a/lib/src/plugins/html/html_document_encoder.dart b/lib/src/plugins/html/html_document_encoder.dart index 4e88eb7b9..658d02867 100644 --- a/lib/src/plugins/html/html_document_encoder.dart +++ b/lib/src/plugins/html/html_document_encoder.dart @@ -211,29 +211,29 @@ class DocumentHTMLEncoder extends Converter { } dom.Element _applyAttributes(Attributes attributes, {required String text}) { - if (attributes[FlowyRichTextKeys.bold] == true) { + if (attributes[AppFlowyRichTextKeys.bold] == true) { final strong = dom.Element.tag(HTMLTags.strong); strong.append(dom.Text(text)); return strong; - } else if (attributes[FlowyRichTextKeys.underline] == true) { + } else if (attributes[AppFlowyRichTextKeys.underline] == true) { final underline = dom.Element.tag(HTMLTags.underline); underline.append(dom.Text(text)); return underline; - } else if (attributes[FlowyRichTextKeys.italic] == true) { + } else if (attributes[AppFlowyRichTextKeys.italic] == true) { final italic = dom.Element.tag(HTMLTags.italic); italic.append(dom.Text(text)); return italic; - } else if (attributes[FlowyRichTextKeys.strikethrough] == true) { + } else if (attributes[AppFlowyRichTextKeys.strikethrough] == true) { final del = dom.Element.tag(HTMLTags.del); del.append(dom.Text(text)); return del; - } else if (attributes[FlowyRichTextKeys.code] == true) { + } else if (attributes[AppFlowyRichTextKeys.code] == true) { final code = dom.Element.tag(HTMLTags.code); code.append(dom.Text(text)); return code; - } else if (attributes[FlowyRichTextKeys.href] != null) { + } else if (attributes[AppFlowyRichTextKeys.href] != null) { final anchor = dom.Element.tag(HTMLTags.anchor); - anchor.attributes['href'] = attributes[FlowyRichTextKeys.href]; + anchor.attributes['href'] = attributes[AppFlowyRichTextKeys.href]; anchor.append(dom.Text(text)); return anchor; } else { diff --git a/lib/src/plugins/quill_delta/quill_delta_encoder.dart b/lib/src/plugins/quill_delta/quill_delta_encoder.dart index 2b25383ae..b40a6cdd0 100644 --- a/lib/src/plugins/quill_delta/quill_delta_encoder.dart +++ b/lib/src/plugins/quill_delta/quill_delta_encoder.dart @@ -69,30 +69,30 @@ class QuillDeltaEncoder extends Converter { void _applyStyle(Node node, String text, Map? attributes) { final Attributes attrs = {}; if (_containsStyle(attributes, 'strike')) { - attrs[FlowyRichTextKeys.strikethrough] = true; + attrs[AppFlowyRichTextKeys.strikethrough] = true; } if (_containsStyle(attributes, 'underline')) { - attrs[FlowyRichTextKeys.underline] = true; + attrs[AppFlowyRichTextKeys.underline] = true; } if (_containsStyle(attributes, 'bold')) { - attrs[FlowyRichTextKeys.bold] = true; + attrs[AppFlowyRichTextKeys.bold] = true; } if (_containsStyle(attributes, 'italic')) { - attrs[FlowyRichTextKeys.italic] = true; + attrs[AppFlowyRichTextKeys.italic] = true; } final link = attributes?['link'] as String?; if (link != null) { - attrs[FlowyRichTextKeys.href] = link; + attrs[AppFlowyRichTextKeys.href] = link; } final color = attributes?['color'] as String?; final colorHex = _convertColorToHexString(color); if (colorHex != null) { - attrs[FlowyRichTextKeys.textColor] = colorHex; + attrs[AppFlowyRichTextKeys.textColor] = colorHex; } final backgroundColor = attributes?['background'] as String?; final backgroundHex = _convertColorToHexString(backgroundColor); if (backgroundHex != null) { - attrs[FlowyRichTextKeys.highlightColor] = backgroundHex; + attrs[AppFlowyRichTextKeys.highlightColor] = backgroundHex; } node.updateAttributes({ 'delta': (node.delta?..insert(text, attributes: attrs))?.toJson(), diff --git a/lib/src/render/image/image_upload_widget.dart b/lib/src/render/image/image_upload_widget.dart deleted file mode 100644 index 9415c30f3..000000000 --- a/lib/src/render/image/image_upload_widget.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -// void showImageMenu( -// OverlayState container, -// EditorState editorState, -// SelectionMenuService menuService, -// ) { -// menuService.dismiss(); - -// final imageMenu = -// } - -OverlayEntry? _imageUploadMenu; -EditorState? _editorState; -void showImageUploadMenu( - EditorState editorState, - SelectionMenuService menuService, - BuildContext context, -) { - menuService.dismiss(); - - _imageUploadMenu?.remove(); - _imageUploadMenu = OverlayEntry( - builder: (context) => Positioned( - top: menuService.topLeft.dy, - left: menuService.topLeft.dx, - child: Material( - child: ImageUploadMenu( - editorState: editorState, - onSubmitted: (src) {}, - onUpload: (src) {}, - ), - ), - ), - ); - - Overlay.of(context).insert(_imageUploadMenu!); - - editorState.service.selectionService.currentSelection - .addListener(_dismissImageUploadMenu); -} - -void _dismissImageUploadMenu() { - _imageUploadMenu?.remove(); - _imageUploadMenu = null; - - _editorState?.service.selectionService.currentSelection - .removeListener(_dismissImageUploadMenu); - _editorState = null; -} - -class ImageUploadMenu extends StatefulWidget { - const ImageUploadMenu({ - Key? key, - required this.onSubmitted, - required this.onUpload, - this.editorState, - }) : super(key: key); - - final void Function(String text) onSubmitted; - final void Function(String text) onUpload; - final EditorState? editorState; - - @override - State createState() => _ImageUploadMenuState(); -} - -class _ImageUploadMenuState extends State { - final _textEditingController = TextEditingController(); - final _focusNode = FocusNode(); - - EditorStyle? get style => widget.editorState?.editorStyle; - - @override - void initState() { - super.initState(); - _focusNode.requestFocus(); - } - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - width: 300, - padding: const EdgeInsets.all(24.0), - decoration: BoxDecoration( - color: style?.selectionMenuBackgroundColor ?? Colors.white, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withOpacity(0.1), - ), - ], - // borderRadius: BorderRadius.circular(6.0), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildInput(), - const SizedBox(height: 18.0), - _buildUploadButton(context), - ], - ), - ); - } - - Widget _buildInput() { - return TextField( - focusNode: _focusNode, - style: const TextStyle(fontSize: 14.0), - textAlign: TextAlign.left, - controller: _textEditingController, - onSubmitted: widget.onSubmitted, - decoration: InputDecoration( - hintText: 'URL', - hintStyle: const TextStyle(fontSize: 14.0), - contentPadding: const EdgeInsets.all(16.0), - isDense: true, - suffixIcon: IconButton( - padding: const EdgeInsets.all(4.0), - icon: const EditorSvg( - name: 'clear', - width: 24, - height: 24, - ), - onPressed: _textEditingController.clear, - ), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12.0)), - borderSide: BorderSide(color: Color(0xFFBDBDBD)), - ), - ), - ); - } - - Widget _buildUploadButton(BuildContext context) { - return SizedBox( - width: 170, - height: 48, - child: TextButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(const Color(0xFF00BCF0)), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - ), - ), - onPressed: () => widget.onUpload(_textEditingController.text), - child: Text( - 'Upload', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, - fontSize: 14.0, - ), - ), - ), - ); - } -} diff --git a/lib/src/render/rich_text/built_in_text_widget.dart b/lib/src/render/rich_text/built_in_text_widget.dart deleted file mode 100644 index c239f6397..000000000 --- a/lib/src/render/rich_text/built_in_text_widget.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -abstract class BuiltInTextWidget extends StatefulWidget { - const BuiltInTextWidget({ - Key? key, - }) : super(key: key); - - EditorState get editorState; - TextNode get textNode; -} - -mixin BuiltInTextWidgetMixin on State - implements DefaultSelectable { - @override - Widget build(BuildContext context) { - if (widget.textNode.children.isEmpty) { - return buildWithSingle(context); - } else { - return buildWithChildren(context); - } - } - - Widget buildWithSingle(BuildContext context); - - Widget buildWithChildren(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - buildWithSingle(context), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // TODO: customize - const SizedBox(width: 20), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: widget.textNode.children - .map( - (child) => const SizedBox(), - ) - .toList(), - ), - ) - ], - ) - ], - ); - } -} diff --git a/lib/src/render/toolbar/toolbar_item.dart b/lib/src/render/toolbar/toolbar_item.dart index ea511769b..81b41dd05 100644 --- a/lib/src/render/toolbar/toolbar_item.dart +++ b/lib/src/render/toolbar/toolbar_item.dart @@ -1,7 +1,4 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; -import 'package:flutter/foundation.dart'; -import 'dart:io' show Platform; import 'package:flutter/material.dart' hide Overlay, OverlayEntry; typedef ToolbarItemEventHandler = void Function( @@ -71,262 +68,6 @@ class ToolbarItem { int get hashCode => id.hashCode; } -const baseToolbarIndex = 1; -List defaultToolbarItems = [ - ToolbarItem( - id: 'appflowy.toolbar.h1', - type: 1, - group: baseToolbarIndex, - tooltipsMessage: AppFlowyEditorLocalizations.current.heading1, - iconBuilder: (isHighlight) => EditorSvg( - name: 'toolbar/h1', - color: isHighlight ? Colors.lightBlue : null, - ), - validator: _onlyShowInSingleTextSelection, - highlightCallback: (editorState) => _allSatisfy( - editorState, - BuiltInAttributeKey.heading, - (value) => value == BuiltInAttributeKey.h1, - ), - handler: (editorState, context) => - formatHeading(editorState, BuiltInAttributeKey.h1), - ), - ToolbarItem( - id: 'appflowy.toolbar.h2', - type: 1, - group: baseToolbarIndex, - tooltipsMessage: AppFlowyEditorLocalizations.current.heading2, - iconBuilder: (isHighlight) => EditorSvg( - name: 'toolbar/h2', - color: isHighlight ? Colors.lightBlue : null, - ), - validator: _onlyShowInSingleTextSelection, - highlightCallback: (editorState) => _allSatisfy( - editorState, - BuiltInAttributeKey.heading, - (value) => value == BuiltInAttributeKey.h2, - ), - handler: (editorState, context) => - formatHeading(editorState, BuiltInAttributeKey.h2), - ), - ToolbarItem( - id: 'appflowy.toolbar.h3', - type: 1, - group: baseToolbarIndex, - tooltipsMessage: AppFlowyEditorLocalizations.current.heading3, - iconBuilder: (isHighlight) => EditorSvg( - name: 'toolbar/h3', - color: isHighlight ? Colors.lightBlue : null, - ), - validator: _onlyShowInSingleTextSelection, - highlightCallback: (editorState) => _allSatisfy( - editorState, - BuiltInAttributeKey.heading, - (value) => value == BuiltInAttributeKey.h3, - ), - handler: (editorState, context) => - formatHeading(editorState, BuiltInAttributeKey.h3), - ), - ToolbarItem( - id: 'appflowy.toolbar.bold', - type: 2, - group: baseToolbarIndex + 1, - tooltipsMessage: - "${AppFlowyEditorLocalizations.current.bold}${_shortcutTooltips("⌘ + B", "CTRL + B", "CTRL + B")}", - iconBuilder: (isHighlight) => EditorSvg( - name: 'toolbar/bold', - color: isHighlight ? Colors.lightBlue : null, - ), - validator: _showInBuiltInTextSelection, - highlightCallback: (editorState) => _allSatisfy( - editorState, - BuiltInAttributeKey.bold, - (value) => value == true, - ), - handler: (editorState, context) => formatBold(editorState), - ), - ToolbarItem( - id: 'appflowy.toolbar.italic', - type: 2, - group: baseToolbarIndex + 1, - tooltipsMessage: - "${AppFlowyEditorLocalizations.current.italic}${_shortcutTooltips("⌘ + I", "CTRL + I", "CTRL + I")}", - iconBuilder: (isHighlight) => EditorSvg( - name: 'toolbar/italic', - color: isHighlight ? Colors.lightBlue : null, - ), - validator: _showInBuiltInTextSelection, - highlightCallback: (editorState) => _allSatisfy( - editorState, - BuiltInAttributeKey.italic, - (value) => value == true, - ), - handler: (editorState, context) => formatItalic(editorState), - ), - ToolbarItem( - id: 'appflowy.toolbar.underline', - type: 2, - group: baseToolbarIndex + 1, - tooltipsMessage: - "${AppFlowyEditorLocalizations.current.underline}${_shortcutTooltips("⌘ + U", "CTRL + U", "CTRL + U")}", - iconBuilder: (isHighlight) => EditorSvg( - name: 'toolbar/underline', - color: isHighlight ? Colors.lightBlue : null, - ), - validator: _showInBuiltInTextSelection, - highlightCallback: (editorState) => _allSatisfy( - editorState, - BuiltInAttributeKey.underline, - (value) => value == true, - ), - handler: (editorState, context) => formatUnderline(editorState), - ), - ToolbarItem( - id: 'appflowy.toolbar.strikethrough', - type: 2, - group: baseToolbarIndex + 1, - tooltipsMessage: - "${AppFlowyEditorLocalizations.current.strikethrough}${_shortcutTooltips("⌘ + SHIFT + S", "CTRL + SHIFT + S", "CTRL + SHIFT + S")}", - iconBuilder: (isHighlight) => EditorSvg( - name: 'toolbar/strikethrough', - color: isHighlight ? Colors.lightBlue : null, - ), - validator: _showInBuiltInTextSelection, - highlightCallback: (editorState) => _allSatisfy( - editorState, - BuiltInAttributeKey.strikethrough, - (value) => value == true, - ), - handler: (editorState, context) => formatStrikethrough(editorState), - ), - ToolbarItem( - id: 'appflowy.toolbar.code', - type: 2, - group: baseToolbarIndex + 1, - tooltipsMessage: - "${AppFlowyEditorLocalizations.current.embedCode}${_shortcutTooltips("⌘ + E", "CTRL + E", "CTRL + E")}", - iconBuilder: (isHighlight) => EditorSvg( - name: 'toolbar/code', - color: isHighlight ? Colors.lightBlue : null, - ), - validator: _showInBuiltInTextSelection, - highlightCallback: (editorState) => _allSatisfy( - editorState, - BuiltInAttributeKey.code, - (value) => value == true, - ), - handler: (editorState, context) => formatEmbedCode(editorState), - ), - ToolbarItem( - id: 'appflowy.toolbar.quote', - type: 3, - group: baseToolbarIndex + 2, - tooltipsMessage: AppFlowyEditorLocalizations.current.quote, - iconBuilder: (isHighlight) => EditorSvg( - name: 'toolbar/quote', - color: isHighlight ? Colors.lightBlue : null, - ), - validator: _onlyShowInSingleTextSelection, - highlightCallback: (editorState) => _allSatisfy( - editorState, - BuiltInAttributeKey.subtype, - (value) => value == BuiltInAttributeKey.quote, - ), - handler: (editorState, context) { - formatQuote(editorState); - }, - ), - ToolbarItem( - id: 'appflowy.toolbar.bulleted_list', - type: 3, - group: baseToolbarIndex + 2, - tooltipsMessage: AppFlowyEditorLocalizations.current.bulletedList, - iconBuilder: (isHighlight) => EditorSvg( - name: 'toolbar/bulleted_list', - color: isHighlight ? Colors.lightBlue : null, - ), - validator: _onlyShowInSingleTextSelection, - highlightCallback: (editorState) => _allSatisfy( - editorState, - BuiltInAttributeKey.subtype, - (value) => value == BuiltInAttributeKey.bulletedList, - ), - handler: (editorState, context) => formatBulletedList(editorState), - ), - ToolbarItem( - id: 'appflowy.toolbar.highlight', - type: 4, - group: baseToolbarIndex + 2, - tooltipsMessage: - "${AppFlowyEditorLocalizations.current.highlight}${_shortcutTooltips("⌘ + SHIFT + H", "CTRL + SHIFT + H", "CTRL + SHIFT + H")}", - iconBuilder: (isHighlight) => EditorSvg( - name: 'toolbar/highlight', - color: isHighlight ? Colors.lightBlue : null, - ), - validator: _showInBuiltInTextSelection, - highlightCallback: (editorState) => _allSatisfy( - editorState, - BuiltInAttributeKey.highlightColor, - (value) { - return value != null && value != '0x00000000'; // transparent color; - }, - ), - handler: (editorState, context) => formatHighlight( - editorState, - editorState.editorStyle.highlightColorHex!, - ), - ), -]; - -String _shortcutTooltips( - String? macOSString, - String? windowsString, - String? linuxString, -) { - if (kIsWeb) return ''; - if (Platform.isMacOS && macOSString != null) { - return '\n$macOSString'; - } else if (Platform.isWindows && windowsString != null) { - return '\n$windowsString'; - } else if (Platform.isLinux && linuxString != null) { - return '\n$linuxString'; - } - return ''; -} - -ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) { - final result = _showInBuiltInTextSelection(editorState); - if (!result) { - return false; - } - final nodes = editorState.service.selectionService.currentSelectedNodes; - return (nodes.length == 1 && nodes.first is TextNode); -}; - -ToolbarItemValidator _showInBuiltInTextSelection = (editorState) { - final nodes = editorState.service.selectionService.currentSelectedNodes - .whereType() - .where( - (textNode) => - BuiltInAttributeKey.globalStyleKeys.contains(textNode.type), - ); - return nodes.isNotEmpty; -}; - -bool _allSatisfy( - EditorState editorState, - String styleKey, - bool Function(dynamic value) test, -) { - final selection = editorState.selection; - return selection != null && - editorState.selectedTextNodes.allSatisfyInSelection( - selection, - styleKey, - test, - ); -} - bool onlyShowInSingleSelectionAndTextType(EditorState editorState) { final selection = editorState.selection; if (selection == null || !selection.isSingle) { diff --git a/lib/src/render/toolbar/toolbar_widget.dart b/lib/src/render/toolbar/toolbar_widget.dart index e8dcea47d..d6022fd0f 100644 --- a/lib/src/render/toolbar/toolbar_widget.dart +++ b/lib/src/render/toolbar/toolbar_widget.dart @@ -58,8 +58,6 @@ class _ToolbarWidgetState extends State with ToolbarMixin { Widget _buildToolbar(BuildContext context) { return Material( borderRadius: BorderRadius.circular(8.0), - color: widget.editorState.editorStyle.toolbarColor, - elevation: widget.editorState.editorStyle.toolbarElevation, child: Padding( padding: const EdgeInsets.only(left: 8.0, right: 8.0), child: SizedBox( diff --git a/lib/src/service/context_menu/context_menu.dart b/lib/src/service/context_menu/context_menu.dart index 92b43abdb..427bef810 100644 --- a/lib/src/service/context_menu/context_menu.dart +++ b/lib/src/service/context_menu/context_menu.dart @@ -30,15 +30,11 @@ class ContextMenu extends StatelessWidget { final children = []; for (var i = 0; i < items.length; i++) { for (var j = 0; j < items[i].length; j++) { - var onHover = false; children.add( StatefulBuilder( builder: (BuildContext context, setState) { return Material( - color: editorState.editorStyle.selectionMenuBackgroundColor, child: InkWell( - hoverColor: - editorState.editorStyle.selectionMenuItemSelectedColor, customBorder: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), @@ -46,21 +42,14 @@ class ContextMenu extends StatelessWidget { items[i][j].onPressed(editorState); onPressed(); }, - onHover: (value) => setState(() { - onHover = value; - }), + onHover: (value) => setState(() {}), child: Padding( padding: const EdgeInsets.all(8.0), child: Text( items[i][j].name, textAlign: TextAlign.start, - style: TextStyle( + style: const TextStyle( fontSize: 14, - color: onHover - ? editorState - .editorStyle.selectionMenuItemSelectedTextColor - : editorState - .editorStyle.selectionMenuItemTextColor, ), ), ), @@ -84,7 +73,6 @@ class ContextMenu extends StatelessWidget { minWidth: 140, ), decoration: BoxDecoration( - color: editorState.editorStyle.selectionMenuBackgroundColor, boxShadow: [ BoxShadow( blurRadius: 5, diff --git a/lib/src/service/internal_key_event_handlers/format_style_handler.dart b/lib/src/service/internal_key_event_handlers/format_style_handler.dart deleted file mode 100644 index 1535efdb9..000000000 --- a/lib/src/service/internal_key_event_handlers/format_style_handler.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; -import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; -import 'package:flutter/material.dart'; - -import 'package:appflowy_editor/src/core/document/node.dart'; - -ShortcutEventHandler formatBoldEventHandler = (editorState, event) { - final selection = editorState.service.selectionService.currentSelection.value; - final nodes = editorState.service.selectionService.currentSelectedNodes; - final textNodes = nodes.whereType().toList(growable: false); - if (selection == null || textNodes.isEmpty) { - return KeyEventResult.ignored; - } - formatBold(editorState); - return KeyEventResult.handled; -}; - -ShortcutEventHandler formatItalicEventHandler = (editorState, event) { - final selection = editorState.service.selectionService.currentSelection.value; - final nodes = editorState.service.selectionService.currentSelectedNodes; - final textNodes = nodes.whereType().toList(growable: false); - if (selection == null || textNodes.isEmpty) { - return KeyEventResult.ignored; - } - formatItalic(editorState); - return KeyEventResult.handled; -}; - -ShortcutEventHandler formatUnderlineEventHandler = (editorState, event) { - final selection = editorState.service.selectionService.currentSelection.value; - final nodes = editorState.service.selectionService.currentSelectedNodes; - final textNodes = nodes.whereType().toList(growable: false); - if (selection == null || textNodes.isEmpty) { - return KeyEventResult.ignored; - } - formatUnderline(editorState); - return KeyEventResult.handled; -}; - -ShortcutEventHandler formatStrikethroughEventHandler = (editorState, event) { - final selection = editorState.service.selectionService.currentSelection.value; - final nodes = editorState.service.selectionService.currentSelectedNodes; - final textNodes = nodes.whereType().toList(growable: false); - if (selection == null || textNodes.isEmpty) { - return KeyEventResult.ignored; - } - formatStrikethrough(editorState); - return KeyEventResult.handled; -}; - -ShortcutEventHandler formatHighlightEventHandler = (editorState, event) { - final selection = editorState.service.selectionService.currentSelection.value; - final nodes = editorState.service.selectionService.currentSelectedNodes; - final textNodes = nodes.whereType().toList(growable: false); - if (selection == null || textNodes.isEmpty) { - return KeyEventResult.ignored; - } - formatHighlight( - editorState, - editorState.editorStyle.highlightColorHex!, - ); - return KeyEventResult.handled; -}; - -ShortcutEventHandler formatLinkEventHandler = (editorState, event) { - final selection = editorState.service.selectionService.currentSelection.value; - final nodes = editorState.service.selectionService.currentSelectedNodes; - final textNodes = nodes.whereType().toList(growable: false); - if (selection == null || textNodes.isEmpty) { - return KeyEventResult.ignored; - } - if (editorState.service.toolbarService - ?.triggerHandler('appflowy.toolbar.link') == - true) { - return KeyEventResult.handled; - } - return KeyEventResult.ignored; -}; - -ShortcutEventHandler formatEmbedCodeEventHandler = (editorState, event) { - final selection = editorState.service.selectionService.currentSelection.value; - final nodes = editorState.service.selectionService.currentSelectedNodes; - final textNodes = nodes.whereType().toList(growable: false); - if (selection == null || textNodes.isEmpty) { - return KeyEventResult.ignored; - } - formatEmbedCode(editorState); - return KeyEventResult.ignored; -}; diff --git a/test/customer/custom_attribute_for_text_block_test.dart b/test/customer/custom_attribute_for_text_block_test.dart index 198630158..3cbd90294 100644 --- a/test/customer/custom_attribute_for_text_block_test.dart +++ b/test/customer/custom_attribute_for_text_block_test.dart @@ -29,7 +29,7 @@ class CustomAttributeKeyForTextBlock extends StatelessWidget { Widget build(BuildContext context) { final editorStyle = EditorStyle.desktop( // Example for customizing a new attribute key. - textSpanDecorator: (_, __, textInsert, textSpan) { + textSpanDecorator: (_, __, ___, textInsert, textSpan) { final attributes = textInsert.attributes; if (attributes == null) { return textSpan; @@ -76,7 +76,7 @@ class CustomAttributeKeyForTextBlock extends StatelessWidget { decoration: BoxDecoration( border: Border.all(color: Colors.blue), ), - child: AppFlowyEditor.standard( + child: AppFlowyEditor( editorState: editorState, editorStyle: editorStyle, ), diff --git a/test/customer/custom_block_icon_test.dart b/test/customer/custom_block_icon_test.dart index 2a83ebb3e..222325385 100644 --- a/test/customer/custom_block_icon_test.dart +++ b/test/customer/custom_block_icon_test.dart @@ -71,7 +71,7 @@ class CustomBlockIcon extends StatelessWidget { body: SafeArea( child: SizedBox( width: 500, - child: AppFlowyEditor.custom( + child: AppFlowyEditor( editorState: editorState, blockComponentBuilders: customBlockComponentBuilders, commandShortcutEvents: standardCommandShortcutEvents, diff --git a/test/customer/custom_image_menu_test.dart b/test/customer/custom_image_menu_test.dart index b8c61d033..0709a2105 100644 --- a/test/customer/custom_image_menu_test.dart +++ b/test/customer/custom_image_menu_test.dart @@ -70,7 +70,7 @@ class CustomImageMenu extends StatelessWidget { decoration: BoxDecoration( border: Border.all(color: Colors.blue), ), - child: AppFlowyEditor.custom( + child: AppFlowyEditor( editorState: editorState, blockComponentBuilders: customBlockComponentBuilders, commandShortcutEvents: standardCommandShortcutEvents, diff --git a/test/customer/text_field_and_editor_test.dart b/test/customer/text_field_and_editor_test.dart index 112fe315f..6bb7b72e0 100644 --- a/test/customer/text_field_and_editor_test.dart +++ b/test/customer/text_field_and_editor_test.dart @@ -56,7 +56,7 @@ class TextFieldAndEditor extends StatelessWidget { decoration: BoxDecoration( border: Border.all(color: Colors.blue), ), - child: AppFlowyEditor.standard( + child: AppFlowyEditor( focusNode: editorFocusNode, editorState: EditorState.blank(), editorStyle: const EditorStyle.mobile(), diff --git a/test/mobile/toolbar/mobile/test_helpers/mobile_app_with_toolbar_widget.dart b/test/mobile/toolbar/mobile/test_helpers/mobile_app_with_toolbar_widget.dart index 4403ee300..a8a5b6141 100644 --- a/test/mobile/toolbar/mobile/test_helpers/mobile_app_with_toolbar_widget.dart +++ b/test/mobile/toolbar/mobile/test_helpers/mobile_app_with_toolbar_widget.dart @@ -28,7 +28,7 @@ class MobileAppWithToolbarWidget extends StatelessWidget { home: Column( children: [ Expanded( - child: AppFlowyEditor.standard( + child: AppFlowyEditor( editorStyle: const EditorStyle.mobile(), editorState: editorState, scrollController: scrollController, diff --git a/test/mobile/toolbar/mobile/toolbar_items/code_mobile_toolbar_item_test.dart b/test/mobile/toolbar/mobile/toolbar_items/code_mobile_toolbar_item_test.dart index 82e76d7ba..89a1cf58d 100644 --- a/test/mobile/toolbar/mobile/toolbar_items/code_mobile_toolbar_item_test.dart +++ b/test/mobile/toolbar/mobile/toolbar_items/code_mobile_toolbar_item_test.dart @@ -37,7 +37,8 @@ void main() { expect( node?.allSatisfyInSelection(selection, (delta) { return delta.whereType().every( - (element) => element.attributes?[FlowyRichTextKeys.code] == true, + (element) => + element.attributes?[AppFlowyRichTextKeys.code] == true, ); }), true, diff --git a/test/mobile/toolbar/mobile/toolbar_items/color/text_and_background_color_tool_bar_item_test.dart b/test/mobile/toolbar/mobile/toolbar_items/color/text_and_background_color_tool_bar_item_test.dart index 30046c758..8c898f27a 100644 --- a/test/mobile/toolbar/mobile/toolbar_items/color/text_and_background_color_tool_bar_item_test.dart +++ b/test/mobile/toolbar/mobile/toolbar_items/color/text_and_background_color_tool_bar_item_test.dart @@ -57,7 +57,7 @@ void main() { node?.allSatisfyInSelection(selection, (delta) { return delta.whereType().every( (element) => - element.attributes?[FlowyRichTextKeys.textColor] == + element.attributes?[AppFlowyRichTextKeys.textColor] == Colors.red.toHex(), ); }), @@ -70,7 +70,7 @@ void main() { node?.allSatisfyInSelection(selection, (delta) { return delta.whereType().every( (element) => - element.attributes?[FlowyRichTextKeys.textColor] == null, + element.attributes?[AppFlowyRichTextKeys.textColor] == null, ); }), true, @@ -92,7 +92,7 @@ void main() { node?.allSatisfyInSelection(selection, (delta) { return delta.whereType().every( (element) => - element.attributes?[FlowyRichTextKeys.highlightColor] == + element.attributes?[AppFlowyRichTextKeys.highlightColor] == Colors.red.withOpacity(0.3).toHex(), ); }), @@ -105,7 +105,8 @@ void main() { node?.allSatisfyInSelection(selection, (delta) { return delta.whereType().every( (element) => - element.attributes?[FlowyRichTextKeys.highlightColor] == null, + element.attributes?[AppFlowyRichTextKeys.highlightColor] == + null, ); }), true, diff --git a/test/mobile/toolbar/mobile/toolbar_items/link_mobile_toolbar_item_test.dart b/test/mobile/toolbar/mobile/toolbar_items/link_mobile_toolbar_item_test.dart index 8d071fd1d..ee3e3b1f7 100644 --- a/test/mobile/toolbar/mobile/toolbar_items/link_mobile_toolbar_item_test.dart +++ b/test/mobile/toolbar/mobile/toolbar_items/link_mobile_toolbar_item_test.dart @@ -52,7 +52,7 @@ void main() { node?.allSatisfyInSelection(selection, (delta) { return delta.whereType().every( (element) => - element.attributes?[FlowyRichTextKeys.href] == linkAddress, + element.attributes?[AppFlowyRichTextKeys.href] == linkAddress, ); }), true, diff --git a/test/mobile/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item_test.dart b/test/mobile/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item_test.dart index ce262d3b4..1f68e5e57 100644 --- a/test/mobile/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item_test.dart +++ b/test/mobile/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item_test.dart @@ -2,13 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import '../../../../new/infra/testable_editor.dart'; -import '../test_helpers/mobile_app_with_toolbar_widget.dart'; void main() { testWidgets('textDecorationMobileToolbarItem', (WidgetTester tester) async { const text = 'Welcome to Appflowy 😁'; final editor = tester.editor..addParagraphs(3, initialText: text); - await editor.startTesting(); + await editor.startTesting( + inMobile: true, + withFloatingToolbar: true, + ); var selection = Selection.single( path: [1], @@ -17,16 +19,6 @@ void main() { ); await editor.updateSelection(selection); - await tester.pumpWidget( - Material( - child: MobileAppWithToolbarWidget( - editorState: editor.editorState, - toolbarItems: [ - textDecorationMobileToolbarItem, - ], - ), - ), - ); // Tap text decoration toolbar item await tester.tap(find.byType(IconButton).first); @@ -34,7 +26,10 @@ void main() { // Show its menu and it has 4 buttons expect(find.byType(MobileToolbarItemMenu), findsOneWidget); - expect(find.text(AppFlowyEditorLocalizations.current.bold), findsOneWidget); + expect( + find.text(AppFlowyEditorLocalizations.current.bold), + findsOneWidget, + ); expect( find.text(AppFlowyEditorLocalizations.current.italic), findsOneWidget, @@ -60,7 +55,8 @@ void main() { expect( node?.allSatisfyInSelection(selection, (delta) { return delta.whereType().every( - (element) => element.attributes?[FlowyRichTextKeys.bold] == true, + (element) => + element.attributes?[AppFlowyRichTextKeys.bold] == true, ); }), true, @@ -78,7 +74,7 @@ void main() { node?.allSatisfyInSelection(selection, (delta) { return delta.whereType().every( (element) => - element.attributes?[FlowyRichTextKeys.italic] == true, + element.attributes?[AppFlowyRichTextKeys.italic] == true, ); }), true, @@ -96,7 +92,7 @@ void main() { node?.allSatisfyInSelection(selection, (delta) { return delta.whereType().every( (element) => - element.attributes?[FlowyRichTextKeys.underline] == true, + element.attributes?[AppFlowyRichTextKeys.underline] == true, ); }), true, @@ -114,7 +110,8 @@ void main() { node?.allSatisfyInSelection(selection, (delta) { return delta.whereType().every( (element) => - element.attributes?[FlowyRichTextKeys.strikethrough] == true, + element.attributes?[AppFlowyRichTextKeys.strikethrough] == + true, ); }), true, diff --git a/test/new/infra/testable_editor.dart b/test/new/infra/testable_editor.dart index 849b44a33..302f45321 100644 --- a/test/new/infra/testable_editor.dart +++ b/test/new/infra/testable_editor.dart @@ -36,6 +36,7 @@ class TestableEditor { bool editable = true, bool shrinkWrap = false, bool withFloatingToolbar = false, + bool inMobile = false, ScrollController? scrollController, Widget Function(Widget child)? wrapper, }) async { @@ -44,30 +45,59 @@ class TestableEditor { if (withFloatingToolbar) { scrollController ??= ScrollController(); } - Widget editor = AppFlowyEditor.standard( + Widget editor = AppFlowyEditor( editorState: editorState, editable: editable, autoFocus: autoFocus, shrinkWrap: shrinkWrap, scrollController: scrollController, + editorStyle: + inMobile ? const EditorStyle.mobile() : const EditorStyle.desktop(), ); if (withFloatingToolbar) { - editor = FloatingToolbar( - items: [ - paragraphItem, - ...headingItems, - ...markdownFormatItems, - quoteItem, - bulletedListItem, - numberedListItem, - linkItem, - buildTextColorItem(), - buildHighlightColorItem() - ], - editorState: editorState, - scrollController: scrollController!, - child: editor, - ); + if (inMobile) { + final items = [ + textDecorationMobileToolbarItem, + headingMobileToolbarItem, + todoListMobileToolbarItem, + listMobileToolbarItem, + linkMobileToolbarItem, + quoteMobileToolbarItem, + codeMobileToolbarItem, + ]; + editor = Column( + children: [ + Expanded( + child: AppFlowyEditor( + editorStyle: const EditorStyle.mobile(), + editorState: editorState, + scrollController: scrollController, + ), + ), + MobileToolbar( + editorState: editorState, + toolbarItems: items, + ), + ], + ); + } else { + editor = FloatingToolbar( + items: [ + paragraphItem, + ...headingItems, + ...markdownFormatItems, + quoteItem, + bulletedListItem, + numberedListItem, + linkItem, + buildTextColorItem(), + buildHighlightColorItem() + ], + editorState: editorState, + scrollController: scrollController!, + child: editor, + ); + } } await tester.pumpWidget( MaterialApp( diff --git a/test/new/service/shortcuts/command_shortcut_events/show_link_menu_command_test.dart b/test/new/service/shortcuts/command_shortcut_events/show_link_menu_command_test.dart index 1617b7d42..6f17f68ca 100644 --- a/test/new/service/shortcuts/command_shortcut_events/show_link_menu_command_test.dart +++ b/test/new/service/shortcuts/command_shortcut_events/show_link_menu_command_test.dart @@ -45,7 +45,7 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { ], editorState: editor.editorState, scrollController: scrollController, - child: AppFlowyEditor.standard(editorState: editor.editorState), + child: AppFlowyEditor(editorState: editor.editorState), ); await tester.pumpWidget( diff --git a/test/new/toolbar/desktop/items/link/link_menu_test.dart b/test/new/toolbar/desktop/items/link/link_menu_test.dart index 5133e5623..5496a9e3e 100644 --- a/test/new/toolbar/desktop/items/link/link_menu_test.dart +++ b/test/new/toolbar/desktop/items/link/link_menu_test.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_editor/src/core/document/text_delta.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/toolbar/desktop/items/link/link_menu.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -49,19 +49,25 @@ void main() async { const link = 'appflowy.io'; final editor = tester.editor; - //create a link [appflowy.io](appflowy.io) + // create a link [appflowy.io](appflowy.io) editor.addParagraph( - builder: (index) => Delta()..insert(link, attributes: {"href": link}), + builder: (index) => Delta() + ..insert( + link, + attributes: { + AppFlowyRichTextKeys.href: link, + }, + ), ); - await editor.startTesting(); - await tester.pumpAndSettle(); + final finder = find.text(link, findRichText: true); expect(finder, findsOneWidget); // tap the link await tester.tap(finder); - await tester.pumpAndSettle(const Duration(milliseconds: 350)); + tester.binding.scheduleWarmUpFrame(); + await tester.pumpAndSettle(const Duration(seconds: 1)); final linkMenu = find.byType(LinkMenu); expect(linkMenu, findsOneWidget); expect(find.text(link, findRichText: true), findsNWidgets(2)); diff --git a/test/render/image/image_node_builder_test.dart b/test/render/image/image_node_builder_test.dart index fd6e1cd13..61b46098a 100644 --- a/test/render/image/image_node_builder_test.dart +++ b/test/render/image/image_node_builder_test.dart @@ -133,12 +133,12 @@ void main() async { final leftImageRect = tester.getRect(imageFinder.at(0)); expect( leftImageRect.left, - editor.editorState.editorStyle.padding!.left, + editor.editorState.editorStyle.padding.left, ); final rightImageRect = tester.getRect(imageFinder.at(2)); expect( rightImageRect.right, - editorRect.right - editor.editorState.editorStyle.padding!.right, + editorRect.right - editor.editorState.editorStyle.padding.right, ); final centerImageRect = tester.getRect(imageFinder.at(1)); expect(