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 =
'';
- 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