Prechádzať zdrojové kódy

Merge pull request #786 from LucasXu0/feat/markdown_input

implement markdown input style, like, #, *, -, -[]
Nathan.fooo 2 rokov pred
rodič
commit
a138b32c8e

+ 2 - 0
frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart

@@ -16,6 +16,7 @@ import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_han
 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/slash_handler.dart';
 import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
+import 'package:flowy_editor/service/internal_key_event_handlers/whitespace_handler.dart';
 import 'package:flowy_editor/service/keyboard_service.dart';
 import 'package:flowy_editor/service/render_plugin_service.dart';
 import 'package:flowy_editor/service/scroll_service.dart';
@@ -40,6 +41,7 @@ List<FlowyKeyEventHandler> defaultKeyEventHandler = [
   copyPasteKeysHandler,
   enterInEdgeOfTextNodeHandler,
   updateTextStyleByCommandXHandler,
+  whiteSpaceHandler,
 ];
 
 class FlowyEditor extends StatefulWidget {

+ 13 - 5
frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart

@@ -1,8 +1,9 @@
-import 'package:flowy_editor/flowy_editor.dart';
-import 'package:flowy_editor/service/keyboard_service.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flowy_editor/service/keyboard_service.dart';
+
 // Handle delete text.
 FlowyKeyEventHandler deleteTextHandler = (editorState, event) {
   if (event.logicalKey != LogicalKeyboardKey.backspace) {
@@ -28,9 +29,16 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) {
     if (index < 0) {
       // 1. style
       if (textNode.subtype != null) {
-        transactionBuilder.updateNode(textNode, {
-          'subtype': null,
-        });
+        transactionBuilder
+          ..updateNode(textNode, {
+            'subtype': null,
+          })
+          ..afterSelection = Selection.collapsed(
+            Position(
+              path: textNode.path,
+              offset: 0,
+            ),
+          );
       } else {
         // 2. non-style
         // find previous text node.

+ 20 - 18
frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart

@@ -72,16 +72,22 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
 
   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();
-  }
 
+  TransactionBuilder(editorState)
+    ..replaceText(textNode, selection.start.offset,
+        selection.end.offset - selection.start.offset, '/')
+    ..commit();
+
+  _editorState = editorState;
+  WidgetsBinding.instance.addPostFrameCallback((_) {
+    showPopupList(context, editorState, offset);
+  });
+
+  return KeyEventResult.handled;
+};
+
+void showPopupList(
+    BuildContext context, EditorState editorState, Offset offset) {
   _popupListOverlay?.remove();
   _popupListOverlay = OverlayEntry(
     builder: (context) => Positioned(
@@ -97,16 +103,12 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
   Overlay.of(context)?.insert(_popupListOverlay!);
 
   editorState.service.selectionService.currentSelection
-      .removeListener(clearPopupListOverlay);
+      .removeListener(clearPopupList);
   editorState.service.selectionService.currentSelection
-      .addListener(clearPopupListOverlay);
-  // editorState.service.keyboardService?.disable();
-  _editorState = editorState;
-
-  return KeyEventResult.handled;
-};
+      .addListener(clearPopupList);
+}
 
-void clearPopupListOverlay() {
+void clearPopupList() {
   _popupListOverlay?.remove();
   _popupListOverlay = null;
 
@@ -215,7 +217,7 @@ class _PopupListWidgetState extends State<PopupListWidget> {
     }
 
     if (event.logicalKey == LogicalKeyboardKey.escape) {
-      clearPopupListOverlay();
+      clearPopupList();
       return KeyEventResult.handled;
     }
 

+ 131 - 0
frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/whitespace_handler.dart

@@ -0,0 +1,131 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/document/position.dart';
+import 'package:flowy_editor/document/selection.dart';
+import 'package:flowy_editor/editor_state.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/keyboard_service.dart';
+
+const _bulletedListSymbols = ['*', '-'];
+const _checkboxListSymbols = ['[x]', '-[x]'];
+const _unCheckboxListSymbols = ['[]', '-[]'];
+
+FlowyKeyEventHandler whiteSpaceHandler = (editorState, event) {
+  if (event.logicalKey != LogicalKeyboardKey.space) {
+    return KeyEventResult.ignored;
+  }
+
+  /// Process markdown input style.
+  ///
+  /// like, #, *, -, 1., -[],
+
+  final selection = editorState.service.selectionService.currentSelection.value;
+  if (selection == null || !selection.isCollapsed) {
+    return KeyEventResult.ignored;
+  }
+
+  final textNodes = editorState.service.selectionService.currentSelectedNodes
+      .whereType<TextNode>();
+  if (textNodes.length != 1) {
+    return KeyEventResult.ignored;
+  }
+
+  final textNode = textNodes.first;
+  final text = textNode.toRawString();
+  if ((_checkboxListSymbols + _unCheckboxListSymbols).any(text.startsWith)) {
+    return _toCheckboxList(editorState, textNode);
+  } else if (_bulletedListSymbols.any(text.startsWith)) {
+    return _toBulletedList(editorState, textNode);
+  } else if (_countOfSign(text) != 0) {
+    return _toHeadingStyle(editorState, textNode);
+  }
+
+  return KeyEventResult.ignored;
+};
+
+KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
+  if (textNode.subtype == StyleKey.bulletedList) {
+    return KeyEventResult.ignored;
+  }
+  TransactionBuilder(editorState)
+    ..deleteText(textNode, 0, 1)
+    ..updateNode(textNode, {
+      StyleKey.subtype: StyleKey.bulletedList,
+    })
+    ..afterSelection = Selection.collapsed(
+      Position(
+        path: textNode.path,
+        offset: 0,
+      ),
+    )
+    ..commit();
+  return KeyEventResult.handled;
+}
+
+KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
+  if (textNode.subtype == StyleKey.checkbox) {
+    return KeyEventResult.ignored;
+  }
+  final String symbol;
+  bool check = false;
+  final symbols = List<String>.from(_checkboxListSymbols)
+    ..retainWhere(textNode.toRawString().startsWith);
+  if (symbols.isNotEmpty) {
+    symbol = symbols.first;
+    check = true;
+  } else {
+    symbol = (List<String>.from(_unCheckboxListSymbols)
+          ..retainWhere(textNode.toRawString().startsWith))
+        .first;
+    check = false;
+  }
+
+  TransactionBuilder(editorState)
+    ..deleteText(textNode, 0, symbol.length)
+    ..updateNode(textNode, {
+      StyleKey.subtype: StyleKey.checkbox,
+      StyleKey.checkbox: check,
+    })
+    ..afterSelection = Selection.collapsed(
+      Position(
+        path: textNode.path,
+        offset: 0,
+      ),
+    )
+    ..commit();
+  return KeyEventResult.handled;
+}
+
+KeyEventResult _toHeadingStyle(EditorState editorState, TextNode textNode) {
+  final x = _countOfSign(textNode.toRawString());
+  final hX = 'h$x';
+  if (textNode.attributes.heading == hX) {
+    return KeyEventResult.ignored;
+  }
+  TransactionBuilder(editorState)
+    ..deleteText(textNode, 0, x)
+    ..updateNode(textNode, {
+      StyleKey.subtype: StyleKey.heading,
+      StyleKey.heading: hX,
+    })
+    ..afterSelection = Selection.collapsed(
+      Position(
+        path: textNode.path,
+        offset: 0,
+      ),
+    )
+    ..commit();
+  return KeyEventResult.handled;
+}
+
+int _countOfSign(String text) {
+  for (var i = 6; i >= 0; i--) {
+    if (text.startsWith('#' * i)) {
+      return i;
+    }
+  }
+  return 0;
+}