mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-11-13 17:07:40 +00: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 'dart:io';
|
||||||
|
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -263,5 +265,24 @@ void main() {
|
|||||||
expect(node.attributes[ToggleListBlockKeys.level], 3);
|
expect(node.attributes[ToggleListBlockKeys.level], 3);
|
||||||
expect(node.delta!.toPlainText(), 'Hello');
|
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(
|
return Provider(
|
||||||
create: (_) {
|
create: (_) {
|
||||||
final context = SharedEditorContext();
|
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;
|
context.requestCoverTitleFocus = true;
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
|
|||||||
@ -163,6 +163,10 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
|
|||||||
bool _shouldFocus(ViewPB view, ViewState? state) {
|
bool _shouldFocus(ViewPB view, ViewState? state) {
|
||||||
final name = state?.view.name ?? view.name;
|
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 the view's name is empty, focus on the title
|
||||||
if (name.isEmpty) {
|
if (name.isEmpty) {
|
||||||
return true;
|
return true;
|
||||||
@ -180,6 +184,15 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
|
|||||||
|
|
||||||
void _onFocusChanged() {
|
void _onFocusChanged() {
|
||||||
if (titleFocusNode.hasFocus) {
|
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) {
|
if (editorState.selection != null) {
|
||||||
Log.info('cover title got focus, clear the editor selection');
|
Log.info('cover title got focus, clear the editor selection');
|
||||||
editorState.selection = null;
|
editorState.selection = null;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:universal_platform/universal_platform.dart';
|
import 'package:universal_platform/universal_platform.dart';
|
||||||
@ -211,25 +211,7 @@ class _ToggleListBlockComponentWidgetState
|
|||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
bool withBackgroundColor = false,
|
bool withBackgroundColor = false,
|
||||||
}) {
|
}) {
|
||||||
final textDirection = calculateTextDirection(
|
Widget child = _buildToggleBlock();
|
||||||
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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
child = BlockSelectionContainer(
|
child = BlockSelectionContainer(
|
||||||
node: node,
|
node: node,
|
||||||
@ -265,6 +247,58 @@ class _ToggleListBlockComponentWidgetState
|
|||||||
return child;
|
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() {
|
Widget _buildRichText() {
|
||||||
final textDirection = calculateTextDirection(
|
final textDirection = calculateTextDirection(
|
||||||
layoutDirection: Directionality.maybeOf(context),
|
layoutDirection: Directionality.maybeOf(context),
|
||||||
@ -304,6 +338,9 @@ class _ToggleListBlockComponentWidgetState
|
|||||||
|
|
||||||
Widget _buildExpandIcon() {
|
Widget _buildExpandIcon() {
|
||||||
double buttonHeight = UniversalPlatform.isDesktop ? 22.0 : 26.0;
|
double buttonHeight = UniversalPlatform.isDesktop ? 22.0 : 26.0;
|
||||||
|
final textDirection = calculateTextDirection(
|
||||||
|
layoutDirection: Directionality.maybeOf(context),
|
||||||
|
);
|
||||||
|
|
||||||
if (level != null) {
|
if (level != null) {
|
||||||
// top padding * 2 + button height = height of the heading text
|
// top padding * 2 + button height = height of the heading text
|
||||||
@ -316,18 +353,23 @@ class _ToggleListBlockComponentWidgetState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final turns = switch (textDirection) {
|
||||||
|
TextDirection.ltr => collapsed ? 0.0 : 0.25,
|
||||||
|
TextDirection.rtl => collapsed ? -0.5 : -0.75,
|
||||||
|
};
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minWidth: 26,
|
minWidth: 26,
|
||||||
minHeight: buttonHeight,
|
minHeight: buttonHeight,
|
||||||
),
|
),
|
||||||
child: FlowyIconButton(
|
alignment: Alignment.center,
|
||||||
width: 20.0,
|
child: FlowyButton(
|
||||||
onPressed: onCollapsed,
|
margin: const EdgeInsets.all(2.0),
|
||||||
icon: Container(
|
useIntrinsicWidth: true,
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
onTap: onCollapsed,
|
||||||
child: AnimatedRotation(
|
text: AnimatedRotation(
|
||||||
turns: collapsed ? 0.0 : 0.25,
|
turns: turns,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.arrow_right,
|
Icons.arrow_right,
|
||||||
@ -335,7 +377,6 @@ class _ToggleListBlockComponentWidgetState
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,4 +388,24 @@ class _ToggleListBlockComponentWidgetState
|
|||||||
transaction.afterSelection = editorState.selection;
|
transaction.afterSelection = editorState.selection;
|
||||||
await editorState.apply(transaction);
|
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",
|
"insertDate": "Insert date",
|
||||||
"emoji": "Emoji",
|
"emoji": "Emoji",
|
||||||
"toggleList": "Toggle list",
|
"toggleList": "Toggle list",
|
||||||
|
"emptyToggleHeading": "Empty toggle heading {}. Click to add content",
|
||||||
|
"emptyToggleList": "Empty toggle list. Click to add content",
|
||||||
"quoteList": "Quote list",
|
"quoteList": "Quote list",
|
||||||
"numberedList": "Numbered list",
|
"numberedList": "Numbered list",
|
||||||
"bulletedList": "Bulleted list",
|
"bulletedList": "Bulleted list",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user