Skip to content

Commit

Permalink
feat: support dragging the block to reorder (AppFlowy-IO#6285)
Browse files Browse the repository at this point in the history
* feat: support dragging the block to reorder

* feat: render feedback widget

* feat: add drag to move translation

* fix: the feedback widget doesn't update after node changed

* feat: render table placeholder

* feat: implement auto scroll when dragging to edge

* chore: add back the drop images/files feature

* chore: code refactor

* feat: exclude image and file block

* feat: exclude image gallery and database block
  • Loading branch information
LucasXu0 authored Sep 16, 2024
1 parent 129db69 commit d67d77f
Show file tree
Hide file tree
Showing 11 changed files with 383 additions and 95 deletions.
33 changes: 24 additions & 9 deletions frontend/appflowy_flutter/lib/plugins/document/document_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ class _DocumentPageState extends State<DocumentPage>
.getDropTargetRenderData(details.globalPosition);

if (data != null &&
data.dropTarget != null &&
data.dropPath != null &&

// We implement custom Drop logic for image blocks, this is
// how we can exclude them from the Drop Target
Expand All @@ -184,14 +184,28 @@ class _DocumentPageState extends State<DocumentPage>
}
},
onDragDone: (details) async {
state.editorState!.selectionService.removeDropTarget();
final editorState = state.editorState;
if (editorState == null) {
return;
}

final data = state.editorState!.selectionService
editorState.selectionService.removeDropTarget();

final data = editorState.selectionService
.getDropTargetRenderData(details.globalPosition);

if (data != null) {
if (data.cursorNode != null) {
if (_excludeFromDropTarget.contains(data.cursorNode?.type)) {
final cursorNode = data.cursorNode;
final dropPath = data.dropPath;

if (cursorNode != null && dropPath != null) {
if (_excludeFromDropTarget.contains(cursorNode.type)) {
return;
}

final node = editorState.getNodeAtPath(dropPath);

if (node == null) {
return;
}

Expand All @@ -209,14 +223,15 @@ class _DocumentPageState extends State<DocumentPage>
}
}

await editorState!.dropImages(
data.dropTarget!,
await editorState.dropImages(
node,
imageFiles,
widget.view.id,
isLocalMode,
);
await editorState!.dropFiles(
data.dropTarget!,

await editorState.dropFiles(
node,
otherFiles,
widget.view.id,
isLocalMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
blockComponentContext: context,
blockComponentState: state,
editorState: editorState,
blockComponentBuilder: builders,
actions: actions,
showSlashMenu: slashMenuItems != null
? () => customSlashCommand(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';

class BlockActionList extends StatelessWidget {
const BlockActionList({
Expand All @@ -12,13 +13,15 @@ class BlockActionList extends StatelessWidget {
required this.editorState,
required this.actions,
required this.showSlashMenu,
required this.blockComponentBuilder,
});

final BlockComponentContext blockComponentContext;
final BlockComponentActionState blockComponentState;
final List<OptionAction> actions;
final VoidCallback showSlashMenu;
final EditorState editorState;
final Map<String, BlockComponentBuilder> blockComponentBuilder;

@override
Widget build(BuildContext context) {
Expand All @@ -31,14 +34,15 @@ class BlockActionList extends StatelessWidget {
editorState: editorState,
showSlashMenu: showSlashMenu,
),
const SizedBox(width: 4.0),
const HSpace(4.0),
BlockOptionButton(
blockComponentContext: blockComponentContext,
blockComponentState: blockComponentState,
actions: actions,
editorState: editorState,
blockComponentBuilder: blockComponentBuilder,
),
const SizedBox(width: 4.0),
const HSpace(4.0),
],
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
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/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';

class BlockOptionButton extends StatelessWidget {
import 'drag_to_reorder/draggable_option_button.dart';

class BlockOptionButton extends StatefulWidget {
const BlockOptionButton({
super.key,
required this.blockComponentContext,
required this.blockComponentState,
required this.actions,
required this.editorState,
required this.blockComponentBuilder,
});

final BlockComponentContext blockComponentContext;
final BlockComponentActionState blockComponentState;
final List<OptionAction> actions;
final EditorState editorState;
final Map<String, BlockComponentBuilder> blockComponentBuilder;

@override
Widget build(BuildContext context) {
final popoverActions = actions.map((e) {
State<BlockOptionButton> createState() => _BlockOptionButtonState();
}

class _BlockOptionButtonState extends State<BlockOptionButton> {
late final List<PopoverAction> popoverActions;

@override
void initState() {
super.initState();

popoverActions = widget.actions.map((e) {
switch (e) {
case OptionAction.divider:
return DividerOptionAction();
case OptionAction.color:
return ColorOptionAction(editorState: editorState);
return ColorOptionAction(editorState: widget.editorState);
case OptionAction.align:
return AlignOptionAction(editorState: editorState);
return AlignOptionAction(editorState: widget.editorState);
case OptionAction.depth:
return DepthOptionAction(editorState: editorState);
return DepthOptionAction(editorState: widget.editorState);
default:
return OptionActionWrapper(e);
}
}).toList();
}

@override
Widget build(BuildContext context) {
return PopoverActionList<PopoverAction>(
popoverMutex: PopoverMutex(),
direction:
Expand All @@ -53,13 +64,13 @@ class BlockOptionButton extends StatelessWidget {
actions: popoverActions,
onPopupBuilder: () {
keepEditorFocusNotifier.increase();
blockComponentState.alwaysShowActions = true;
widget.blockComponentState.alwaysShowActions = true;
},
onClosed: () {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
editorState.selectionType = null;
editorState.selection = null;
blockComponentState.alwaysShowActions = false;
widget.editorState.selectionType = null;
widget.editorState.selection = null;
widget.blockComponentState.alwaysShowActions = false;
keepEditorFocusNotifier.decrease();
});
},
Expand All @@ -69,62 +80,18 @@ class BlockOptionButton extends StatelessWidget {
controller.close();
}
},
buildChild: (controller) => _buildOptionButton(context, controller),
);
}

Widget _buildOptionButton(
BuildContext context,
PopoverController controller,
) {
return BlockActionButton(
svg: FlowySvgs.drag_element_s,
richMessage: TextSpan(
children: [
TextSpan(
// todo: customize the color to highlight the text.
text: LocaleKeys.document_plugins_optionAction_click.tr(),
style: context.tooltipTextStyle(),
),
TextSpan(
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
style: context.tooltipTextStyle(),
),
],
buildChild: (controller) => DraggableOptionButton(
controller: controller,
editorState: widget.editorState,
blockComponentContext: widget.blockComponentContext,
blockComponentBuilder: widget.blockComponentBuilder,
),
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);
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(BuildContext context, OptionAction action) {
final node = blockComponentContext.node;
final transaction = editorState.transaction;
final node = widget.blockComponentContext.node;
final transaction = widget.editorState.transaction;
switch (action) {
case OptionAction.delete:
transaction.deleteNode(node);
Expand All @@ -146,7 +113,7 @@ class BlockOptionButton extends StatelessWidget {
case OptionAction.depth:
throw UnimplementedError();
}
editorState.apply(transaction);
widget.editorState.apply(transaction);
}

void _duplicateBlock(
Expand All @@ -156,8 +123,7 @@ class BlockOptionButton extends StatelessWidget {
) {
// 1. verify the node integrity
final type = node.type;
final builder =
context.read<EditorState>().renderer.blockComponentBuilder(type);
final builder = widget.editorState.renderer.blockComponentBuilder(type);

if (builder == null) {
Log.error('Block type $type is not supported');
Expand All @@ -184,8 +150,7 @@ class BlockOptionButton extends StatelessWidget {
Node copiedNode = node.copyWith();

final type = node.type;
final builder =
context.read<EditorState>().renderer.blockComponentBuilder(type);
final builder = widget.editorState.renderer.blockComponentBuilder(type);

if (builder == null) {
Log.error('Block type $type is not supported');
Expand Down
Loading

0 comments on commit d67d77f

Please sign in to comment.