diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index 2fb4a4d8a0..6a0fba3021 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -6,7 +6,7 @@ { "type": "image", "attributes": { - "image_src": "https://images.pexels.com/photos/2253275/pexels-photo-2253275.jpeg?cs=srgb&dl=pexels-helena-lopes-2253275.jpg&fm=jpg" + "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png" } }, { @@ -37,57 +37,7 @@ "type": "text", "delta": [ { - "insert": "Here are the plugin demos:" - } - ], - "attributes": { - "subtype": "heading", - "heading": "h3" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Checkbox example ......" - } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": false - }, - "children": [ - { - "type": "text", - "delta": [ - { - "insert": "AAA Checkbox example ......\nAAA Checkbox example ......" - } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": false - } - }, - { - "type": "text", - "delta": [ - { - "insert": "BBB Checkbox example ......" - } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": true - } - } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "Raw text example ......" + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." } ] }, @@ -109,11 +59,7 @@ { "insert": "Click " }, { "insert": "anywhere", "attributes": { "underline": true } }, { "insert": " and just typing." } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": true - } + ] }, { "type": "text", @@ -128,11 +74,7 @@ { "insert": "to see all the types of content you can add - entity, headers, videos, sub pages, etc." } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": true - } + ] }, { "type": "text", @@ -144,17 +86,13 @@ { "insert": " your ", "attributes": { "italic": true } }, { "insert": "writing", "attributes": { "strikethrough": true } }, { "insert": "." } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": true - } + ] }, { "type": "text", "delta": [ { - "insert": "Here are the examples:" + "insert": "Here are the plugins:" } ], "attributes": { @@ -162,6 +100,42 @@ "heading": "h3" } }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "subtype": "checkbox", + "checkbox": false + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "subtype": "checkbox", + "checkbox": false + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "subtype": "checkbox", + "checkbox": false + } + }, { "type": "text", "delta": [ @@ -203,7 +177,7 @@ } ], "attributes": { - "quote": true + "subtype": "quote" } }, { @@ -214,7 +188,18 @@ } ], "attributes": { - "quote": true + "subtype": "quote" + } + }, + { + "type": "text", + "delta": [ + { + "insert": "Hello world" + } + ], + "attributes": { + "subtype": "quote" } }, { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index e33ff83e2f..a57f41cf76 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -85,23 +85,8 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { children: [ Image.network( src, - height: 150.0, - ), - if (node.children.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( - (e) => editorState.renderPlugins.buildWidget( - context: NodeWidgetContext( - buildContext: context, - node: e, - editorState: editorState, - ), - ), - ) - .toList(), - ), + width: MediaQuery.of(context).size.width, + ) ], ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index a417d80513..82ffc38e6a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -4,6 +4,7 @@ import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; import 'package:flowy_editor/render/rich_text/heading_text.dart'; import 'package:flowy_editor/render/rich_text/number_list_text.dart'; +import 'package:flowy_editor/render/rich_text/quoted_text.dart'; import 'package:flowy_editor/service/service.dart'; import 'package:flutter/material.dart'; @@ -53,6 +54,7 @@ class EditorState { 'text/bullet-list', BulletedListTextNodeWidgetBuilder.create); renderPlugins.register( 'text/number-list', NumberListTextNodeWidgetBuilder.create); + renderPlugins.register('text/quote', QuotedTextNodeWidgetBuilder.create); undoManager.state = this; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 0891ee72cf..89ebce3026 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -4,12 +4,9 @@ import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/document/path.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/render/node_widget_builder.dart'; import 'package:flowy_editor/render/render_plugins.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flutter/material.dart'; @@ -56,37 +53,21 @@ class FlowyRichText extends StatefulWidget { class _FlowyRichTextState extends State with Selectable { final _textKey = GlobalKey(); - final _decorationKey = GlobalKey(); - EditorState get _editorState => widget.editorState; - TextNode get _textNode => widget.textNode; RenderParagraph get _renderParagraph => _textKey.currentContext?.findRenderObject() as RenderParagraph; @override Widget build(BuildContext context) { - final attributes = _textNode.attributes; - // TODO: use factory method ?? - if (attributes.list == 'todo') { - // return _buildTodoListRichText(context); - } else if (attributes.list == 'bullet') { - // return _buildBulletedListRichText(context); - } else if (attributes.quote == true) { - return _buildQuotedRichText(context); - } else if (attributes.heading != null) { - // return _buildHeadingRichText(context); - } else if (attributes.number != null) { - // return _buildNumberListRichText(context); - } return _buildRichText(context); } @override - Position start() => Position(path: _textNode.path, offset: 0); + Position start() => Position(path: widget.textNode.path, offset: 0); @override - Position end() => - Position(path: _textNode.path, offset: _textNode.toRawString().length); + Position end() => Position( + path: widget.textNode.path, offset: widget.textNode.toRawString().length); @override Rect getCursorRectInPosition(Position position) { @@ -108,23 +89,22 @@ class _FlowyRichTextState extends State with Selectable { Position getPositionInOffset(Offset start) { final offset = _renderParagraph.globalToLocal(start); final baseOffset = _renderParagraph.getPositionForOffset(offset).offset; - return Position(path: _textNode.path, offset: baseOffset); + return Position(path: widget.textNode.path, offset: baseOffset); } @override List getRectsInSelection(Selection selection) { assert(pathEquals(selection.start.path, selection.end.path) && - pathEquals(selection.start.path, _textNode.path)); + pathEquals(selection.start.path, widget.textNode.path)); final textSelection = TextSelection( baseOffset: selection.start.offset, extentOffset: selection.end.offset, ); - final baseRect = frontWidgetRect(); - return _renderParagraph.getBoxesForSelection(textSelection).map((box) { - final rect = box.toRect(); - return rect.translate(baseRect.centerRight.dx, 0); - }).toList(); + return _renderParagraph + .getBoxesForSelection(textSelection) + .map((box) => box.toRect()) + .toList(); } @override @@ -134,7 +114,7 @@ class _FlowyRichTextState extends State with Selectable { final baseOffset = _renderParagraph.getPositionForOffset(localStart).offset; final extentOffset = _renderParagraph.getPositionForOffset(localEnd).offset; return Selection.single( - path: _textNode.path, + path: widget.textNode.path, startOffset: baseOffset, endOffset: extentOffset, ); @@ -144,18 +124,29 @@ class _FlowyRichTextState extends State with Selectable { return _buildSingleRichText(context); } + Widget _buildSingleRichText(BuildContext context) { + final textSpan = _textSpan; + return RichText( + key: _textKey, + text: widget.textSpanDecorator != null + ? widget.textSpanDecorator!(textSpan) + : textSpan, + ); + } + + // unused now. Widget _buildRichTextWithChildren(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSingleRichText(context), - ..._textNode.children + ...widget.textNode.children .map( - (child) => _editorState.renderPlugins.buildWidget( + (child) => widget.editorState.renderPlugins.buildWidget( context: NodeWidgetContext( buildContext: context, node: child, - editorState: _editorState, + editorState: widget.editorState, ), ), ) @@ -164,115 +155,8 @@ class _FlowyRichTextState extends State with Selectable { ); } - Widget _buildSingleRichText(BuildContext context) { - return RichText( - key: _textKey, - text: widget.textSpanDecorator != null - ? widget.textSpanDecorator!(_decorateTextSpanWithGlobalStyle) - : _decorateTextSpanWithGlobalStyle, - ); - } - - Widget _buildTodoListRichText(BuildContext context) { - final name = _textNode.attributes.todo ? 'check' : 'uncheck'; - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - child: FlowySvg( - key: _decorationKey, - name: name, - ), - onTap: () => TransactionBuilder(_editorState) - ..updateNode(_textNode, { - 'todo': !_textNode.attributes.todo, - }) - ..commit(), - ), - _buildRichText(context), - ], - ); - } - - Widget _buildBulletedListRichText(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - FlowySvg( - key: _decorationKey, - name: 'point', - ), - _buildRichText(context), - ], - ); - } - - Widget _buildNumberListRichText(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - FlowySvg( - key: _decorationKey, - number: _textNode.attributes.number, - ), - _buildRichText(context), - ], - ); - } - - Widget _buildQuotedRichText(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - key: _decorationKey, - name: 'quote', - ), - _buildRichText(context), - ], - ); - } - - Widget _buildHeadingRichText(BuildContext context) { - // TODO: customize - return Column( - children: [ - const Padding(padding: EdgeInsets.only(top: 5)), - _buildRichText(context), - const Padding(padding: EdgeInsets.only(top: 5)), - ], - ); - } - - Rect frontWidgetRect() { - // FIXME: find a more elegant way to solve this situation. - final renderBox = _decorationKey.currentContext - ?.findRenderObject() - ?.unwrapOrNull(); - if (renderBox != null) { - return renderBox.localToGlobal(Offset.zero) & renderBox.size; - } - return Rect.zero; - } - - TextSpan get _decorateTextSpanWithGlobalStyle => TextSpan( - children: _textSpan.children - ?.whereType() - .map( - (span) => TextSpan( - text: span.text, - style: span.style?.copyWith( - fontSize: _textNode.attributes.fontSize, - color: _textNode.attributes.quoteColor, - ), - recognizer: span.recognizer, - ), - ) - .toList(), - ); - TextSpan get _textSpan => TextSpan( - children: _textNode.delta.operations + children: widget.textNode.delta.operations .whereType() .map((insert) => RichTextStyle( attributes: insert.attributes ?? {}, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart new file mode 100644 index 0000000000..773fd0debe --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart @@ -0,0 +1,73 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/render/node_widget_builder.dart'; +import 'package:flowy_editor/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flutter/material.dart'; + +class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder { + QuotedTextNodeWidgetBuilder.create({ + required super.editorState, + required super.node, + required super.key, + }) : super.create(); + + @override + Widget build(BuildContext context) { + return QuotedTextNodeWidget( + key: key, + textNode: node as TextNode, + editorState: editorState, + ); + } +} + +class QuotedTextNodeWidget extends StatefulWidget { + const QuotedTextNodeWidget({ + Key? key, + required this.textNode, + required this.editorState, + }) : super(key: key); + + final TextNode textNode; + final EditorState editorState; + + @override + State createState() => _QuotedTextNodeWidgetState(); +} + +// customize + +class _QuotedTextNodeWidgetState extends State + with Selectable, DefaultSelectable { + final _richTextKey = GlobalKey(debugLabel: 'quoted_text'); + final leftPadding = 20.0; + + @override + Selectable get forward => + _richTextKey.currentState as Selectable; + + @override + Offset get baseOffset { + return Offset(leftPadding, 0); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const FlowySvg( + name: 'quote', + ), + FlowyRichText( + key: _richTextKey, + textNode: widget.textNode, + editorState: widget.editorState, + ), + ], + ); + } +}