From 35bafbaadc55543a6b8a30e9cbe64c026db34949 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 3 Aug 2022 16:34:11 +0800 Subject: [PATCH] feat: implement popup list service --- .../assets/images/popup_list/bullets.svg | 8 + .../assets/images/popup_list/checkbox.svg | 4 + .../assets/images/popup_list/h1.svg | 4 + .../assets/images/popup_list/h2.svg | 4 + .../assets/images/popup_list/h3.svg | 4 + .../assets/images/popup_list/number.svg | 3 + .../assets/images/popup_list/text.svg | 4 + .../assets/images/toolbar/code.svg | 4 + .../flowy_editor/example/pubspec.lock | 7 + .../flowy_editor/example/pubspec.yaml | 1 + .../lib/extensions/node_extensions.dart | 1 + .../flowy_editor/lib/infra/flowy_svg.dart | 20 +- .../lib/render/selection/toolbar_widget.dart | 1 + .../lib/service/editor_service.dart | 2 +- .../shortcut_handler.dart | 12 - .../slash_handler.dart | 223 ++++++++++++++++++ .../packages/flowy_editor/pubspec.yaml | 1 + 17 files changed, 280 insertions(+), 23 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/bullets.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/checkbox.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h1.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h2.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h3.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/number.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/text.svg create mode 100644 frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/code.svg delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/bullets.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/bullets.svg new file mode 100644 index 0000000000..97a2e9c434 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/bullets.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/checkbox.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/checkbox.svg new file mode 100644 index 0000000000..37f52c47ed --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h1.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h1.svg new file mode 100644 index 0000000000..6e97796956 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h2.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h2.svg new file mode 100644 index 0000000000..2c1d1d9d1c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h3.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h3.svg new file mode 100644 index 0000000000..8c6276263d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/number.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/number.svg new file mode 100644 index 0000000000..9d8b98d10d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/number.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/text.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/text.svg new file mode 100644 index 0000000000..7befa5080f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/text.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/code.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/code.svg new file mode 100644 index 0000000000..9b96b3c2dc --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/code.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock index cfadcb8242..83334af630 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock @@ -69,6 +69,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + url: "https://pub.dartlang.org" + source: hosted + version: "5.4.3+7" flutter_lints: dependency: "direct dev" description: diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml index 9a80a73a0a..0c58de8b7d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: path: ../ provider: ^6.0.3 url_launcher: ^6.1.5 + flutter_inappwebview: ^5.4.3+7 dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart index 52b7596240..b421b258b6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart @@ -9,6 +9,7 @@ extension NodeExtensions on Node { RenderBox? get renderBox => key?.currentContext?.findRenderObject()?.unwrapOrNull(); + BuildContext? get context => key?.currentContext; Selectable? get selectable => key?.currentState?.unwrapOrNull(); bool inSelection(Selection selection) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart index 136b5db4bc..12da5b5dc8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart @@ -18,20 +18,20 @@ class FlowySvg extends StatelessWidget { @override Widget build(BuildContext context) { if (name != null) { - return SizedBox.fromSize( - size: size, - child: SvgPicture.asset( - 'assets/images/$name.svg', - color: color, - package: 'flowy_editor', - ), + return SvgPicture.asset( + 'assets/images/$name.svg', + color: color, + package: 'flowy_editor', + width: size.width, + height: size.width, ); } else if (number != null) { final numberText = '$number.'; - return SizedBox.fromSize( - size: size, - child: SvgPicture.string(numberText), + return SvgPicture.string( + numberText, + width: size.width, + height: size.width, ); } return Container(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart index 91659e1d1f..1314260bca 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart @@ -201,6 +201,7 @@ class _ToolbarWidgetState extends State { ), ); }); + // TODO: disable scrolling. Overlay.of(context)?.insert(_listToolbarOverlay!); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index b62fe1bb15..c98b21c17a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -14,7 +14,7 @@ import 'package:flowy_editor/service/input_service.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/slash_handler.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart deleted file mode 100644 index f424bcf314..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -/// type '/' to trigger shortcut widget -FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { - if (event.logicalKey != LogicalKeyboardKey.slash) { - return KeyEventResult.ignored; - } - - return KeyEventResult.ignored; -}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart new file mode 100644 index 0000000000..d896f81eb6 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart @@ -0,0 +1,223 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/extensions/node_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +final List _popupListItems = [ + PopupListItem( + text: 'Text', + icon: _popupListIcon('text'), + handler: (editorState) => formatText(editorState), + ), + PopupListItem( + text: 'Heading 1', + icon: _popupListIcon('h1'), + handler: (editorState) => formatHeading(editorState, StyleKey.h1), + ), + PopupListItem( + text: 'Heading 2', + icon: _popupListIcon('h2'), + handler: (editorState) => formatHeading(editorState, StyleKey.h2), + ), + PopupListItem( + text: 'Heading 3', + icon: _popupListIcon('h3'), + handler: (editorState) => formatHeading(editorState, StyleKey.h3), + ), + PopupListItem( + text: 'Bullets', + icon: _popupListIcon('bullets'), + handler: (editorState) => formatBulletedList(editorState), + ), + PopupListItem( + text: 'Numbered list', + icon: _popupListIcon('number'), + handler: (editorState) => debugPrint('Not implement yet!'), + ), + PopupListItem( + text: 'Checkboxes', + icon: _popupListIcon('checkbox'), + handler: (editorState) => formatCheckbox(editorState), + ), +]; + +OverlayEntry? popupListOverlay; +FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.slash && !event.isMetaPressed) { + return KeyEventResult.ignored; + } + + final textNodes = editorState + .service.selectionService.currentSelectedNodes.value + .whereType(); + if (textNodes.length != 1) { + return KeyEventResult.ignored; + } + + final selection = editorState.service.selectionService.currentSelection; + final textNode = textNodes.first; + final context = textNode.context; + final selectable = textNode.selectable; + if (selection == null || context == null || selectable == null) { + return KeyEventResult.ignored; + } + + final rect = selectable.getCursorRectInPosition(selection.start); + final offset = selectable.localToGlobal(rect.topLeft); + if (!selection.isCollapsed) { + TransactionBuilder(editorState) + ..deleteText( + textNode, + selection.start.offset, + selection.end.offset - selection.start.offset, + ) + ..commit(); + } + + popupListOverlay?.remove(); + popupListOverlay = OverlayEntry( + builder: (context) => Positioned( + top: offset.dy + 15.0, + left: offset.dx, + child: PopupListWidget( + editorState: editorState, + items: _popupListItems, + ), + ), + ); + + Overlay.of(context)?.insert(popupListOverlay!); + + editorState.service.selectionService.currentSelectedNodes + .removeListener(clearPopupListOverlay); + editorState.service.selectionService.currentSelectedNodes + .addListener(clearPopupListOverlay); + + return KeyEventResult.handled; +}; + +void clearPopupListOverlay() { + popupListOverlay?.remove(); + popupListOverlay = null; +} + +class PopupListWidget extends StatefulWidget { + const PopupListWidget({ + Key? key, + required this.editorState, + required this.items, + this.maxItemInRow = 8, + }) : super(key: key); + + final EditorState editorState; + final List items; + final int maxItemInRow; + + @override + State createState() => _PopupListWidgetState(); +} + +class _PopupListWidgetState extends State { + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumns(widget.items), + ), + ); + } + + List _buildColumns(List items) { + List columns = []; + List itemWidgets = []; + for (var i = 0; i < items.length; i++) { + if (i != 0 && i % (widget.maxItemInRow - 1) == 0) { + columns.add(Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: itemWidgets, + )); + itemWidgets = []; + } + itemWidgets.add(_PopupListItemWidget( + editorState: widget.editorState, item: items[i])); + } + if (itemWidgets.isNotEmpty) { + columns.add(Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: itemWidgets, + )); + itemWidgets = []; + } + return columns; + } +} + +class _PopupListItemWidget extends StatelessWidget { + const _PopupListItemWidget({ + Key? key, + required this.item, + required this.editorState, + }) : super(key: key); + + final EditorState editorState; + final PopupListItem item; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0), + child: TextButton.icon( + icon: item.icon, + label: Text( + item.text, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.black, + fontSize: 14.0, + ), + ), + onPressed: () { + item.handler(editorState); + }, + ), + ); + } +} + +class PopupListItem { + PopupListItem({ + required this.text, + this.message = '', + required this.icon, + required this.handler, + }); + + final String text; + final String message; + final Widget icon; + final void Function(EditorState editorState) handler; +} + +Widget _popupListIcon(String name) => FlowySvg( + name: 'popup_list/$name', + color: Colors.black, + size: const Size.square(18.0), + ); diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index db0eef5296..d828d5501e 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -27,6 +27,7 @@ flutter: # To add assets to your package, add an assets section, like this: assets: - assets/images/toolbar/ + - assets/images/popup_list/ - assets/images/ - assets/document.json # - images/a_dot_burr.jpeg