mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #1184 from LucasXu0/latex
feat: implement TeX plugin
This commit is contained in:
commit
8a2c57014c
@ -9,6 +9,12 @@
|
|||||||
"align": "center"
|
"align": "center"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "tex",
|
||||||
|
"attributes": {
|
||||||
|
"tex": "x = 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"attributes": { "subtype": "heading", "heading": "h1" },
|
"attributes": { "subtype": "heading", "heading": "h1" },
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:example/plugin/code_block_node_widget.dart';
|
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/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -119,6 +120,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
editable: true,
|
editable: true,
|
||||||
customBuilders: {
|
customBuilders: {
|
||||||
'text/code_block': CodeBlockNodeWidgetBuilder(),
|
'text/code_block': CodeBlockNodeWidgetBuilder(),
|
||||||
|
'tex': TeXBlockNodeWidgetBuidler(),
|
||||||
},
|
},
|
||||||
shortcutEvents: [
|
shortcutEvents: [
|
||||||
enterInCodeBlock,
|
enterInCodeBlock,
|
||||||
@ -126,7 +128,8 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
underscoreToItalic,
|
underscoreToItalic,
|
||||||
],
|
],
|
||||||
selectionMenuItems: [
|
selectionMenuItems: [
|
||||||
codeBlockItem,
|
codeBlockMenuItem,
|
||||||
|
teXBlockMenuItem,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -44,7 +44,7 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) {
|
|||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
};
|
};
|
||||||
|
|
||||||
SelectionMenuItem codeBlockItem = SelectionMenuItem(
|
SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
|
||||||
name: () => 'Code Block',
|
name: () => 'Code Block',
|
||||||
icon: const Icon(Icons.abc),
|
icon: const Icon(Icons.abc),
|
||||||
keywords: ['code block'],
|
keywords: ['code block'],
|
||||||
|
@ -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<TextNode>();
|
||||||
|
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<Node> {
|
||||||
|
@override
|
||||||
|
Widget build(NodeWidgetContext<Node> context) {
|
||||||
|
return _TeXBlockNodeWidget(
|
||||||
|
key: context.node.key,
|
||||||
|
node: context.node,
|
||||||
|
editorState: context.editorState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
NodeValidator<Node> 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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -44,6 +44,7 @@ dependencies:
|
|||||||
file_picker: ^5.0.1
|
file_picker: ^5.0.1
|
||||||
universal_html: ^2.0.8
|
universal_html: ^2.0.8
|
||||||
highlight: ^0.7.0
|
highlight: ^0.7.0
|
||||||
|
flutter_math_fork: ^0.6.3+1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user