mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-11-30 10:12:50 +03:00
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:
parent
2ad2a79bd0
commit
bde1457524
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
@ -2865,4 +2867,4 @@
|
||||
"unfavorite": "Unfavorite",
|
||||
"favoriteDisabledHint": "Cannot favorite this view"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user