feat: support text align and block align (#3292)

* feat: support text align and block align

* test: add test
This commit is contained in:
Lucas.Xu 2023-08-30 17:21:32 +08:00 committed by GitHub
parent 897d7980f6
commit f73d59fb57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 241 additions and 15 deletions

View File

@ -0,0 +1,46 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document alignment', () {
testWidgets('edit alignment in toolbar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final selection = Selection.single(
path: [0],
startOffset: 0,
endOffset: 1,
);
// click the first line of the readme
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.updateSelection(selection);
await tester.pumpAndSettle();
// click the align center
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
// expect to see the align center
final editorState = tester.editor.getCurrentEditorState();
final first = editorState.getNodeAtPath([0])!;
expect(first.attributes[blockComponentAlign], 'center');
// click the align right
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
expect(first.attributes[blockComponentAlign], 'right');
// click the align left
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
expect(first.attributes[blockComponentAlign], 'left');
});
});
}

View File

@ -1,5 +1,8 @@
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'document_alignment_test.dart' as document_alignment_test;
import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
import 'document_create_and_delete_test.dart' import 'document_create_and_delete_test.dart'
as document_create_and_delete_test; as document_create_and_delete_test;
import 'document_with_cover_image_test.dart' as document_with_cover_image_test; import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
@ -7,11 +10,9 @@ import 'document_with_database_test.dart' as document_with_database_test;
import 'document_with_inline_math_equation_test.dart' import 'document_with_inline_math_equation_test.dart'
as document_with_inline_math_equation_test; as document_with_inline_math_equation_test;
import 'document_with_inline_page_test.dart' as document_with_inline_page_test; import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test; import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
import 'edit_document_test.dart' as document_edit_test; import 'edit_document_test.dart' as document_edit_test;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
void startTesting() { void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@ -27,4 +28,5 @@ void startTesting() {
document_with_toggle_list_test.main(); document_with_toggle_list_test.main();
document_copy_and_paste_test.main(); document_copy_and_paste_test.main();
document_codeblock_paste_test.main(); document_codeblock_paste_test.main();
document_alignment_test.main();
} }

View File

@ -1,14 +1,14 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
@ -409,6 +409,14 @@ extension CommonOperations on WidgetTester {
await gesture.up(); await gesture.up();
await pumpAndSettle(); await pumpAndSettle();
} }
// tap the button with [FlowySvgData]
Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
final button = find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg.path == svg.path,
);
await tapButton(button);
}
} }
extension ViewLayoutPBTest on ViewLayoutPB { extension ViewLayoutPBTest on ViewLayoutPB {

View File

@ -102,7 +102,8 @@ class _DocumentPageState extends State<DocumentPage> {
editorState: editorState!, editorState: editorState!,
styleCustomizer: EditorStyleCustomizer( styleCustomizer: EditorStyleCustomizer(
context: context, context: context,
padding: const EdgeInsets.symmetric(horizontal: 50), // the 44 is the width of the left action list
padding: const EdgeInsets.only(left: 40, right: 40 + 44),
), ),
header: _buildCoverAndIcon(context), header: _buildCoverAndIcon(context),
); );

View File

@ -66,6 +66,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
numberedListItem, numberedListItem,
inlineMathEquationItem, inlineMathEquationItem,
linkItem, linkItem,
alignToolbarItem,
buildTextColorItem(), buildTextColorItem(),
buildHighlightColorItem(), buildHighlightColorItem(),
]; ];

View File

@ -0,0 +1,153 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
final alignToolbarItem = ToolbarItem(
id: 'editor.align',
group: 4,
isActive: onlyShowInTextType,
builder: (context, editorState, highlightColor) {
final selection = editorState.selection!;
final nodes = editorState.getNodesInSelection(selection);
bool isSatisfyCondition(bool Function(Object? value) test) {
return nodes.every(
(n) => test(n.attributes[blockComponentAlign]),
);
}
bool isHighlight = false;
FlowySvgData data = FlowySvgs.toolbar_align_left_s;
if (isSatisfyCondition((value) => value == 'left')) {
isHighlight = true;
data = FlowySvgs.toolbar_align_left_s;
} else if (isSatisfyCondition((value) => value == 'center')) {
isHighlight = true;
data = FlowySvgs.toolbar_align_center_s;
} else if (isSatisfyCondition((value) => value == 'right')) {
isHighlight = true;
data = FlowySvgs.toolbar_align_right_s;
}
final child = FlowySvg(
data,
size: const Size.square(16),
color: isHighlight ? highlightColor : Colors.white,
);
return _AlignmentButtons(
child: child,
onAlignChanged: (align) async {
await editorState.updateNode(
selection,
(node) => node.copyWith(
attributes: {
...node.attributes,
blockComponentAlign: align,
},
),
);
},
);
},
);
class _AlignmentButtons extends StatefulWidget {
const _AlignmentButtons({
required this.child,
required this.onAlignChanged,
});
final Widget child;
final Function(String align) onAlignChanged;
@override
State<_AlignmentButtons> createState() => _AlignmentButtonsState();
}
class _AlignmentButtonsState extends State<_AlignmentButtons> {
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
windowPadding: const EdgeInsets.all(0),
margin: const EdgeInsets.all(0),
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 10),
child: widget.child,
popupBuilder: (_) => _AlignButtons(onAlignChanged: widget.onAlignChanged),
);
}
}
class _AlignButtons extends StatelessWidget {
const _AlignButtons({
required this.onAlignChanged,
});
final Function(String align) onAlignChanged;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 32,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(4),
_AlignButton(
icon: FlowySvgs.toolbar_align_left_s,
onTap: () => onAlignChanged('left'),
),
const _Divider(),
_AlignButton(
icon: FlowySvgs.toolbar_align_center_s,
onTap: () => onAlignChanged('center'),
),
const _Divider(),
_AlignButton(
icon: FlowySvgs.toolbar_align_right_s,
onTap: () => onAlignChanged('right'),
),
const HSpace(4),
],
),
);
}
}
class _AlignButton extends StatelessWidget {
const _AlignButton({
required this.icon,
required this.onTap,
});
final FlowySvgData icon;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: FlowySvg(
icon,
size: const Size.square(16),
),
);
}
}
class _Divider extends StatelessWidget {
const _Divider();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Container(
width: 1,
color: Colors.grey,
),
);
}
}

View File

@ -3,7 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View File

@ -19,6 +19,7 @@ export 'image/image_menu.dart';
export 'image/image_selection_menu.dart'; export 'image/image_selection_menu.dart';
export 'inline_math_equation/inline_math_equation.dart'; export 'inline_math_equation/inline_math_equation.dart';
export 'inline_math_equation/inline_math_equation_toolbar_item.dart'; export 'inline_math_equation/inline_math_equation_toolbar_item.dart';
export 'align_toolbar_item/align_toolbar_item.dart';
export 'math_equation/math_equation_block_component.dart'; export 'math_equation/math_equation_block_component.dart';
export 'openai/widgets/auto_completion_node_widget.dart'; export 'openai/widgets/auto_completion_node_widget.dart';
export 'openai/widgets/smart_edit_node_widget.dart'; export 'openai/widgets/smart_edit_node_widget.dart';

View File

@ -132,7 +132,7 @@ class _ToggleListBlockComponentWidgetState
EdgeInsets get indentPadding => configuration.indentPadding( EdgeInsets get indentPadding => configuration.indentPadding(
node, node,
calculateTextDirection( calculateTextDirection(
defaultTextDirection: Directionality.maybeOf(context), layoutDirection: Directionality.maybeOf(context),
), ),
); );
@ -148,7 +148,7 @@ class _ToggleListBlockComponentWidgetState
@override @override
Widget buildComponent(BuildContext context) { Widget buildComponent(BuildContext context) {
final textDirection = calculateTextDirection( final textDirection = calculateTextDirection(
defaultTextDirection: Directionality.maybeOf(context), layoutDirection: Directionality.maybeOf(context),
); );
Widget child = Container( Widget child = Container(

View File

@ -54,8 +54,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: a9af2bb ref: a912c1c
resolved-ref: a9af2bbd373a6a478f1bd63d6037817e81d23de2 resolved-ref: a912c1c96532ec561ea68d5138aee415fdecede2
url: "https://github.com/AppFlowy-IO/appflowy-editor.git" url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git source: git
version: "1.2.4" version: "1.2.4"

View File

@ -48,7 +48,7 @@ dependencies:
appflowy_editor: appflowy_editor:
git: git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: a9af2bb ref: a912c1c
appflowy_popover: appflowy_popover:
path: packages/appflowy_popover path: packages/appflowy_popover

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4L12 4" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 8H11" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 12L12 12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4L12 4" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 8H10" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 12L12 12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4L12 4" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8H12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 12L12 12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B