diff --git a/CHANGELOG.md b/CHANGELOG.md index 2333890e8..40c9dc347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## 1.1.0 +* feat: support IME by @LucasXu0 in ([#253](https://github.com/AppFlowy-IO/appflowy-editor/pull/253)) +* feat: support text and background color in mobile toolbar by @hyj1204 in ([#233](https://github.com/AppFlowy-IO/appflowy-editor/pull/233)) +* feat: support broadcast the transaction before applying it by @LucasXu0 in ([#226](https://github.com/AppFlowy-IO/appflowy-editor/pull/226)) +* feat: support customizing text attribute key and rendering by @LucasXu0 in ([#244](https://github.com/AppFlowy-IO/appflowy-editor/pull/244)) +* feat: support customizing the block icon widget by @LucasXu0 in ([#274](https://github.com/AppFlowy-IO/appflowy-editor/pull/274)) +* feat: support uploading images from local files by @Mukund-Tandon in ([#232](https://github.com/AppFlowy-IO/appflowy-editor/pull/232)) +* feat: add underline syntax parser by @vedant-pandey in ([#256](https://github.com/AppFlowy-IO/appflowy-editor/pull/256)) +* feat: migrate the delta encoder by @LucasXu0 in ([#277](https://github.com/AppFlowy-IO/appflowy-editor/pull/277)) +* feat: support divider toolbar item in mobile by @hyj1204 in ([#281](https://github.com/AppFlowy-IO/appflowy-editor/pull/281)) +* feat: customized color options by @hyj1204 in ([#270](https://github.com/AppFlowy-IO/appflowy-editor/pull/270)) +* feat: support exiting link menu by ESC by @vincenzoursano in ([#124](https://github.com/AppFlowy-IO/appflowy-editor/pull/124)) +* fix: focus node doesn't work on mobile by @LucasXu0 in ([#227](https://github.com/AppFlowy-IO/appflowy-editor/pull/227)) +* fix: the cursor is inaccuracy when the text contains special emoji by @LucasXu0 in ([#238](https://github.com/AppFlowy-IO/appflowy-editor/pull/238)) +* fix: extend attribute keys shouldn't be sliced by @LucasXu0 in ([#248](https://github.com/AppFlowy-IO/appflowy-editor/pull/248)) +* fix: keep keyboard appearance as same brightness as system theme by @hyj1204 in ([#264](https://github.com/AppFlowy-IO/appflowy-editor/pull/264)) + ## 1.0.4 * feat: support mobile drag selection by @LucasXu0 in ([#209](https://github.com/AppFlowy-IO/appflowy-editor/pull/209)) * feat: support customizing number of the numbered list by @LucasXu0 in ([#219](https://github.com/AppFlowy-IO/appflowy-editor/pull/219)) diff --git a/README.md b/README.md index e795dabd0..a65fd937d 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,13 @@ and the Flutter guide for ## Key Features * Build rich, intuitive editors -* Design and modify an ever expanding list of customizable features including - * components (such as form input controls, numbered lists, and rich text widgets) +* Design and modify an ever-expanding list of customizable features including + * block components (such as form input controls, numbered lists, and rich text widgets) * shortcut events * themes - * menu options (**coming soon!**) -* [Test-coverage](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/documentation/testing.md) and ongoing maintenance by AppFlowy's core team and community of more than 1,000 builders + * selection menu + * toolbar menu +* [Test Coverage](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/documentation/testing.md) and ongoing maintenance by AppFlowy's core team and community of more than 1,000 builders ## Getting Started @@ -54,28 +55,28 @@ flutter pub get Start by creating a new empty AppFlowyEditor object. ```dart -final editorState = EditorState.empty(); // an empty state -final editor = AppFlowyEditor( - editorState: editorState, +final editorState = EditorState.blank(withInitialText: true); // with an empty paragraph +final editor = AppFlowyEditor.standard( + editorState: editorState, ); ``` You can also create an editor from a JSON object in order to configure your initial state. Or you can [create an editor from Markdown or Quill Delta](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/documentation/importing.md). ```dart -final json = ...; -final editorState = EditorState(Document.fromJson(data)); -final editor = AppFlowyEditor( - editorState: editorState, +final json = jsonDecode('YOUR INPUT JSON STRING'); +final editorState = EditorState(document: Document.fromJson(json)); +final editor = AppFlowyEditor.standard( + editorState: editorState, ); ``` > Note: The parameters `localizationsDelegates` need to be assigned in MaterialApp widget ```dart MaterialApp( - localizationsDelegates: const [ - AppFlowyEditorLocalizations.delegate, - ], + localizationsDelegates: const [ + AppFlowyEditorLocalizations.delegate, + ], ); ``` @@ -89,15 +90,15 @@ flutter run ## Customizing Your Editor -### Customizing Components +### Customizing Block Components Please refer to our documentation on customizing AppFlowy for a detailed discussion about [customizing components](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/documentation/customizing.md#customize-a-component). Below are some examples of component customizations: - * [Checkbox Text](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/lib/src/render/rich_text/checkbox_text.dart) demonstrates how to extend new styles based on existing rich text components - * [Image](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/example/lib/plugin/network_image_node_widget.dart) demonstrates how to extend a new node and render it - * See further examples of [rich-text plugins](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/lib/src/render/rich_text) + * [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) ### Customizing Shortcut Events @@ -105,9 +106,8 @@ Please refer to our documentation on customizing AppFlowy for a detailed discuss Below are some examples of shortcut event customizations: - * [BIUS](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/lib/src/service/internal_key_event_handlers/format_style_handler.dart) demonstrates how to make text bold/italic/underline/strikethrough through shortcut keys - * [Paste HTML](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart) gives you an idea on how to handle pasted styles through shortcut keys - * Need more examples? Check out [Internal key event handlers](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/lib/src/service/internal_key_event_handlers) + * [BIUS](https://github.com/AppFlowy-IO/appflowy-editor/tree/main/lib/src/editor/editor_component/service/shortcuts/character_shortcut_events/format_single_character) demonstrates how to make text bold/italic/underline/strikethrough through shortcut keys + * Need more examples? Check out [shortcuts](https://github.com/AppFlowy-IO/appflowy-editor/tree/main/lib/src/editor/editor_component/service/shortcuts) ## Glossary Please refer to the API documentation. diff --git a/documentation/customizing.md b/documentation/customizing.md index bd0016480..95df046c3 100644 --- a/documentation/customizing.md +++ b/documentation/customizing.md @@ -1,7 +1,5 @@ # Customizing Editor Features -Note: `AppFlowyEditor` has since been depreciated and `AppFlowyEditor.standard` or `AppFlowyEditor.custom` should be used instead. To recreate the examples below, you would use `AppFlowyEditor.custom`. - ## Customizing a Shortcut Event We will use a simple example to illustrate how to quickly add a shortcut event. @@ -11,18 +9,20 @@ In this example, text that starts and ends with an underscore ( \_ ) character w Let's start with a blank document: ```dart -@override -Widget build(BuildContext context) { - return Scaffold( - body: Container( - alignment: Alignment.topCenter, - child: AppFlowyEditor( - editorState: EditorState.empty(), - shortcutEvents: const [], - customBuilders: const {}, - ), - ), - ); +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +class UnderScoreToItalic extends StatelessWidget { + const UnderScoreToItalic({super.key}); + + @override + Widget build(BuildContext context) { + return AppFlowyEditor.custom( + editorState: EditorState.blank(withInitialText: true), + blockComponentBuilders: standardBlockComponentBuilderMap, + characterShortcutEvents: const [], + ); + } } ``` @@ -30,118 +30,69 @@ At this point, nothing magic will happen after typing `_xxx_`. ![Before](./images/customize_a_shortcut_event_before.gif) -To implement our shortcut event we will create a `ShortcutEvent` instance to handle an underscore input. +To implement our shortcut event we will create a `CharacterShortcutEvent` instance to handle an underscore input. -We need to define `key` and `command` in a ShortCutEvent object to customize hotkeys. We recommend using the description of your event as a key. For example, if the underscore `_` is defined to make text italic, the key can be 'Underscore to italic'. +We need to define `key` and `character` in a `CharacterShortcutEvent` object to customize hotkeys. We recommend using the description of your event as a key. For example, if the underscore `_` is defined to make text italic, the key can be 'Underscore to italic'. -> The command, made up of a single keyword such as `underscore` or a combination of keywords using the `+` sign in between to concatenate, is a condition that triggers a user-defined function. To see which keywords are available to define a command, please refer to [key_mapping.dart](../lib/src/service/shortcut_event/key_mapping.dart). -> If more than one commands trigger the same handler, then we use ',' to split them. For example, using CTRL and A or CMD and A to 'select all', we describe it as `cmd+a,ctrl+a`(case-insensitive). ```dart import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; -ShortcutEvent underscoreToItalicEvent = ShortcutEvent( +// ... + +CharacterShortcutEvent underscoreToItalicEvent = CharacterShortcutEvent( key: 'Underscore to italic', - command: 'shift+underscore', - handler: _underscoreToItalicHandler, + character: '_', + handler: (editorState) async => handleFormatByWrappingWithSingleCharacter( + editorState: editorState, + character: '_', + formatStyle: FormatStyleByWrappingWithSingleChar.italic, + ), ); - -ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) { - -}; ``` -Then, we need to determine if the currently selected node is a `TextNode` and if the selection is collapsed. - -If so, we will continue. +Now our 'underscore handler' function is done and the only task left is to inject it into the AppFlowyEditor. ```dart -// ... -ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) { - // Obtain the selection and selected nodes of the current document through the 'selectionService' - // to determine whether the selection is collapsed and whether the selected node is a text node. - final selectionService = editorState.service.selectionService; - final selection = selectionService.currentSelection.value; - final textNodes = selectionService.currentSelectedNodes.whereType(); - if (selection == null || !selection.isSingle || textNodes.length != 1) { - return KeyEventResult.ignored; - } -``` - -Now, we deal with handling the underscore. - -Look for the position of the previous underscore and -1. if one is _not_ found, return without doing anything. -2. if one is found, the text enclosed within the two underscores will be formatted to display in italics. +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; -```dart -// ... -ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) { - // ... - - final textNode = textNodes.first; - final text = textNode.toRawString(); - // Determine if an 'underscore' already exists in the text node and only once. - final firstUnderscore = text.indexOf('_'); - final lastUnderscore = text.lastIndexOf('_'); - if (firstUnderscore == -1 || - firstUnderscore != lastUnderscore || - firstUnderscore == selection.start.offset - 1) { - return KeyEventResult.ignored; - } +class UnderScoreToItalic extends StatelessWidget { + const UnderScoreToItalic({super.key}); - // Delete the previous 'underscore', - // update the style of the text surrounded by the two underscores to 'italic', - // and update the cursor position. - final transaction = editorState.transaction - ..deleteText(textNode, firstUnderscore, 1) - ..formatText( - textNode, - firstUnderscore, - selection.end.offset - firstUnderscore - 1, - { - BuiltInAttributeKey.italic: true, - }, - ) - ..afterSelection = Selection.collapsed( - Position( - path: textNode.path, - offset: selection.end.offset - 1, - ), + @override + Widget build(BuildContext context) { + return AppFlowyEditor.custom( + editorState: EditorState.blank(withInitialText: true), + blockComponentBuilders: standardBlockComponentBuilderMap, + characterShortcutEvents: [ + underScoreToItalicEvent, + ], ); - editorState.apply(transaction); - - return KeyEventResult.handled; -}; -``` - -Now our 'underscore handler' function is done and the only task left is to inject it into the AppFlowyEditor. - -```dart -@override -Widget build(BuildContext context) { - return Scaffold( - body: Container( - alignment: Alignment.topCenter, - child: AppFlowyEditor( - editorState: EditorState.empty(), - customBuilders: const {}, - shortcutEvents: [ - underscoreToItalic, - ], - ), - ), - ); + } } + +CharacterShortcutEvent underScoreToItalicEvent = CharacterShortcutEvent( + key: 'Underscore to italic', + character: '_', + handler: (editorState) async => handleFormatByWrappingWithSingleCharacter( + editorState: editorState, + character: '_', + formatStyle: FormatStyleByWrappingWithSingleChar.italic, + ), +); ``` ![After](./images/customize_a_shortcut_event_after.gif) -Check out the [complete code](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart) file of this example. +Check out the [complete code](https://github.com/AppFlowy-IO/appflowy-editor/blob/main/example/lib/samples/underscore_to_italic.dart) file of this example. ## Customizing a Component + +> ⚠️ 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. diff --git a/lib/src/editor/editor_component/service/shortcuts/character_shortcut_events/format_single_character/format_single_character.dart b/lib/src/editor/editor_component/service/shortcuts/character_shortcut_events/format_single_character/format_single_character.dart index e88028431..f11c799d0 100644 --- a/lib/src/editor/editor_component/service/shortcuts/character_shortcut_events/format_single_character/format_single_character.dart +++ b/lib/src/editor/editor_component/service/shortcuts/character_shortcut_events/format_single_character/format_single_character.dart @@ -78,7 +78,7 @@ bool handleFormatByWrappingWithSingleCharacter({ // if the text is already formatted, we should remove the format. final sliced = delta.slice( headCharIndex + 1, - selection.end.offset - headCharIndex - 1, + selection.end.offset, ); final result = sliced.everyAttributes((element) => element[style] == true); diff --git a/pubspec.yaml b/pubspec.yaml index d2d6077b5..5f6bd9c47 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: appflowy_editor description: A highly customizable rich-text editor for Flutter. The AppFlowy Editor project for AppFlowy and beyond. -version: 1.0.4 +version: 1.1.0 homepage: https://github.com/AppFlowy-IO/appflowy-editor platforms: diff --git a/test/mobile/toolbar/mobile/toolbar_items/divider_mobile_toolbar_item_test.dart b/test/mobile/toolbar/mobile/toolbar_items/divider_mobile_toolbar_item_test.dart index 59cad71cb..879692cb8 100644 --- a/test/mobile/toolbar/mobile/toolbar_items/divider_mobile_toolbar_item_test.dart +++ b/test/mobile/toolbar/mobile/toolbar_items/divider_mobile_toolbar_item_test.dart @@ -5,7 +5,7 @@ import '../../../../new/infra/testable_editor.dart'; import '../test_helpers/mobile_app_with_toolbar_widget.dart'; void main() { - group('dividerMobileToolbarItem\n', () { + group('dividerMobileToolbarItem', () { testWidgets( 'If the user tries to insert a divider while some text is selected, no action should be taken', (WidgetTester tester) async { diff --git a/test/new/service/shortcuts/character_shortcut_events/format_by_wrapping_with_single_char/format_italic_test.dart b/test/new/service/shortcuts/character_shortcut_events/format_by_wrapping_with_single_char/format_italic_test.dart index f2aa5f564..2b4237788 100644 --- a/test/new/service/shortcuts/character_shortcut_events/format_by_wrapping_with_single_char/format_italic_test.dart +++ b/test/new/service/shortcuts/character_shortcut_events/format_by_wrapping_with_single_char/format_italic_test.dart @@ -150,9 +150,12 @@ void main() async { expect(result, true); final after = editorState.getNodeAtPath([0])!; - expect(after.delta!.toPlainText(), '$text1$text2'); - expect(after.delta!.toList()[0].attributes, null); - expect(after.delta!.toList()[1].attributes, {'italic': true}); + final afterDelta = after.delta!; + expect(afterDelta.toPlainText(), '$text1$text2'); + final deltaList = afterDelta.toList(); + expect(deltaList.length, 2); + expect(deltaList[0].attributes, null); + expect(deltaList[1].attributes, {'italic': true}); }); // Before