feat: add toggle list and integrate add block button (#2547)

This commit is contained in:
Lucas.Xu 2023-05-17 11:02:55 +08:00 committed by GitHub
parent bc66f43f47
commit 623f182bba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 507 additions and 27 deletions

View File

@ -1,4 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="7.5" y="4" width="1" height="8" rx="0.5" fill="#333333"/>
<rect x="12" y="7.5" width="1" height="8" rx="0.5" transform="rotate(90 12 7.5)" fill="#333333"/>
</svg>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="plus" style="width: 16px; height: 100%; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="16" height="16" ><path d="M7.977 14.963c.407 0 .747-.324.747-.723V8.72h5.362c.399 0 .74-.34.74-.747a.746.746 0 00-.74-.738H8.724V1.706c0-.398-.34-.722-.747-.722a.732.732 0 00-.739.722v5.529h-5.37a.746.746 0 00-.74.738c0 .407.341.747.74.747h5.37v5.52c0 .399.332.723.739.723z" fill-opacity="0.35" fill="#37352F"></path></svg>

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 558 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-width="2.6" d="M9.41 7.3H9.4M14.6 7.3h-.01M9.31 12H9.3M14.6 12h-.01M9.41 16.7H9.4M14.6 16.7h-.01"/></svg>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" class="dragHandle" style="width: 14px; height: 14px; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="14" height="14" ><path d="M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z" fill-opacity="0.35" fill="#37352F"></path></svg>

Before

Width:  |  Height:  |  Size: 238 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -395,6 +395,10 @@
"mathEquation": {
"addMathEquation": "Add Math Equation",
"editMathEquation": "Edit Math Equation"
},
"optionAction": {
"click": "Click",
"toOpenMenu": " to open menu"
}
}
},

View File

@ -89,11 +89,11 @@ class _DocumentPageState extends State<DocumentPage> {
Widget _buildEditorPage(BuildContext context, DocumentState state) {
final appflowyEditorPage = AppFlowyEditorPage(
editorState: editorState!,
header: _buildCoverAndIcon(context),
);
return Column(
children: [
if (state.isDeleted) _buildBanner(context),
_buildCoverAndIcon(context),
Expanded(
child: appflowyEditorPage,
),

View File

@ -1,6 +1,6 @@
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_list.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@ -13,9 +13,11 @@ class AppFlowyEditorPage extends StatefulWidget {
const AppFlowyEditorPage({
super.key,
required this.editorState,
this.header,
});
final EditorState editorState;
final Widget? header;
@override
State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
@ -41,16 +43,12 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
final List<ToolbarItem> toolbarItems = [
smartEditItem,
placeholderItem,
paragraphItem,
...headingItems,
placeholderItem,
...markdownFormatItems,
placeholderItem,
quoteItem,
bulletedListItem,
numberedListItem,
placeholderItem,
linkItem,
textColorItem,
highlightColorItem,
@ -70,8 +68,15 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
(element) => element == slashCommand,
), // remove the default slash command.
customSlashCommand(slashMenuItems),
// formatGreaterToToggleList,
];
late final showSlashMenu = customSlashCommand(
slashMenuItems,
shouldInsertSlash: false,
).handler;
late final styleCustomizer = EditorStyleCustomizer(context: context);
DocumentBloc get documentBloc => context.read<DocumentBloc>();
@ -92,6 +97,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
// customize the shortcuts
characterShortcutEvents: characterShortcutEvents,
commandShortcutEvents: commandShortcutEvents,
header: widget.header,
);
return Center(
@ -183,6 +189,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
),
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
// ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(),
};
final builders = {
@ -207,24 +214,25 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
TodoListBlockKeys.type,
CalloutBlockKeys.type
];
if (!supportColorBuilderTypes.contains(entry.key)) {
builder.actionBuilder = (context, state) => OptionActionList(
blockComponentContext: context,
blockComponentState: state,
editorState: widget.editorState,
actions: standardActions,
);
continue;
}
final colorAction = [
OptionAction.divider,
OptionAction.color,
];
builder.actionBuilder = (context, state) => OptionActionList(
final List<OptionAction> actions = [
...standardActions,
if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
];
builder.actionBuilder = (context, state) => BlockActionList(
blockComponentContext: context,
blockComponentState: state,
editorState: widget.editorState,
actions: standardActions + colorAction,
actions: actions,
showSlashMenu: () => showSlashMenu(
widget.editorState,
),
);
}

View File

@ -0,0 +1,52 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
class BlockAddButton extends StatelessWidget {
const BlockAddButton({
Key? key,
required this.blockComponentContext,
required this.blockComponentState,
required this.editorState,
required this.showSlashMenu,
}) : super(key: key);
final BlockComponentContext blockComponentContext;
final BlockComponentState blockComponentState;
final EditorState editorState;
final VoidCallback showSlashMenu;
@override
Widget build(BuildContext context) {
return BlockActionButton(
svgName: 'editor/add',
richMessage: const TextSpan(
children: [
TextSpan(
// todo: l10n.
text: 'Click to add below',
),
],
),
onTap: () {
final transaction = editorState.transaction;
// if the current block is not a empty paragraph block, then insert a new block below the current block.
final node = blockComponentContext.node;
if (node.type != ParagraphBlockKeys.type ||
(node.delta?.isNotEmpty ?? true)) {
transaction.insertNode(node.path.next, paragraphNode());
transaction.afterSelection = Selection.collapse(node.path.next, 0);
} else {
transaction.afterSelection = Selection.collapse(node.path, 0);
}
// show the slash menu.
editorState.apply(transaction).then(
(_) => WidgetsBinding.instance.addPostFrameCallback(
(_) => showSlashMenu(),
),
);
},
);
}
}

View File

@ -0,0 +1,41 @@
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
import 'package:flutter/material.dart';
class BlockActionButton extends StatelessWidget {
const BlockActionButton({
super.key,
required this.svgName,
required this.richMessage,
required this.onTap,
});
final String svgName;
final InlineSpan richMessage;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.center,
child: Tooltip(
preferBelow: false,
richMessage: richMessage,
child: MouseRegion(
cursor: SystemMouseCursors.grab,
child: IgnoreParentGestureWidget(
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.deferToChild,
child: svgWidget(
svgName,
size: const Size.square(14.0),
color: Theme.of(context).iconTheme.color,
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,45 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
class BlockActionList extends StatelessWidget {
const BlockActionList({
super.key,
required this.blockComponentContext,
required this.blockComponentState,
required this.editorState,
required this.actions,
required this.showSlashMenu,
});
final BlockComponentContext blockComponentContext;
final BlockComponentState blockComponentState;
final List<OptionAction> actions;
final VoidCallback showSlashMenu;
final EditorState editorState;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
BlockAddButton(
blockComponentContext: blockComponentContext,
blockComponentState: blockComponentState,
editorState: editorState,
showSlashMenu: showSlashMenu,
),
const SizedBox(width: 8.0),
BlockOptionButton(
blockComponentContext: blockComponentContext,
blockComponentState: blockComponentState,
actions: actions,
editorState: editorState,
),
const SizedBox(width: 6.0),
],
);
}
}

View File

@ -0,0 +1,131 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
class BlockOptionButton extends StatelessWidget {
const BlockOptionButton({
Key? key,
required this.blockComponentContext,
required this.blockComponentState,
required this.actions,
required this.editorState,
}) : super(key: key);
final BlockComponentContext blockComponentContext;
final BlockComponentState blockComponentState;
final List<OptionAction> actions;
final EditorState editorState;
@override
Widget build(BuildContext context) {
final popoverActions = actions.map((e) {
if (e == OptionAction.divider) {
return DividerOptionAction();
} else if (e == OptionAction.color) {
return ColorOptionAction(
editorState: editorState,
);
} else {
return OptionActionWrapper(e);
}
}).toList();
return PopoverActionList<PopoverAction>(
direction: PopoverDirection.leftWithCenterAligned,
actions: popoverActions,
onPopupBuilder: () => blockComponentState.alwaysShowActions = true,
onClosed: () {
editorState.selectionType = null;
editorState.selection = null;
blockComponentState.alwaysShowActions = false;
},
onSelected: (action, controller) {
if (action is OptionActionWrapper) {
_onSelectAction(action.inner);
controller.close();
}
},
buildChild: (controller) => _buildOptionButton(controller),
);
}
Widget _buildOptionButton(PopoverController controller) {
return BlockActionButton(
svgName: 'editor/option',
richMessage: TextSpan(
children: [
TextSpan(
// todo: customize the color to highlight the text.
text: LocaleKeys.document_plugins_optionAction_click.tr(),
),
TextSpan(
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
)
],
),
onTap: () {
controller.show();
// update selection
_updateBlockSelection();
},
);
}
void _updateBlockSelection() {
final startNode = blockComponentContext.node;
var endNode = startNode;
while (endNode.children.isNotEmpty) {
endNode = endNode.children.last;
}
final start = Position(path: startNode.path, offset: 0);
final end = endNode.selectable?.end() ??
Position(
path: endNode.path,
offset: endNode.delta?.length ?? 0,
);
editorState.selectionType = SelectionType.block;
editorState.selection = Selection(
start: start,
end: end,
);
}
void _onSelectAction(OptionAction action) {
final node = blockComponentContext.node;
final transaction = editorState.transaction;
switch (action) {
case OptionAction.delete:
transaction.deleteNode(node);
break;
case OptionAction.duplicate:
transaction.insertNode(
node.path.next,
node.copyWith(),
);
break;
case OptionAction.turnInto:
break;
case OptionAction.moveUp:
transaction.moveNode(node.path.previous, node);
break;
case OptionAction.moveDown:
transaction.moveNode(node.path.next.next, node);
break;
case OptionAction.color:
// show the color picker
break;
case OptionAction.divider:
throw UnimplementedError();
}
editorState.apply(transaction);
}
}

View File

@ -206,6 +206,7 @@ class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
node: widget.node,
editorState: editorState,
placeholderText: placeholderText,
lineHeight: 1.5,
textSpanDecorator: (textSpan) => TextSpan(
style: textStyle,
children: codeTextSpans,
@ -218,10 +219,12 @@ class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
}
Widget _buildSwitchLanguageButton(BuildContext context) {
const maxWidth = 100.0;
return AppFlowyPopover(
controller: popoverController,
child: Container(
width: 100,
width: maxWidth,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 4),
child: FlowyTextButton(
'${language?.capitalize() ?? 'auto'} ',
@ -229,8 +232,10 @@ class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
horizontal: 12.0,
vertical: 4.0,
),
constraints: const BoxConstraints(maxWidth: maxWidth),
fontColor: Theme.of(context).colorScheme.onBackground,
fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.start,
onPressed: () {},
),
),

View File

@ -12,6 +12,7 @@ import 'package:easy_localization/easy_localization.dart';
final ToolbarItem smartEditItem = ToolbarItem(
id: 'appflowy.editor.smart_edit',
group: 0,
isActive: (editorState) {
final selection = editorState.selection;
if (selection == null) {

View File

@ -18,3 +18,5 @@ export 'math_equation/math_equation_block_component.dart';
export 'openai/widgets/auto_completion_node_widget.dart';
export 'openai/widgets/smart_edit_node_widget.dart';
export 'openai/widgets/smart_edit_toolbar_item.dart';
export 'toggle/toggle_block_component.dart';
export 'toggle/toggle_block_shortcut_event.dart';

View File

@ -0,0 +1,165 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ToggleListBlockKeys {
const ToggleListBlockKeys._();
static const String type = 'toggle_list';
/// The content of a code block.
///
/// The value is a String.
static const String delta = 'delta';
/// The value is a bool.
static const String collapsed = 'collapsed';
}
Node toggleListBlockNode({
Delta? delta,
bool collapsed = false,
}) {
final attributes = {
ToggleListBlockKeys.delta: (delta ?? Delta()).toJson(),
ToggleListBlockKeys.collapsed: collapsed,
};
return Node(
type: ToggleListBlockKeys.type,
attributes: attributes,
children: [paragraphNode()],
);
}
class ToggleListBlockComponentBuilder extends BlockComponentBuilder {
ToggleListBlockComponentBuilder({
this.configuration = const BlockComponentConfiguration(),
this.padding = const EdgeInsets.all(0),
});
@override
final BlockComponentConfiguration configuration;
final EdgeInsets padding;
@override
Widget build(BlockComponentContext blockComponentContext) {
final node = blockComponentContext.node;
return ToggleListBlockComponentWidget(
key: node.key,
node: node,
configuration: configuration,
padding: padding,
);
}
@override
bool validate(Node node) => node.delta != null;
}
class ToggleListBlockComponentWidget extends StatefulWidget {
const ToggleListBlockComponentWidget({
Key? key,
required this.node,
this.configuration = const BlockComponentConfiguration(),
this.padding = const EdgeInsets.all(0),
}) : super(key: key);
final Node node;
final BlockComponentConfiguration configuration;
final EdgeInsets padding;
@override
State<ToggleListBlockComponentWidget> createState() =>
_ToggleListBlockComponentWidgetState();
}
class _ToggleListBlockComponentWidgetState
extends State<ToggleListBlockComponentWidget>
with
SelectableMixin,
DefaultSelectable,
BlockComponentConfigurable,
BackgroundColorMixin {
// the key used to forward focus to the richtext child
@override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
@override
BlockComponentConfiguration get configuration => widget.configuration;
@override
GlobalKey<State<StatefulWidget>> get containerKey => node.key;
@override
Node get node => widget.node;
bool get collapsed => node.attributes[ToggleListBlockKeys.collapsed] ?? false;
late final editorState = context.read<EditorState>();
@override
Widget build(BuildContext context) {
return collapsed
? buildToggleListBlockComponent(context)
: buildToggleListBlockComponentWithChildren(context);
}
Widget buildToggleListBlockComponentWithChildren(BuildContext context) {
return Container(
color: backgroundColor,
child: NestedListWidget(
children: editorState.renderer.buildList(
context,
widget.node.children,
),
child: buildToggleListBlockComponent(context),
),
);
}
// build the richtext child
Widget buildToggleListBlockComponent(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// the emoji picker button for the note
FlowyIconButton(
width: 24.0,
icon: Icon(
collapsed ? Icons.arrow_right : Icons.arrow_drop_down,
),
onPressed: onCollapsed,
),
const SizedBox(
width: 4.0,
),
Expanded(
child: FlowyRichText(
key: forwardKey,
node: widget.node,
editorState: editorState,
placeholderText: placeholderText,
lineHeight: 1.5,
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
textStyle,
),
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(
placeholderTextStyle,
),
),
),
],
);
}
Future<void> onCollapsed() async {
final transaction = editorState.transaction
..updateNode(node, {
ToggleListBlockKeys.collapsed: !collapsed,
});
await editorState.apply(transaction);
}
}

View File

@ -0,0 +1,24 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
const _greater = '>';
/// Convert '> ' to toggle list
///
/// - support
/// - desktop
/// - mobile
/// - web
///
CharacterShortcutEvent formatGreaterToToggleList = CharacterShortcutEvent(
key: 'format greater to quote',
character: ' ',
handler: (editorState) async => await formatMarkdownSymbol(
editorState,
(node) => node.type != ToggleListBlockKeys.type,
(text, _) => text == _greater,
(_, node, delta) => toggleListBlockNode(
delta: delta.compose(Delta()..delete(_greater.length)),
),
),
);

View File

@ -28,7 +28,10 @@ class EditorStyleCustomizer {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
return EditorStyle.desktop(
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
padding: EdgeInsets.only(
left: horizontalPadding / 2.0,
right: horizontalPadding,
),
backgroundColor: theme.colorScheme.surface,
cursorColor: theme.colorScheme.primary,
textStyleConfiguration: TextStyleConfiguration(
@ -115,11 +118,13 @@ class EditorStyleCustomizer {
}
TextStyle codeBlockStyleBuilder() {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
return TextStyle(
fontFamily: 'poppins',
fontSize: fontSize,
height: 1.5,
color: theme.colorScheme.onBackground,
);
}
}

View File

@ -45,8 +45,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "4f66f7"
resolved-ref: "4f66f77debabbc35cf4a56c816f9432a831a40e2"
ref: b1a1b14
resolved-ref: b1a1b14f35114a7becdb3e2de909d546d7328a59
url: "https://github.com/LucasXu0/appflowy-editor.git"
source: git
version: "0.1.12"

View File

@ -47,7 +47,7 @@ dependencies:
# path: /Users/lucas.xu/Desktop/appflowy-editor
git:
url: https://github.com/LucasXu0/appflowy-editor.git
ref: 4f66f7
ref: b1a1b14
appflowy_popover:
path: packages/appflowy_popover