feat: support click to create content inside empty toggle list (#6854)

* feat: support click to create content inside empty toggle list

* test: support click to create content inside empty toggle list

* fix: toggle list rtl issue

* chore: optimize cover title request node logic
This commit is contained in:
Lucas 2024-11-25 10:39:23 +08:00 committed by GitHub
parent 2ad2a79bd0
commit bde1457524
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 135 additions and 34 deletions

View File

@ -1,7 +1,9 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@ -263,5 +265,24 @@ void main() {
expect(node.attributes[ToggleListBlockKeys.level], 3);
expect(node.delta!.toPlainText(), 'Hello');
});
testWidgets('click the toggle list to create a new paragraph',
(tester) async {
await prepareToggleHeadingBlock(tester, '> # Hello');
final emptyHintText = find.text(
LocaleKeys.document_plugins_emptyToggleHeading.tr(
args: ['1'],
),
);
expect(emptyHintText, findsOneWidget);
await tester.tapButton(emptyHintText);
await tester.pumpAndSettle();
// check the new paragraph is created
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0, 0])!;
expect(node.type, ParagraphBlockKeys.type);
});
});
}

View File

@ -163,7 +163,11 @@ class _DocumentPageState extends State<DocumentPage>
return Provider(
create: (_) {
final context = SharedEditorContext();
if (widget.view.name.isEmpty) {
final children = editorState.document.root.children;
final firstDelta = children.firstOrNull?.delta;
final isEmptyDocument =
children.length == 1 && (firstDelta == null || firstDelta.isEmpty);
if (widget.view.name.isEmpty && isEmptyDocument) {
context.requestCoverTitleFocus = true;
}
return context;

View File

@ -163,6 +163,10 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
bool _shouldFocus(ViewPB view, ViewState? state) {
final name = state?.view.name ?? view.name;
if (editorState.document.root.children.isNotEmpty) {
return false;
}
// if the view's name is empty, focus on the title
if (name.isEmpty) {
return true;
@ -180,6 +184,15 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
void _onFocusChanged() {
if (titleFocusNode.hasFocus) {
// if the document is empty, disable the keyboard service
final children = editorState.document.root.children;
final firstDelta = children.firstOrNull?.delta;
final isEmptyDocument =
children.length == 1 && (firstDelta == null || firstDelta.isEmpty);
if (!isEmptyDocument) {
return;
}
if (editorState.selection != null) {
Log.info('cover title got focus, clear the editor selection');
editorState.selection = null;

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:universal_platform/universal_platform.dart';
@ -211,25 +211,7 @@ class _ToggleListBlockComponentWidgetState
BuildContext context, {
bool withBackgroundColor = false,
}) {
final textDirection = calculateTextDirection(
layoutDirection: Directionality.maybeOf(context),
);
Widget child = Container(
width: double.infinity,
alignment: alignment,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
textDirection: textDirection,
children: [
_buildExpandIcon(),
Flexible(
child: _buildRichText(),
),
],
),
);
Widget child = _buildToggleBlock();
child = BlockSelectionContainer(
node: node,
@ -265,6 +247,58 @@ class _ToggleListBlockComponentWidgetState
return child;
}
Widget _buildToggleBlock() {
final textDirection = calculateTextDirection(
layoutDirection: Directionality.maybeOf(context),
);
final crossAxisAlignment = textDirection == TextDirection.ltr
? CrossAxisAlignment.start
: CrossAxisAlignment.end;
return Container(
width: double.infinity,
alignment: alignment,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: crossAxisAlignment,
children: [
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
textDirection: textDirection,
children: [
_buildExpandIcon(),
Flexible(
child: _buildRichText(),
),
],
),
_buildPlaceholder(),
],
),
);
}
Widget _buildPlaceholder() {
// if the toggle block is collapsed or it contains children, don't show the
// placeholder.
if (collapsed || node.children.isNotEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: indentPadding,
child: FlowyButton(
text: FlowyText(
buildPlaceholderText(),
color: Theme.of(context).hintColor,
),
margin: const EdgeInsets.symmetric(horizontal: 3.0, vertical: 8),
onTap: onAddContent,
),
);
}
Widget _buildRichText() {
final textDirection = calculateTextDirection(
layoutDirection: Directionality.maybeOf(context),
@ -304,6 +338,9 @@ class _ToggleListBlockComponentWidgetState
Widget _buildExpandIcon() {
double buttonHeight = UniversalPlatform.isDesktop ? 22.0 : 26.0;
final textDirection = calculateTextDirection(
layoutDirection: Directionality.maybeOf(context),
);
if (level != null) {
// top padding * 2 + button height = height of the heading text
@ -316,23 +353,27 @@ class _ToggleListBlockComponentWidgetState
}
}
final turns = switch (textDirection) {
TextDirection.ltr => collapsed ? 0.0 : 0.25,
TextDirection.rtl => collapsed ? -0.5 : -0.75,
};
return Container(
constraints: BoxConstraints(
minWidth: 26,
minHeight: buttonHeight,
),
child: FlowyIconButton(
width: 20.0,
onPressed: onCollapsed,
icon: Container(
padding: const EdgeInsets.only(right: 4.0),
child: AnimatedRotation(
turns: collapsed ? 0.0 : 0.25,
duration: const Duration(milliseconds: 200),
child: const Icon(
Icons.arrow_right,
size: 18.0,
),
alignment: Alignment.center,
child: FlowyButton(
margin: const EdgeInsets.all(2.0),
useIntrinsicWidth: true,
onTap: onCollapsed,
text: AnimatedRotation(
turns: turns,
duration: const Duration(milliseconds: 200),
child: const Icon(
Icons.arrow_right,
size: 18.0,
),
),
),
@ -347,4 +388,24 @@ class _ToggleListBlockComponentWidgetState
transaction.afterSelection = editorState.selection;
await editorState.apply(transaction);
}
Future<void> onAddContent() async {
final transaction = editorState.transaction;
final path = node.path.child(0);
transaction.insertNode(
path,
paragraphNode(),
);
transaction.afterSelection = Selection.collapsed(Position(path: path));
await editorState.apply(transaction);
}
String buildPlaceholderText() {
if (level != null) {
return LocaleKeys.document_plugins_emptyToggleHeading.tr(
args: [level.toString()],
);
}
return LocaleKeys.document_plugins_emptyToggleList.tr();
}
}

View File

@ -1704,6 +1704,8 @@
"insertDate": "Insert date",
"emoji": "Emoji",
"toggleList": "Toggle list",
"emptyToggleHeading": "Empty toggle heading {}. Click to add content",
"emptyToggleList": "Empty toggle list. Click to add content",
"quoteList": "Quote list",
"numberedList": "Numbered list",
"bulletedList": "Bulleted list",