From e476337a6a524e402359cca16e6242ee4ae5f08f Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 1 Dec 2022 16:55:10 +0800 Subject: [PATCH] feat: move code block plugin to appflowy editor plugins directory --- .../example/lib/pages/simple_editor.dart | 7 + .../lib/plugin/tex_block_node_widget.dart | 193 ------------------ .../lib/appflowy_editor_plugins.dart | 4 + .../code_block}/code_block_node_widget.dart | 157 +++++--------- .../code_block/code_block_shortcut_event.dart | 91 +++++++++ .../appflowy_editor_plugins/pubspec.yaml | 1 + 6 files changed, 155 insertions(+), 298 deletions(-) delete mode 100644 frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart rename frontend/app_flowy/packages/{appflowy_editor/example/lib/plugin => appflowy_editor_plugins/lib/src/code_block}/code_block_node_widget.dart (62%) create mode 100644 frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/code_block/code_block_shortcut_event.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/pages/simple_editor.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/pages/simple_editor.dart index 109a027a89..b998d0d4c0 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/pages/simple_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/pages/simple_editor.dart @@ -41,16 +41,23 @@ class SimpleEditor extends StatelessWidget { kDividerType: DividerWidgetBuilder(), // Math Equation kMathEquationType: MathEquationNodeWidgetBuidler(), + // Code Block + kCodeBlockType: CodeBlockNodeWidgetBuilder(), }, shortcutEvents: [ // Divider insertDividerEvent, + // Code Block + enterInCodeBlock, + ignoreKeysInCodeBlock, ], selectionMenuItems: [ // Divider dividerMenuItem, // Math Equation mathEquationMenuItem, + // Code Block + codeBlockMenuItem, ], ); } else { diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart deleted file mode 100644 index c9b0e8d478..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart +++ /dev/null @@ -1,193 +0,0 @@ -import 'dart:collection'; - -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_math_fork/flutter_math.dart'; - -SelectionMenuItem teXBlockMenuItem = SelectionMenuItem( - name: () => 'Tex', - icon: (_, __) => const Icon( - Icons.text_fields_rounded, - color: Colors.black, - size: 18.0, - ), - keywords: ['tex, latex, katex'], - handler: (editorState, _, __) { - final selection = - editorState.service.selectionService.currentSelection.value; - final textNodes = editorState.service.selectionService.currentSelectedNodes - .whereType(); - if (selection == null || !selection.isCollapsed || textNodes.isEmpty) { - return; - } - final Path texNodePath; - if (textNodes.first.toPlainText().isEmpty) { - texNodePath = selection.end.path; - final transaction = editorState.transaction - ..insertNode( - selection.end.path, - Node( - type: 'tex', - children: LinkedList(), - attributes: {'tex': ''}, - ), - ) - ..deleteNode(textNodes.first) - ..afterSelection = selection; - editorState.apply(transaction); - } else { - texNodePath = selection.end.path.next; - final transaction = editorState.transaction - ..insertNode( - selection.end.path.next, - Node( - type: 'tex', - children: LinkedList(), - attributes: {'tex': ''}, - ), - ) - ..afterSelection = selection; - editorState.apply(transaction); - } - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final texState = - editorState.document.nodeAtPath(texNodePath)?.key?.currentState; - if (texState != null && texState is __TeXBlockNodeWidgetState) { - texState.showEditingDialog(); - } - }); - }, -); - -class TeXBlockNodeWidgetBuidler extends NodeWidgetBuilder { - @override - Widget build(NodeWidgetContext context) { - return _TeXBlockNodeWidget( - key: context.node.key, - node: context.node, - editorState: context.editorState, - ); - } - - @override - NodeValidator get nodeValidator => (node) { - return node.attributes['tex'] is String; - }; -} - -class _TeXBlockNodeWidget extends StatefulWidget { - const _TeXBlockNodeWidget({ - Key? key, - required this.node, - required this.editorState, - }) : super(key: key); - - final Node node; - final EditorState editorState; - - @override - State<_TeXBlockNodeWidget> createState() => __TeXBlockNodeWidgetState(); -} - -class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> { - String get _tex => widget.node.attributes['tex'] as String; - bool _isHover = false; - - @override - Widget build(BuildContext context) { - return InkWell( - onHover: (value) { - setState(() { - _isHover = value; - }); - }, - onTap: () { - showEditingDialog(); - }, - child: Stack( - children: [ - _buildTex(context), - if (_isHover) _buildDeleteButton(context), - ], - ), - ); - } - - Widget _buildTex(BuildContext context) { - return Container( - width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.symmetric(vertical: 20), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - color: _isHover ? Colors.grey[200] : Colors.transparent, - ), - child: Center( - child: Math.tex( - _tex, - textStyle: const TextStyle(fontSize: 20), - mathStyle: MathStyle.display, - ), - ), - ); - } - - Widget _buildDeleteButton(BuildContext context) { - return Positioned( - top: -5, - right: -5, - child: IconButton( - icon: Icon( - Icons.delete_outline, - color: Colors.blue[400], - size: 16, - ), - onPressed: () { - final transaction = widget.editorState.transaction - ..deleteNode(widget.node); - widget.editorState.apply(transaction); - }, - ), - ); - } - - void showEditingDialog() { - showDialog( - context: context, - builder: (context) { - final controller = TextEditingController(text: _tex); - return AlertDialog( - title: const Text('Edit Katex'), - content: TextField( - controller: controller, - maxLines: null, - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - if (controller.text != _tex) { - final transaction = widget.editorState.transaction - ..updateNode( - widget.node, - {'tex': controller.text}, - ); - widget.editorState.apply(transaction); - } - }, - child: const Text('OK'), - ), - ], - ); - }, - ); - } -} diff --git a/frontend/app_flowy/packages/appflowy_editor_plugins/lib/appflowy_editor_plugins.dart b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/appflowy_editor_plugins.dart index 1d35d272e9..771a88720c 100644 --- a/frontend/app_flowy/packages/appflowy_editor_plugins/lib/appflowy_editor_plugins.dart +++ b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/appflowy_editor_plugins.dart @@ -6,3 +6,7 @@ export 'src/divider/divider_shortcut_event.dart'; // Math Equation export 'src/math_ equation/math_equation_node_widget.dart'; + +// Code Block +export 'src/code_block/code_block_node_widget.dart'; +export 'src/code_block/code_block_shortcut_event.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/code_block/code_block_node_widget.dart similarity index 62% rename from frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart rename to frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/code_block/code_block_node_widget.dart index 5ecf4d4ed8..3c1774b87f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/code_block/code_block_node_widget.dart @@ -1,93 +1,12 @@ -import 'dart:collection'; - import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:highlight/highlight.dart' as highlight; import 'package:highlight/languages/all.dart'; -ShortcutEvent enterInCodeBlock = ShortcutEvent( - key: 'Enter in code block', - command: 'enter', - handler: _enterInCodeBlockHandler, -); - -ShortcutEvent ignoreKeysInCodeBlock = ShortcutEvent( - key: 'White space in code block', - command: 'space,slash,shift+underscore', - handler: _ignorekHandler, -); - -ShortcutEventHandler _enterInCodeBlockHandler = (editorState, event) { - final selection = editorState.service.selectionService.currentSelection.value; - final nodes = editorState.service.selectionService.currentSelectedNodes; - final codeBlockNode = - nodes.whereType().where((node) => node.id == 'text/code_block'); - if (codeBlockNode.length != 1 || selection == null) { - return KeyEventResult.ignored; - } - if (selection.isCollapsed) { - final transaction = editorState.transaction - ..insertText(codeBlockNode.first, selection.end.offset, '\n'); - editorState.apply(transaction); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; -}; - -ShortcutEventHandler _ignorekHandler = (editorState, event) { - final nodes = editorState.service.selectionService.currentSelectedNodes; - final codeBlockNodes = - nodes.whereType().where((node) => node.id == 'text/code_block'); - if (codeBlockNodes.length == 1) { - return KeyEventResult.skipRemainingHandlers; - } - return KeyEventResult.ignored; -}; - -SelectionMenuItem codeBlockMenuItem = SelectionMenuItem( - name: () => 'Code Block', - icon: (_, __) => const Icon( - Icons.abc, - color: Colors.black, - size: 18.0, - ), - keywords: ['code block'], - handler: (editorState, _, __) { - final selection = - editorState.service.selectionService.currentSelection.value; - final textNodes = editorState.service.selectionService.currentSelectedNodes - .whereType(); - if (selection == null || textNodes.isEmpty) { - return; - } - if (textNodes.first.toPlainText().isEmpty) { - final transaction = editorState.transaction - ..updateNode(textNodes.first, { - 'subtype': 'code_block', - 'theme': 'vs', - 'language': null, - }) - ..afterSelection = selection; - editorState.apply(transaction); - } else { - final transaction = editorState.transaction - ..insertNode( - selection.end.path.next, - TextNode( - children: LinkedList(), - attributes: { - 'subtype': 'code_block', - 'theme': 'vs', - 'language': null, - }, - delta: Delta()..insert('\n'), - ), - ) - ..afterSelection = selection; - editorState.apply(transaction); - } - }, -); +const String kCodeBlockType = 'text/$kCodeBlockSubType'; +const String kCodeBlockSubType = 'code_block'; +const String kCodeBlockAttrTheme = 'theme'; +const String kCodeBlockAttrLanguage = 'language'; class CodeBlockNodeWidgetBuilder extends NodeWidgetBuilder { @override @@ -101,7 +20,8 @@ class CodeBlockNodeWidgetBuilder extends NodeWidgetBuilder { @override NodeValidator get nodeValidator => (node) { - return node is TextNode && node.attributes['theme'] is String; + return node is TextNode && + node.attributes[kCodeBlockAttrTheme] is String; }; } @@ -121,9 +41,10 @@ class _CodeBlockNodeWidge extends StatefulWidget { class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge> with SelectableMixin, DefaultSelectable { - final _richTextKey = GlobalKey(debugLabel: 'code_block_text'); - final _padding = const EdgeInsets.only(left: 20, top: 20, bottom: 20); - String? get _language => widget.textNode.attributes['language'] as String?; + final _richTextKey = GlobalKey(debugLabel: kCodeBlockType); + final _padding = const EdgeInsets.only(left: 20, top: 30, bottom: 30); + String? get _language => + widget.textNode.attributes[kCodeBlockAttrLanguage] as String?; String? _detectLanguage; @override @@ -142,11 +63,13 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge> children: [ _buildCodeBlock(context), _buildSwitchCodeButton(context), + _buildDeleteButton(context), ], ); } Widget _buildCodeBlock(BuildContext context) { + final plainText = widget.textNode.toPlainText(); final result = highlight.highlight.parse( widget.textNode.toPlainText(), language: _language, @@ -177,25 +100,49 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge> Widget _buildSwitchCodeButton(BuildContext context) { return Positioned( top: -5, - right: 0, - child: DropdownButton( - value: _detectLanguage, - onChanged: (value) { + left: 10, + child: SizedBox( + height: 35, + child: DropdownButton( + value: _detectLanguage, + iconSize: 14.0, + onChanged: (value) { + final transaction = widget.editorState.transaction + ..updateNode(widget.textNode, { + kCodeBlockAttrLanguage: value, + }); + widget.editorState.apply(transaction); + }, + items: + allLanguages.keys.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: const TextStyle(fontSize: 12.0), + ), + ); + }).toList(growable: false), + ), + ), + ); + } + + Widget _buildDeleteButton(BuildContext context) { + return Positioned( + top: -5, + right: -5, + child: IconButton( + icon: Icon( + Icons.delete_forever_outlined, + color: widget.editorState.editorStyle.selectionMenuItemIconColor, + size: 16, + ), + onPressed: () { final transaction = widget.editorState.transaction - ..updateNode(widget.textNode, { - 'language': value, - }); + ..deleteNode(widget.textNode); widget.editorState.apply(transaction); }, - items: allLanguages.keys.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text( - value, - style: const TextStyle(fontSize: 12.0), - ), - ); - }).toList(growable: false), ), ); } diff --git a/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/code_block/code_block_shortcut_event.dart b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/code_block/code_block_shortcut_event.dart new file mode 100644 index 0000000000..a001698ec1 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/code_block/code_block_shortcut_event.dart @@ -0,0 +1,91 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/src/code_block/code_block_node_widget.dart'; +import 'package:flutter/material.dart'; + +ShortcutEvent enterInCodeBlock = ShortcutEvent( + key: 'Press Enter In Code Block', + command: 'enter', + handler: _enterInCodeBlockHandler, +); + +ShortcutEvent ignoreKeysInCodeBlock = ShortcutEvent( + key: 'White space in code block', + command: 'space, slash, shift+underscore', + handler: _ignorekHandler, +); + +ShortcutEventHandler _enterInCodeBlockHandler = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final nodes = editorState.service.selectionService.currentSelectedNodes; + final codeBlockNode = + nodes.whereType().where((node) => node.id == kCodeBlockType); + if (codeBlockNode.length != 1 || + selection == null || + !selection.isCollapsed) { + return KeyEventResult.ignored; + } + + final transaction = editorState.transaction + ..insertText( + codeBlockNode.first, + selection.end.offset, + '\n', + ); + editorState.apply(transaction); + return KeyEventResult.handled; +}; + +ShortcutEventHandler _ignorekHandler = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final codeBlockNodes = + nodes.whereType().where((node) => node.id == kCodeBlockType); + if (codeBlockNodes.length == 1) { + return KeyEventResult.skipRemainingHandlers; + } + return KeyEventResult.ignored; +}; + +SelectionMenuItem codeBlockMenuItem = SelectionMenuItem( + name: () => 'Code Block', + icon: (editorState, onSelected) => Icon( + Icons.abc, + color: onSelected + ? editorState.editorStyle.selectionMenuItemSelectedIconColor + : editorState.editorStyle.selectionMenuItemIconColor, + size: 18.0, + ), + keywords: ['code block', 'code snippet'], + handler: (editorState, _, __) { + final selection = + editorState.service.selectionService.currentSelection.value; + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (selection == null || textNodes.isEmpty) { + return; + } + final transaction = editorState.transaction; + if (textNodes.first.toPlainText().isEmpty) { + transaction.updateNode(textNodes.first, { + BuiltInAttributeKey.subtype: kCodeBlockSubType, + kCodeBlockAttrTheme: 'vs', + kCodeBlockAttrLanguage: null, + }); + transaction.afterSelection = selection; + editorState.apply(transaction); + } else { + transaction.insertNode( + selection.end.path, + TextNode( + attributes: { + BuiltInAttributeKey.subtype: kCodeBlockSubType, + kCodeBlockAttrTheme: 'vs', + kCodeBlockAttrLanguage: null, + }, + delta: Delta()..insert('\n'), + ), + ); + transaction.afterSelection = selection; + } + editorState.apply(transaction); + }, +); diff --git a/frontend/app_flowy/packages/appflowy_editor_plugins/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor_plugins/pubspec.yaml index ca1908a31a..9d62309c22 100644 --- a/frontend/app_flowy/packages/appflowy_editor_plugins/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor_plugins/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: appflowy_editor: path: ../appflowy_editor flutter_math_fork: ^0.6.3+1 + highlight: ^0.7.0 dev_dependencies: flutter_test: