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 5fa772d11f..b58b367ef3 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 @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:flutter/material.dart'; class SimpleEditor extends StatelessWidget { @@ -30,10 +31,20 @@ class SimpleEditor extends StatelessWidget { ), ); onEditorStateChange(editorState); + return AppFlowyEditor( editorState: editorState, themeData: themeData, autoFocus: editorState.document.isEmpty, + customBuilders: { + kDividerType: DividerWidgetBuilder(), + }, + shortcutEvents: [ + insertDividerEvent, + ], + selectionMenuItems: [ + dividerMenuItem, + ], ); } else { return const Center( diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart deleted file mode 100644 index 0ca302de18..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'dart:collection'; - -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -ShortcutEvent insertHorizontalRule = ShortcutEvent( - key: 'Horizontal rule', - command: 'Minus', - handler: _insertHorzaontalRule, -); - -ShortcutEventHandler _insertHorzaontalRule = (editorState, event) { - final selection = editorState.service.selectionService.currentSelection.value; - final textNodes = editorState.service.selectionService.currentSelectedNodes - .whereType(); - if (textNodes.length != 1 || selection == null) { - return KeyEventResult.ignored; - } - final textNode = textNodes.first; - if (textNode.toPlainText() == '--') { - final transaction = editorState.transaction - ..deleteText(textNode, 0, 2) - ..insertNode( - textNode.path, - Node( - type: 'horizontal_rule', - children: LinkedList(), - attributes: {}, - ), - ) - ..afterSelection = - Selection.single(path: textNode.path.next, startOffset: 0); - editorState.apply(transaction); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; -}; - -SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem( - name: () => 'Horizontal rule', - icon: (_, __) => const Icon( - Icons.horizontal_rule, - color: Colors.black, - size: 18.0, - ), - keywords: ['horizontal rule'], - handler: (editorState, _, __) { - final selection = - editorState.service.selectionService.currentSelection.value; - final textNodes = editorState.service.selectionService.currentSelectedNodes - .whereType(); - if (selection == null || textNodes.isEmpty) { - return; - } - final textNode = textNodes.first; - if (textNode.toPlainText().isEmpty) { - final transaction = editorState.transaction - ..insertNode( - textNode.path, - Node( - type: 'horizontal_rule', - children: LinkedList(), - attributes: {}, - ), - ) - ..afterSelection = - Selection.single(path: textNode.path.next, startOffset: 0); - editorState.apply(transaction); - } else { - final transaction = editorState.transaction - ..insertNode( - selection.end.path.next, - TextNode( - children: LinkedList(), - attributes: { - 'subtype': 'horizontal_rule', - }, - delta: Delta()..insert('---'), - ), - ) - ..afterSelection = selection; - editorState.apply(transaction); - } - }, -); - -class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder { - @override - Widget build(NodeWidgetContext context) { - return _HorizontalRuleWidget( - key: context.node.key, - node: context.node, - editorState: context.editorState, - ); - } - - @override - NodeValidator get nodeValidator => (node) { - return true; - }; -} - -class _HorizontalRuleWidget extends StatefulWidget { - const _HorizontalRuleWidget({ - Key? key, - required this.node, - required this.editorState, - }) : super(key: key); - - final Node node; - final EditorState editorState; - - @override - State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState(); -} - -class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget> - with SelectableMixin { - RenderBox get _renderBox => context.findRenderObject() as RenderBox; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Container( - height: 1, - color: Colors.grey, - ), - ); - } - - @override - Position start() => Position(path: widget.node.path, offset: 0); - - @override - Position end() => Position(path: widget.node.path, offset: 1); - - @override - Position getPositionInOffset(Offset start) => end(); - - @override - bool get shouldCursorBlink => false; - - @override - CursorStyle get cursorStyle => CursorStyle.borderLine; - - @override - Rect? getCursorRectInPosition(Position position) { - final size = _renderBox.size; - return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height); - } - - @override - List getRectsInSelection(Selection selection) => - [Offset.zero & _renderBox.size]; - - @override - Selection getSelectionInRange(Offset start, Offset end) => Selection.single( - path: widget.node.path, - startOffset: 0, - endOffset: 1, - ); - - @override - Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset); -} diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index a60ba61f30..885573b00a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -45,6 +45,8 @@ dependencies: universal_html: ^2.0.8 highlight: ^0.7.0 flutter_math_fork: ^0.6.3+1 + appflowy_editor_plugins: + path: ../../../packages/appflowy_editor_plugins dev_dependencies: flutter_test: 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 2755e2f2a3..6fe8f0d01a 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 @@ -1,7 +1,4 @@ library appflowy_editor_plugins; -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; -} +export 'src/divider/divider_node_widget.dart'; +export 'src/divider/divider_shortcut_event.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/divider/divider_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/divider/divider_node_widget.dart new file mode 100644 index 0000000000..73d7030804 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/divider/divider_node_widget.dart @@ -0,0 +1,84 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +const String kDividerType = 'divider'; + +class DividerWidgetBuilder extends NodeWidgetBuilder { + @override + Widget build(NodeWidgetContext context) { + return _DividerWidget( + key: context.node.key, + node: context.node, + editorState: context.editorState, + ); + } + + @override + NodeValidator get nodeValidator => (node) { + return true; + }; +} + +class _DividerWidget extends StatefulWidget { + const _DividerWidget({ + Key? key, + required this.node, + required this.editorState, + }) : super(key: key); + + final Node node; + final EditorState editorState; + + @override + State<_DividerWidget> createState() => _DividerWidgetState(); +} + +class _DividerWidgetState extends State<_DividerWidget> with SelectableMixin { + RenderBox get _renderBox => context.findRenderObject() as RenderBox; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Container( + height: 1, + color: Colors.grey, + ), + ); + } + + @override + Position start() => Position(path: widget.node.path, offset: 0); + + @override + Position end() => Position(path: widget.node.path, offset: 1); + + @override + Position getPositionInOffset(Offset start) => end(); + + @override + bool get shouldCursorBlink => false; + + @override + CursorStyle get cursorStyle => CursorStyle.borderLine; + + @override + Rect? getCursorRectInPosition(Position position) { + final size = _renderBox.size; + return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height); + } + + @override + List getRectsInSelection(Selection selection) => + [Offset.zero & _renderBox.size]; + + @override + Selection getSelectionInRange(Offset start, Offset end) => Selection.single( + path: widget.node.path, + startOffset: 0, + endOffset: 1, + ); + + @override + Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset); +} diff --git a/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/divider/divider_shortcut_event.dart b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/divider/divider_shortcut_event.dart new file mode 100644 index 0000000000..2d4ad39bf1 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/divider/divider_shortcut_event.dart @@ -0,0 +1,72 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/src/divider/divider_node_widget.dart'; +import 'package:flutter/material.dart'; + +// insert divider into a document by typing three minuses. +// --- +ShortcutEvent insertDividerEvent = ShortcutEvent( + key: 'Divider', + command: 'Minus', + handler: _insertDividerHandler, +); + +ShortcutEventHandler _insertDividerHandler = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (textNodes.length != 1 || selection == null) { + return KeyEventResult.ignored; + } + final textNode = textNodes.first; + if (textNode.toPlainText() != '--') { + return KeyEventResult.ignored; + } + final transaction = editorState.transaction + ..deleteText(textNode, 0, 2) // remove the existing minuses. + ..insertNode(textNode.path, Node(type: kDividerType)) // insert the divder + ..afterSelection = Selection.single( + // update selection to the next text node. + path: textNode.path.next, + startOffset: 0, + ); + editorState.apply(transaction); + return KeyEventResult.handled; +}; + +SelectionMenuItem dividerMenuItem = SelectionMenuItem( + name: () => 'Divider', + icon: (editorState, onSelected) => Icon( + Icons.horizontal_rule, + color: onSelected + ? editorState.editorStyle.selectionMenuItemSelectedIconColor + : editorState.editorStyle.selectionMenuItemIconColor, + size: 18.0, + ), + keywords: ['horizontal rule', 'divider'], + handler: (editorState, _, __) { + final selection = + editorState.service.selectionService.currentSelection.value; + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (textNodes.length != 1 || selection == null) { + return; + } + final textNode = textNodes.first; + // insert the divider at current path if the text node is empty. + if (textNode.toPlainText().isEmpty) { + final transaction = editorState.transaction + ..insertNode(textNode.path, Node(type: kDividerType)) + ..afterSelection = Selection.single( + path: textNode.path.next, + startOffset: 0, + ); + editorState.apply(transaction); + } else { + // insert the divider at the path next to current path if the text node is not empty. + final transaction = editorState.transaction + ..insertNode(selection.end.path.next, Node(type: kDividerType)) + ..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 09b3c9813f..74ee8176ca 100644 --- a/frontend/app_flowy/packages/appflowy_editor_plugins/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor_plugins/pubspec.yaml @@ -1,7 +1,9 @@ name: appflowy_editor_plugins description: A new Flutter package project. version: 0.0.1 -homepage: +homepage: https://github.com/AppFlowy-IO/AppFlowy + +publish_to: none environment: sdk: ">=2.17.6 <3.0.0" @@ -10,6 +12,8 @@ environment: dependencies: flutter: sdk: flutter + appflowy_editor: + path: ../appflowy_editor dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/appflowy_editor_plugins/test/appflowy_editor_plugins_test.dart b/frontend/app_flowy/packages/appflowy_editor_plugins/test/appflowy_editor_plugins_test.dart index 28c3d5a539..8b13789179 100644 --- a/frontend/app_flowy/packages/appflowy_editor_plugins/test/appflowy_editor_plugins_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor_plugins/test/appflowy_editor_plugins_test.dart @@ -1,12 +1 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; - -void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); - }); -}