diff --git a/lib/src/editor_state.dart b/lib/src/editor_state.dart index 4b214adad661..ea98fbb65148 100644 --- a/lib/src/editor_state.dart +++ b/lib/src/editor_state.dart @@ -213,13 +213,11 @@ class EditorState { return; } - final tr = transaction; - // Rules - _insureLastNodeEditable(tr); + _insureLastNodeEditable(transaction); - if (tr.operations.isNotEmpty) { - apply(tr, ruleCount: ruleCount + 1, withUpdateCursor: false); + if (transaction.operations.isNotEmpty) { + apply(transaction, ruleCount: ruleCount + 1, withUpdateCursor: false); } } diff --git a/lib/src/infra/html_converter.dart b/lib/src/infra/html_converter.dart index 30369cafe70a..0ccec5ce551f 100644 --- a/lib/src/infra/html_converter.dart +++ b/lib/src/infra/html_converter.dart @@ -138,12 +138,8 @@ class HTMLToNodesConverter { } else { final delta = Delta(); delta.insert(element.text); - if (delta.isNotEmpty) { - return [TextNode(delta: delta)]; - } + return [TextNode(delta: delta)]; } - - return []; } Node _handleParagraph(html.Element element, @@ -240,12 +236,14 @@ class HTMLToNodesConverter { } else if (element.localName == HTMLTag.strong || element.localName == HTMLTag.bold) { delta.insert(element.text, attributes: {BuiltInAttributeKey.bold: true}); + } else if ([HTMLTag.em, HTMLTag.italic].contains(element.localName)) { + delta.insert( + element.text, + attributes: {BuiltInAttributeKey.italic: true}, + ); } else if (element.localName == HTMLTag.underline) { delta.insert(element.text, attributes: {BuiltInAttributeKey.underline: true}); - } else if ([HTMLTag.italic, HTMLTag.em].contains(element.localName)) { - delta - .insert(element.text, attributes: {BuiltInAttributeKey.italic: true}); } else if (element.localName == HTMLTag.del) { delta.insert(element.text, attributes: {BuiltInAttributeKey.strikethrough: true}); diff --git a/lib/src/render/image/image_upload_widget.dart b/lib/src/render/image/image_upload_widget.dart index ac5f448bc96f..64e927de9138 100644 --- a/lib/src/render/image/image_upload_widget.dart +++ b/lib/src/render/image/image_upload_widget.dart @@ -15,25 +15,19 @@ void showImageUploadMenu( menuService.dismiss(); _imageUploadMenu?.remove(); - _imageUploadMenu = OverlayEntry(builder: (context) { - return Positioned( + _imageUploadMenu = OverlayEntry( + builder: (context) => Positioned( top: menuService.topLeft.dy, left: menuService.topLeft.dx, child: Material( child: ImageUploadMenu( editorState: editorState, - onSubmitted: (text) { - // _dismissImageUploadMenu(); - editorState.insertImageNode(text); - }, - onUpload: (text) { - // _dismissImageUploadMenu(); - editorState.insertImageNode(text); - }, + onSubmitted: editorState.insertImageNode, + onUpload: editorState.insertImageNode, ), ), - ); - }); + ), + ); Overlay.of(context).insert(_imageUploadMenu!); @@ -144,9 +138,7 @@ class _ImageUploadMenuState extends State { width: 24, height: 24, ), - onPressed: () { - _textEditingController.clear(); - }, + onPressed: _textEditingController.clear, ), border: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12.0)), @@ -169,9 +161,7 @@ class _ImageUploadMenuState extends State { ), ), ), - onPressed: () { - widget.onUpload(_textEditingController.text); - }, + onPressed: () => widget.onUpload(_textEditingController.text), child: const Text( 'Upload', style: TextStyle(color: Colors.white, fontSize: 14.0), @@ -181,7 +171,7 @@ class _ImageUploadMenuState extends State { } } -extension on EditorState { +extension InsertImage on EditorState { void insertImageNode(String src) { final selection = service.selectionService.currentSelection.value; if (selection == null) { diff --git a/lib/src/render/style/plugin_styles.dart b/lib/src/render/style/plugin_styles.dart index 19f70b2a9236..12c2198de9cb 100644 --- a/lib/src/render/style/plugin_styles.dart +++ b/lib/src/render/style/plugin_styles.dart @@ -7,6 +7,7 @@ Iterable> get lightPluginStyleExtension => [ CheckboxPluginStyle.light, NumberListPluginStyle.light, QuotedTextPluginStyle.light, + BulletedListPluginStyle.light, ]; Iterable> get darkPluginStyleExtension => [ diff --git a/test/infra/html_converter_test.dart b/test/infra/html_converter_test.dart index 407363027587..c81cd6da9c58 100644 --- a/test/infra/html_converter_test.dart +++ b/test/infra/html_converter_test.dart @@ -2,12 +2,27 @@ import 'package:appflowy_editor/src/infra/html_converter.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { + group('HTMLTag tests', () { + test('isTopLevel', () { + const topLevelTag = HTMLTag.h1; + const nTopLevelTag = HTMLTag.strong; + + expect(HTMLTag.isTopLevel(topLevelTag), true); + expect(HTMLTag.isTopLevel(nTopLevelTag), false); + }); + }); + group('HTMLConverter tests', () { - test('HTMLToNodesConverter', () { - final converter = HTMLToNodesConverter(rawHTML); - final nodes = converter.toNodes(); + test('HTMLToNodesConverter and NodesToHTMLConverter', () { + final fromHTMLConverter = HTMLToNodesConverter(rawHTML); + final nodes = fromHTMLConverter.toNodes(); expect(nodes.isNotEmpty, true); + + final toHTMLConverter = NodesToHTMLConverter(nodes: nodes); + final html = toHTMLConverter.toHTMLString(); + + expect(html.isNotEmpty, true); }); }); } @@ -16,11 +31,21 @@ const rawHTML = """

AppFlowyEditor

👋 Welcome to AppFlowy Editor

AppFlowy Editor is a highly customizable rich-text editor

-

Here is an example you can give a try

+
-Span element +

Here is an example your you can give a try

-Span element two +Span element + +Span element two + +Span element three + +This is an anchor tag! + + + +

Features!

  • [x] Customizable
  • @@ -39,11 +64,11 @@ const rawHTML = """

    AppFlowyEditor

    This is a quote!

    -
    -  
    -    Code block
    -  
    -
    + + Code block + + +Italic one Italic two Bold tag AppFlowy diff --git a/test/infra/test_editor.dart b/test/infra/test_editor.dart index 0352a3add433..2e33aa394808 100644 --- a/test/infra/test_editor.dart +++ b/test/infra/test_editor.dart @@ -30,25 +30,26 @@ class EditorWidgetTester { bool autoFocus = false, bool editable = true, }) async { - final app = MaterialApp( - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - AppFlowyEditorLocalizations.delegate, - ], - supportedLocales: AppFlowyEditorLocalizations.delegate.supportedLocales, - locale: locale, - home: Scaffold( - body: AppFlowyEditor( - editorState: _editorState, - shrinkWrap: shrinkWrap, - autoFocus: autoFocus, - editable: editable, + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + AppFlowyEditorLocalizations.delegate, + ], + supportedLocales: AppFlowyEditorLocalizations.delegate.supportedLocales, + locale: locale, + home: Scaffold( + body: AppFlowyEditor( + editorState: _editorState, + shrinkWrap: shrinkWrap, + autoFocus: autoFocus, + editable: editable, + ), ), ), ); - await tester.pumpWidget(app); await tester.pump(); return this; } diff --git a/test/render/image/image_upload_widget_test.dart b/test/render/image/image_upload_widget_test.dart new file mode 100644 index 000000000000..dd55f8f23252 --- /dev/null +++ b/test/render/image/image_upload_widget_test.dart @@ -0,0 +1,43 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/image/image_upload_widget.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../infra/test_editor.dart'; + +void main() { + group('ImageUploadMenu tests', () { + testWidgets('showImageUploadMenu', (tester) async { + final editor = tester.editor..insertTextNode('Welcome to AppFlowy'); + await editor.startTesting(); + + await editor.updateSelection( + Selection.single(path: [0], startOffset: 19), + ); + + await editor.pressLogicKey(character: '/'); + await tester.pumpAndSettle(); + + expect(find.byType(SelectionMenuWidget), findsOneWidget); + + final imageMenuItemFinder = find.text('Image'); + expect(imageMenuItemFinder, findsOneWidget); + + await tester.tap(imageMenuItemFinder); + await tester.pumpAndSettle(); + }); + + testWidgets('insertImageNode extension', (tester) async { + final editor = tester.editor..insertTextNode('Welcome to AppFlowy'); + await editor.startTesting(); + + await editor.updateSelection( + Selection.single(path: [0], startOffset: 19), + ); + + editor.editorState.insertImageNode('no_src'); + await tester.pumpAndSettle(); + + expect(editor.documentLength, 2); + }); + }); +} diff --git a/test/render/selection_menu/selection_menu_widget_test.dart b/test/render/selection_menu/selection_menu_widget_test.dart index 55b6a76ebb8c..a590b140f402 100644 --- a/test/render/selection_menu/selection_menu_widget_test.dart +++ b/test/render/selection_menu/selection_menu_widget_test.dart @@ -218,13 +218,19 @@ Future _prepare(WidgetTester tester) async { } Future _testDefaultSelectionMenuItems( - int index, EditorWidgetTester editor) async { + int index, + EditorWidgetTester editor, +) async { expect(editor.documentLength, 4); expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0)); - expect((editor.nodeAtPath([0]) as TextNode).toPlainText(), - 'Welcome to Appflowy 😁'); - expect((editor.nodeAtPath([1]) as TextNode).toPlainText(), - 'Welcome to Appflowy 😁'); + expect( + (editor.nodeAtPath([0]) as TextNode).toPlainText(), + 'Welcome to Appflowy 😁', + ); + expect( + (editor.nodeAtPath([1]) as TextNode).toPlainText(), + 'Welcome to Appflowy 😁', + ); final node = editor.nodeAtPath([2]); final item = defaultSelectionMenuItems[index]; if (item.name == 'Text') { @@ -252,8 +258,10 @@ Future _testDefaultSelectionMenuItems( SelectionMenuItemWidget getSelectedMenuItem(WidgetTester tester) { return tester - .state(find.byWidgetPredicate( - (widget) => widget is SelectionMenuItemWidget && widget.isSelected, - )) + .state( + find.byWidgetPredicate( + (widget) => widget is SelectionMenuItemWidget && widget.isSelected, + ), + ) .widget as SelectionMenuItemWidget; } diff --git a/test/render/style/editor_style_test.dart b/test/render/style/editor_style_test.dart new file mode 100644 index 000000000000..7a12157f9bf3 --- /dev/null +++ b/test/render/style/editor_style_test.dart @@ -0,0 +1,64 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('EditorStyle tests', () { + test('extensions', () { + final lightExtensions = lightEditorStyleExtension; + expect(lightExtensions.length, 1); + expect(lightExtensions.contains(EditorStyle.light), true); + + final darkExtensions = darkEditorStyleExtension; + expect(darkExtensions.length, 1); + expect(darkExtensions.contains(EditorStyle.dark), true); + }); + + test('EditorStyle members', () { + EditorStyle style = EditorStyle.light; + expect(style.padding, isNot(EdgeInsets.zero)); + + style = style.copyWith(padding: EdgeInsets.zero); + expect(style.padding, EdgeInsets.zero); + }); + + testWidgets('EditorStyle.of not found', (tester) async { + late BuildContext context; + + await tester.pumpWidget( + Builder(builder: (ctx) { + context = ctx; + return const SizedBox.shrink(); + }), + ); + + expect(EditorStyle.of(context), null); + }); + + testWidgets('EditorStyle.of found', (tester) async { + late BuildContext context; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData.light().copyWith( + extensions: [...lightEditorStyleExtension], + ), + home: Builder(builder: (ctx) { + context = ctx; + return const SizedBox.shrink(); + }), + ), + ); + + final editorStyle = EditorStyle.of(context); + expect(editorStyle, isNotNull); + expect(editorStyle!.backgroundColor, EditorStyle.light.backgroundColor); + }); + + test('EditorStyle.lerp', () { + final editorStyle = + EditorStyle.light.lerp(EditorStyle.dark, 1.0) as EditorStyle; + expect(editorStyle.backgroundColor, EditorStyle.dark.backgroundColor); + }); + }); +} diff --git a/test/render/style/plugin_styles_test.dart b/test/render/style/plugin_styles_test.dart new file mode 100644 index 000000000000..aefb14d2bbdf --- /dev/null +++ b/test/render/style/plugin_styles_test.dart @@ -0,0 +1,154 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../infra/test_editor.dart'; + +void main() { + group('PluginStyle tests', () { + test('extensions', () { + final lightExtensions = lightPluginStyleExtension; + expect(lightExtensions.length, 5); + expect(lightExtensions.contains(HeadingPluginStyle.light), true); + + final darkExtensions = darkPluginStyleExtension; + expect(darkExtensions.length, 5); + expect(darkExtensions.contains(HeadingPluginStyle.dark), true); + }); + + testWidgets('HeadingPluginStyle', (tester) async { + final editor = tester.editor..insertTextNode('Welcome to AppFlowy'); + await editor.startTesting(); + + HeadingPluginStyle style = HeadingPluginStyle.light; + style = style.copyWith( + padding: (_, __) => EdgeInsets.zero, + textStyle: (_, __) => _newTextStyle, + ); + + final padding = style.padding( + editor.editorState, + editor.editorState.getTextNode(path: [0]), + ); + expect(padding, EdgeInsets.zero); + + final textStyle = style.textStyle( + editor.editorState, + editor.editorState.getTextNode(path: [0]), + ); + expect(textStyle, _newTextStyle); + + style = style.lerp(HeadingPluginStyle.dark, 1.0) as HeadingPluginStyle; + expect(style.textStyle, HeadingPluginStyle.dark.textStyle); + }); + + testWidgets('CheckboxPluginStyle', (tester) async { + final editor = tester.editor..insertTextNode('Welcome to AppFlowy'); + await editor.startTesting(); + + CheckboxPluginStyle style = CheckboxPluginStyle.light; + style = style.copyWith( + padding: (_, __) => EdgeInsets.zero, + textStyle: (_, __) => _newTextStyle, + ); + + final padding = style.padding( + editor.editorState, + editor.editorState.getTextNode(path: [0]), + ); + expect(padding, EdgeInsets.zero); + + final textStyle = style.textStyle( + editor.editorState, + editor.editorState.getTextNode(path: [0]), + ); + expect(textStyle, _newTextStyle); + + style = style.lerp(CheckboxPluginStyle.dark, 1.0) as CheckboxPluginStyle; + expect(style.textStyle, CheckboxPluginStyle.dark.textStyle); + }); + + testWidgets('BulletedListPluginStyle', (tester) async { + final editor = tester.editor..insertTextNode('Welcome to AppFlowy'); + await editor.startTesting(); + + BulletedListPluginStyle style = BulletedListPluginStyle.light; + style = style.copyWith( + padding: (_, __) => EdgeInsets.zero, + textStyle: (_, __) => _newTextStyle, + ); + + final padding = style.padding( + editor.editorState, + editor.editorState.getTextNode(path: [0]), + ); + expect(padding, EdgeInsets.zero); + + final textStyle = style.textStyle( + editor.editorState, + editor.editorState.getTextNode(path: [0]), + ); + expect(textStyle, _newTextStyle); + + style = style.lerp(BulletedListPluginStyle.dark, 1.0) + as BulletedListPluginStyle; + expect(style.textStyle, BulletedListPluginStyle.dark.textStyle); + }); + + testWidgets('NumberListPluginStyle', (tester) async { + final editor = tester.editor..insertTextNode('Welcome to AppFlowy'); + await editor.startTesting(); + + NumberListPluginStyle style = NumberListPluginStyle.light; + style = style.copyWith( + padding: (_, __) => EdgeInsets.zero, + textStyle: (_, __) => _newTextStyle, + ); + + final padding = style.padding( + editor.editorState, + editor.editorState.getTextNode(path: [0]), + ); + expect(padding, EdgeInsets.zero); + + final textStyle = style.textStyle( + editor.editorState, + editor.editorState.getTextNode(path: [0]), + ); + expect(textStyle, _newTextStyle); + + style = + style.lerp(NumberListPluginStyle.dark, 1.0) as NumberListPluginStyle; + expect(style.textStyle, NumberListPluginStyle.dark.textStyle); + }); + + testWidgets('QuotedTextPluginStyle', (tester) async { + final editor = tester.editor..insertTextNode('Welcome to AppFlowy'); + await editor.startTesting(); + + QuotedTextPluginStyle style = QuotedTextPluginStyle.light; + style = style.copyWith( + padding: (_, __) => EdgeInsets.zero, + textStyle: (_, __) => _newTextStyle, + ); + + final padding = style.padding( + editor.editorState, + editor.editorState.getTextNode(path: [0]), + ); + expect(padding, EdgeInsets.zero); + + final textStyle = style.textStyle( + editor.editorState, + editor.editorState.getTextNode(path: [0]), + ); + expect(textStyle, _newTextStyle); + + style = + style.lerp(QuotedTextPluginStyle.dark, 1.0) as QuotedTextPluginStyle; + expect(style.textStyle, QuotedTextPluginStyle.dark.textStyle); + }); + }); +} + +const _newTextStyle = TextStyle(color: Colors.teal);