mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support table plugin (#3280)
This commit is contained in:
committed by
GitHub
parent
abb6eff23d
commit
df8642d446
@ -57,13 +57,18 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
];
|
||||
|
||||
final List<ToolbarItem> toolbarItems = [
|
||||
smartEditItem,
|
||||
paragraphItem,
|
||||
...headingItems,
|
||||
smartEditItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
|
||||
paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
|
||||
...(headingItems
|
||||
..forEach(
|
||||
(e) => e.isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
|
||||
)),
|
||||
...markdownFormatItems,
|
||||
quoteItem,
|
||||
bulletedListItem,
|
||||
numberedListItem,
|
||||
quoteItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
|
||||
bulletedListItem
|
||||
..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
|
||||
numberedListItem
|
||||
..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
|
||||
inlineMathEquationItem,
|
||||
linkItem,
|
||||
alignToolbarItem,
|
||||
@ -241,6 +246,28 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
TableBlockKeys.type: TableBlockComponentBuilder(
|
||||
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
||||
TableMenu(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
position: position,
|
||||
dir: dir,
|
||||
onBuild: onBuild,
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
TableCellBlockKeys.type: TableCellBlockComponentBuilder(
|
||||
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
||||
TableMenu(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
position: position,
|
||||
dir: dir,
|
||||
onBuild: onBuild,
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
@ -338,7 +365,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
if (supportAlignBuilderType.contains(entry.key)) ...alignAction,
|
||||
];
|
||||
|
||||
builder.showActions = (_) => true;
|
||||
builder.showActions =
|
||||
(node) => node.parent?.type != TableCellBlockKeys.type;
|
||||
builder.actionBuilder = (context, state) {
|
||||
final top = builder.configuration.padding(context.node).top;
|
||||
final padding = context.node.type == HeadingBlockKeys.type
|
||||
|
@ -0,0 +1,29 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
bool notShowInTable(EditorState editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return false;
|
||||
}
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
return nodes.every((element) {
|
||||
if (element.type == TableBlockKeys.type) {
|
||||
return false;
|
||||
}
|
||||
var parent = element.parent;
|
||||
while (parent != null) {
|
||||
if (parent.type == TableBlockKeys.type) {
|
||||
return false;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool onlyShowInSingleTextTypeSelectionAndExcludeTable(
|
||||
EditorState editorState,
|
||||
) {
|
||||
return onlyShowInSingleSelectionAndTextType(editorState) &&
|
||||
notShowInTable(editorState);
|
||||
}
|
@ -195,7 +195,7 @@ class _CalloutBlockComponentWidgetState
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (widget.actionBuilder != null) {
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: widget.node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
|
@ -203,7 +203,7 @@ class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (widget.actionBuilder != null) {
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: widget.node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
|
@ -88,7 +88,7 @@ class _DatabaseBlockComponentWidgetState
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (widget.actionBuilder != null) {
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: widget.node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
|
@ -144,7 +144,7 @@ class _MathEquationBlockComponentWidgetState
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.actionBuilder != null) {
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
|
@ -1,26 +1,19 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/user/application/user_service.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:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
return nodes.every((element) => element.delta != null);
|
||||
},
|
||||
isActive: onlyShowInSingleSelectionAndTextType,
|
||||
builder: (context, editorState, _) => SmartEditActionList(
|
||||
editorState: editorState,
|
||||
),
|
||||
|
@ -1,5 +1,7 @@
|
||||
export 'actions/block_action_list.dart';
|
||||
export 'actions/option_action.dart';
|
||||
export 'align_toolbar_item/align_toolbar_item.dart';
|
||||
export 'base/toolbar_extension.dart';
|
||||
export 'callout/callout_block_component.dart';
|
||||
export 'code_block/code_block_component.dart';
|
||||
export 'code_block/code_block_shortcut_event.dart';
|
||||
@ -19,11 +21,12 @@ export 'image/image_menu.dart';
|
||||
export 'image/image_selection_menu.dart';
|
||||
export 'inline_math_equation/inline_math_equation.dart';
|
||||
export 'inline_math_equation/inline_math_equation_toolbar_item.dart';
|
||||
export 'align_toolbar_item/align_toolbar_item.dart';
|
||||
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 'outline/outline_block_component.dart';
|
||||
export 'table/table_menu.dart';
|
||||
export 'table/table_option_action.dart';
|
||||
export 'toggle/toggle_block_component.dart';
|
||||
export 'toggle/toggle_block_shortcut_event.dart';
|
||||
|
@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_option_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
const tableActions = <TableOptionAction>[
|
||||
TableOptionAction.addAfter,
|
||||
TableOptionAction.addBefore,
|
||||
TableOptionAction.delete,
|
||||
TableOptionAction.duplicate,
|
||||
TableOptionAction.clear,
|
||||
TableOptionAction.bgColor,
|
||||
];
|
||||
|
||||
class TableMenu extends StatelessWidget {
|
||||
const TableMenu({
|
||||
super.key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
required this.position,
|
||||
required this.dir,
|
||||
this.onBuild,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
final int position;
|
||||
final TableDirection dir;
|
||||
final VoidCallback? onBuild;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final actions = tableActions.map((action) {
|
||||
switch (action) {
|
||||
case TableOptionAction.bgColor:
|
||||
return TableColorOptionAction(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
position: position,
|
||||
dir: dir,
|
||||
);
|
||||
default:
|
||||
return TableOptionActionWrapper(action);
|
||||
}
|
||||
}).toList();
|
||||
|
||||
return PopoverActionList<PopoverAction>(
|
||||
direction: dir == TableDirection.col
|
||||
? PopoverDirection.bottomWithCenterAligned
|
||||
: PopoverDirection.rightWithTopAligned,
|
||||
actions: actions,
|
||||
onPopupBuilder: onBuild,
|
||||
onClosed: onClose,
|
||||
onSelected: (action, controller) {
|
||||
if (action is TableOptionActionWrapper) {
|
||||
_onSelectAction(action.inner);
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
buildChild: (controller) => _buildOptionButton(controller, context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionButton(
|
||||
PopoverController controller,
|
||||
BuildContext context,
|
||||
) {
|
||||
return Card(
|
||||
elevation: 1.0,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.show(),
|
||||
child: Transform.rotate(
|
||||
angle: dir == TableDirection.col ? math.pi / 2 : 0,
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.drag_element_s,
|
||||
size: Size.square(18.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSelectAction(TableOptionAction action) {
|
||||
switch (action) {
|
||||
case TableOptionAction.addAfter:
|
||||
TableActions.add(node, position + 1, editorState, dir);
|
||||
break;
|
||||
case TableOptionAction.addBefore:
|
||||
TableActions.add(node, position, editorState, dir);
|
||||
break;
|
||||
case TableOptionAction.delete:
|
||||
TableActions.delete(node, position, editorState, dir);
|
||||
break;
|
||||
case TableOptionAction.clear:
|
||||
TableActions.clear(node, position, editorState, dir);
|
||||
break;
|
||||
case TableOptionAction.duplicate:
|
||||
TableActions.duplicate(node, position, editorState, dir);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/extensions/flowy_tint_extension.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:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum TableOptionAction {
|
||||
addAfter,
|
||||
addBefore,
|
||||
delete,
|
||||
duplicate,
|
||||
clear,
|
||||
|
||||
/// row|cell background color
|
||||
bgColor;
|
||||
|
||||
Widget icon(Color? color) {
|
||||
switch (this) {
|
||||
case TableOptionAction.addAfter:
|
||||
return const FlowySvg(FlowySvgs.add_s);
|
||||
case TableOptionAction.addBefore:
|
||||
return const FlowySvg(FlowySvgs.add_s);
|
||||
case TableOptionAction.delete:
|
||||
return const FlowySvg(FlowySvgs.delete_s);
|
||||
case TableOptionAction.duplicate:
|
||||
return const FlowySvg(FlowySvgs.copy_s);
|
||||
case TableOptionAction.clear:
|
||||
return const FlowySvg(FlowySvgs.close_s);
|
||||
case TableOptionAction.bgColor:
|
||||
return const FlowySvg(
|
||||
FlowySvgs.color_format_m,
|
||||
size: Size.square(12),
|
||||
).padding(all: 2.0);
|
||||
}
|
||||
}
|
||||
|
||||
String get description {
|
||||
switch (this) {
|
||||
case TableOptionAction.addAfter:
|
||||
return LocaleKeys.document_plugins_table_addAfter.tr();
|
||||
case TableOptionAction.addBefore:
|
||||
return LocaleKeys.document_plugins_table_addBefore.tr();
|
||||
case TableOptionAction.delete:
|
||||
return LocaleKeys.document_plugins_table_delete.tr();
|
||||
case TableOptionAction.duplicate:
|
||||
return LocaleKeys.document_plugins_table_duplicate.tr();
|
||||
case TableOptionAction.clear:
|
||||
return LocaleKeys.document_plugins_table_clear.tr();
|
||||
case TableOptionAction.bgColor:
|
||||
return LocaleKeys.document_plugins_table_bgColor.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TableOptionActionWrapper extends ActionCell {
|
||||
TableOptionActionWrapper(this.inner);
|
||||
|
||||
final TableOptionAction inner;
|
||||
|
||||
@override
|
||||
Widget? leftIcon(Color iconColor) => inner.icon(iconColor);
|
||||
|
||||
@override
|
||||
String get name => inner.description;
|
||||
}
|
||||
|
||||
class TableColorOptionAction extends PopoverActionCell {
|
||||
TableColorOptionAction({
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
required this.position,
|
||||
required this.dir,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
final int position;
|
||||
final TableDirection dir;
|
||||
|
||||
@override
|
||||
Widget? leftIcon(Color iconColor) =>
|
||||
TableOptionAction.bgColor.icon(iconColor);
|
||||
|
||||
@override
|
||||
String get name => TableOptionAction.bgColor.description;
|
||||
|
||||
@override
|
||||
Widget Function(
|
||||
BuildContext context,
|
||||
PopoverController parentController,
|
||||
PopoverController controller,
|
||||
) get builder => (context, parentController, controller) {
|
||||
int row = 0, col = position;
|
||||
if (dir == TableDirection.row) {
|
||||
col = 0;
|
||||
row = position;
|
||||
}
|
||||
|
||||
final cell = node.children.firstWhereOrNull(
|
||||
(n) =>
|
||||
n.attributes[TableCellBlockKeys.colPosition] == col &&
|
||||
n.attributes[TableCellBlockKeys.rowPosition] == row,
|
||||
);
|
||||
final key = dir == TableDirection.col
|
||||
? TableCellBlockKeys.colBackgroundColor
|
||||
: TableCellBlockKeys.rowBackgroundColor;
|
||||
final bgColor = cell?.attributes[key] as String?;
|
||||
final selectedColor = bgColor?.toColor();
|
||||
// get default background color from themeExtension
|
||||
final defaultColor = AFThemeExtension.of(context).tableCellBGColor;
|
||||
final colors = [
|
||||
// reset to default background color
|
||||
FlowyColorOption(
|
||||
color: defaultColor,
|
||||
name: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),
|
||||
),
|
||||
...FlowyTint.values.map(
|
||||
(e) => FlowyColorOption(
|
||||
color: e.color(context),
|
||||
name: e.tintName(AppFlowyEditorLocalizations.current),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return FlowyColorPicker(
|
||||
colors: colors,
|
||||
selected: selectedColor,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
width: 1,
|
||||
),
|
||||
onTap: (color, index) async {
|
||||
final backgroundColor = selectedColor != color ? color.toHex() : "";
|
||||
TableActions.setBgColor(
|
||||
node,
|
||||
position,
|
||||
editorState,
|
||||
backgroundColor,
|
||||
dir,
|
||||
);
|
||||
|
||||
controller.close();
|
||||
parentController.close();
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
@ -195,7 +195,7 @@ class _ToggleListBlockComponentWidgetState
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (widget.actionBuilder != null) {
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
|
Reference in New Issue
Block a user