mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support toggle list (#3016)
This commit is contained in:
@ -37,6 +37,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
final inlinePageReferenceService = InlinePageReferenceService();
|
||||
|
||||
final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||
toggleToggleListCommand,
|
||||
...codeBlockCommands,
|
||||
...standardCommandShortcutEvents,
|
||||
];
|
||||
@ -68,7 +69,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
...codeBlockCharacterEvents,
|
||||
|
||||
// toggle list
|
||||
// formatGreaterToToggleList,
|
||||
formatGreaterToToggleList,
|
||||
insertChildNodeInsideToggleList,
|
||||
|
||||
// customize the slash menu command
|
||||
customSlashCommand(
|
||||
@ -107,6 +109,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
indentableBlockTypes.add(ToggleListBlockKeys.type);
|
||||
slashMenuItems = _customSlashMenuItems();
|
||||
|
||||
effectiveScrollController = widget.scrollController ?? ScrollController();
|
||||
@ -286,6 +289,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
TodoListBlockKeys.type,
|
||||
CalloutBlockKeys.type,
|
||||
OutlineBlockKeys.type,
|
||||
ToggleListBlockKeys.type,
|
||||
];
|
||||
|
||||
final supportAlignBuilderType = [
|
||||
@ -313,7 +317,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
final top = builder.configuration.padding(context.node).top;
|
||||
final padding = context.node.type == HeadingBlockKeys.type
|
||||
? EdgeInsets.only(top: top + 8.0)
|
||||
: EdgeInsets.only(top: top);
|
||||
: EdgeInsets.only(top: top + 2.0);
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: BlockActionList(
|
||||
|
@ -1,7 +1,6 @@
|
||||
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._();
|
||||
@ -11,24 +10,35 @@ class ToggleListBlockKeys {
|
||||
/// The content of a code block.
|
||||
///
|
||||
/// The value is a String.
|
||||
static const String delta = 'delta';
|
||||
static const String delta = blockComponentDelta;
|
||||
|
||||
static const String backgroundColor = blockComponentBackgroundColor;
|
||||
|
||||
static const String textDirection = blockComponentTextDirection;
|
||||
|
||||
/// The value is a bool.
|
||||
static const String collapsed = 'collapsed';
|
||||
}
|
||||
|
||||
Node toggleListBlockNode({
|
||||
String? text,
|
||||
Delta? delta,
|
||||
bool collapsed = false,
|
||||
String? textDirection,
|
||||
Attributes? attributes,
|
||||
Iterable<Node>? children,
|
||||
}) {
|
||||
final attributes = {
|
||||
ToggleListBlockKeys.delta: (delta ?? Delta()).toJson(),
|
||||
ToggleListBlockKeys.collapsed: collapsed,
|
||||
};
|
||||
return Node(
|
||||
type: ToggleListBlockKeys.type,
|
||||
attributes: attributes,
|
||||
children: [paragraphNode()],
|
||||
attributes: {
|
||||
ToggleListBlockKeys.collapsed: collapsed,
|
||||
ToggleListBlockKeys.delta:
|
||||
(delta ?? (Delta()..insert(text ?? ''))).toJson(),
|
||||
if (attributes != null) ...attributes,
|
||||
if (textDirection != null)
|
||||
ToggleListBlockKeys.textDirection: textDirection,
|
||||
},
|
||||
children: children ?? [paragraphNode()],
|
||||
);
|
||||
}
|
||||
|
||||
@ -86,7 +96,9 @@ class _ToggleListBlockComponentWidgetState
|
||||
SelectableMixin,
|
||||
DefaultSelectableMixin,
|
||||
BlockComponentConfigurable,
|
||||
BlockComponentBackgroundColorMixin {
|
||||
BlockComponentBackgroundColorMixin,
|
||||
NestedBlockComponentStatefulWidgetMixin,
|
||||
BlockComponentTextDirectionMixin {
|
||||
// the key used to forward focus to the richtext child
|
||||
@override
|
||||
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
|
||||
@ -105,63 +117,65 @@ class _ToggleListBlockComponentWidgetState
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
bool get collapsed => node.attributes[ToggleListBlockKeys.collapsed] ?? false;
|
||||
@override
|
||||
EdgeInsets get indentPadding => configuration.indentPadding(
|
||||
node,
|
||||
calculateTextDirection(
|
||||
defaultTextDirection: Directionality.maybeOf(context),
|
||||
),
|
||||
);
|
||||
|
||||
late final editorState = context.read<EditorState>();
|
||||
bool get collapsed => node.attributes[ToggleListBlockKeys.collapsed] ?? false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return collapsed
|
||||
? buildToggleListBlockComponent(context)
|
||||
: buildToggleListBlockComponentWithChildren(context);
|
||||
? buildComponent(context)
|
||||
: buildComponentWithChildren(context);
|
||||
}
|
||||
|
||||
Widget buildToggleListBlockComponentWithChildren(BuildContext context) {
|
||||
return Container(
|
||||
color: backgroundColor,
|
||||
child: NestedListWidget(
|
||||
children: editorState.renderer.buildList(
|
||||
context,
|
||||
widget.node.children,
|
||||
),
|
||||
child: buildToggleListBlockComponent(context),
|
||||
),
|
||||
@override
|
||||
Widget buildComponent(BuildContext context) {
|
||||
final textDirection = calculateTextDirection(
|
||||
defaultTextDirection: Directionality.maybeOf(context),
|
||||
);
|
||||
}
|
||||
|
||||
// build the richtext child
|
||||
Widget buildToggleListBlockComponent(BuildContext context) {
|
||||
Widget child = 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: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
placeholderText: placeholderText,
|
||||
lineHeight: 1.5,
|
||||
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
textStyle,
|
||||
Widget child = Container(
|
||||
color: backgroundColor,
|
||||
width: double.infinity,
|
||||
child: 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,
|
||||
),
|
||||
placeholderTextSpanDecorator: (textSpan) =>
|
||||
textSpan.updateTextStyle(
|
||||
placeholderTextStyle,
|
||||
onPressed: onCollapsed,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4.0,
|
||||
),
|
||||
Expanded(
|
||||
child: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
placeholderText: placeholderText,
|
||||
lineHeight: 1.5,
|
||||
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
textStyle,
|
||||
),
|
||||
placeholderTextSpanDecorator: (textSpan) =>
|
||||
textSpan.updateTextStyle(
|
||||
placeholderTextStyle,
|
||||
),
|
||||
textDirection: textDirection,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
child = Padding(
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const _greater = '>';
|
||||
|
||||
@ -22,3 +23,107 @@ CharacterShortcutEvent formatGreaterToToggleList = CharacterShortcutEvent(
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/// Press enter key to insert child node inside the toggle list
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - mobile
|
||||
/// - web
|
||||
CharacterShortcutEvent insertChildNodeInsideToggleList = CharacterShortcutEvent(
|
||||
key: 'insert child node inside toggle list',
|
||||
character: '\n',
|
||||
handler: (editorState) async {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null ||
|
||||
node.type != ToggleListBlockKeys.type ||
|
||||
delta == null) {
|
||||
return false;
|
||||
}
|
||||
final slicedDelta = delta.slice(selection.start.offset);
|
||||
final transaction = editorState.transaction;
|
||||
final collapsed = node.attributes[ToggleListBlockKeys.collapsed] as bool;
|
||||
if (collapsed) {
|
||||
// if the delta is empty, clear the format
|
||||
if (delta.isEmpty) {
|
||||
transaction
|
||||
..insertNode(
|
||||
selection.start.path.next,
|
||||
paragraphNode(),
|
||||
)
|
||||
..deleteNode(node)
|
||||
..afterSelection = Selection.collapse(selection.start.path, 0);
|
||||
} else {
|
||||
// insert a toggle list block below the current toggle list block
|
||||
transaction
|
||||
..deleteText(node, selection.startIndex, slicedDelta.length)
|
||||
..insertNode(
|
||||
selection.start.path.next,
|
||||
toggleListBlockNode(collapsed: true, delta: slicedDelta),
|
||||
)
|
||||
..afterSelection = Selection.collapse(selection.start.path.next, 0);
|
||||
}
|
||||
} else {
|
||||
// insert a paragraph block inside the current toggle list block
|
||||
transaction
|
||||
..deleteText(node, selection.startIndex, slicedDelta.length)
|
||||
..insertNode(
|
||||
selection.start.path + [0],
|
||||
paragraphNode(delta: slicedDelta),
|
||||
)
|
||||
..afterSelection = Selection.collapse(selection.start.path + [0], 0);
|
||||
}
|
||||
await editorState.apply(transaction);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
/// cmd/ctrl + enter to close or open the toggle list
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
///
|
||||
|
||||
// toggle the todo list
|
||||
final CommandShortcutEvent toggleToggleListCommand = CommandShortcutEvent(
|
||||
key: 'toggle the toggle list',
|
||||
command: 'ctrl+enter',
|
||||
macOSCommand: 'cmd+enter',
|
||||
handler: _toggleToggleListCommandHandler,
|
||||
);
|
||||
|
||||
CommandShortcutEventHandler _toggleToggleListCommandHandler = (editorState) {
|
||||
if (PlatformExtension.isMobile) {
|
||||
assert(false, 'enter key is not supported on mobile platform.');
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
if (nodes.isEmpty || nodes.length > 1) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final node = nodes.first;
|
||||
if (node.type != ToggleListBlockKeys.type) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final collapsed = node.attributes[ToggleListBlockKeys.collapsed] as bool;
|
||||
final transaction = editorState.transaction;
|
||||
transaction.updateNode(node, {
|
||||
ToggleListBlockKeys.collapsed: !collapsed,
|
||||
});
|
||||
transaction.afterSelection = selection;
|
||||
editorState.apply(transaction);
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
Reference in New Issue
Block a user