From 9df11256ccd84d1ecb36fa038b7c04e9a8df9137 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 27 Sep 2022 17:52:32 +0800 Subject: [PATCH] feat: implement TeX plugin --- .../example/assets/example.json | 6 + .../appflowy_editor/example/lib/main.dart | 5 +- .../lib/plugin/code_block_node_widget.dart | 2 +- .../lib/plugin/tex_block_node_widget.dart | 189 ++++++++++++++++++ .../appflowy_editor/example/pubspec.yaml | 1 + 5 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index 2d441d3367..991c03296a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -9,6 +9,12 @@ "align": "center" } }, + { + "type": "tex", + "attributes": { + "tex": "x = 2" + } + }, { "type": "text", "attributes": { "subtype": "heading", "heading": "h1" }, diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index 744359052f..fecec3d3e0 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:example/plugin/code_block_node_widget.dart'; +import 'package:example/plugin/tex_block_node_widget.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -119,6 +120,7 @@ class _MyHomePageState extends State { editable: true, customBuilders: { 'text/code_block': CodeBlockNodeWidgetBuilder(), + 'tex': TeXBlockNodeWidgetBuidler(), }, shortcutEvents: [ enterInCodeBlock, @@ -126,7 +128,8 @@ class _MyHomePageState extends State { underscoreToItalic, ], selectionMenuItems: [ - codeBlockItem, + codeBlockMenuItem, + teXBlockMenuItem, ], ), ); diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart index 3949073756..645e4c0c75 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart @@ -44,7 +44,7 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) { return KeyEventResult.ignored; }; -SelectionMenuItem codeBlockItem = SelectionMenuItem( +SelectionMenuItem codeBlockMenuItem = SelectionMenuItem( name: 'Code Block', icon: const Icon(Icons.abc), keywords: ['code block'], 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 new file mode 100644 index 0000000000..80ad3642ca --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart @@ -0,0 +1,189 @@ +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), + 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.toRawString().isEmpty) { + texNodePath = selection.end.path; + TransactionBuilder(editorState) + ..insertNode( + selection.end.path, + Node( + type: 'tex', + children: LinkedList(), + attributes: {'tex': ''}, + ), + ) + ..deleteNode(textNodes.first) + ..afterSelection = selection + ..commit(); + } else { + texNodePath = selection.end.path.next; + TransactionBuilder(editorState) + ..insertNode( + selection.end.path.next, + Node( + type: 'tex', + children: LinkedList(), + attributes: {'tex': ''}, + ), + ) + ..afterSelection = selection + ..commit(); + } + 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: () { + TransactionBuilder(widget.editorState) + ..deleteNode(widget.node) + ..commit(); + }, + ), + ); + } + + 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) { + TransactionBuilder(widget.editorState) + ..updateNode( + widget.node, + {'tex': controller.text}, + ) + ..commit(); + } + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index 9f7b4e805b..fc67a9d933 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: file_picker: ^5.0.1 universal_html: ^2.0.8 highlight: ^0.7.0 + flutter_math_fork: ^0.6.3+1 dev_dependencies: flutter_test: