Browse Source

feat: add checkbox style

Lucas.Xu 2 years ago
parent
commit
fce8ea1e80

+ 44 - 59
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,7 +37,15 @@
         "type": "text",
         "delta": [
           {
-            "insert": "Here are the plugin demos:"
+            "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."
+          }
+        ]
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Here are the basics:"
           }
         ],
         "attributes": {
@@ -45,41 +53,26 @@
           "heading": "h3"
         }
       },
+      {
+        "type": "text",
+        "delta": [
+          { "insert": "Click " },
+          { "insert": "anywhere", "attributes": { "underline": true } },
+          { "insert": " and just typing." }
+        ]
+      },
       {
         "type": "text",
         "delta": [
           {
-            "insert": "Checkbox example ......"
-          }
-        ],
-        "attributes": {
-          "subtype": "checkbox",
-          "checkbox": false
-        },
-        "children": [
+            "insert": "Hit"
+          },
           {
-            "type": "text",
-            "delta": [
-              {
-                "insert": "AAA Checkbox example ......\nAAA Checkbox example ......"
-              }
-            ],
-            "attributes": {
-              "subtype": "checkbox",
-              "checkbox": false
-            }
+            "insert": "  /  ",
+            "attributes": { "highlightColor": "0xFFFFFF00" }
           },
           {
-            "type": "text",
-            "delta": [
-              {
-                "insert": "BBB Checkbox example ......"
-              }
-            ],
-            "attributes": {
-              "subtype": "checkbox",
-              "checkbox": true
-            }
+            "insert": "to see all the types of content you can add - entity, headers, videos, sub pages, etc."
           }
         ]
       },
@@ -87,15 +80,19 @@
         "type": "text",
         "delta": [
           {
-            "insert": "Raw text example ......"
-          }
+            "insert": "Highlight any text, and use the menu that pops up to "
+          },
+          { "insert": "style", "attributes": { "bold": true } },
+          { "insert": " your ", "attributes": { "italic": true } },
+          { "insert": "writing", "attributes": { "strikethrough": true } },
+          { "insert": "." }
         ]
       },
       {
         "type": "text",
         "delta": [
           {
-            "insert": "Here are the basics:"
+            "insert": "Here are the plugins:"
           }
         ],
         "attributes": {
@@ -106,60 +103,48 @@
       {
         "type": "text",
         "delta": [
-          { "insert": "Click " },
-          { "insert": "anywhere", "attributes": { "underline": true } },
-          { "insert": " and just typing." }
+          {
+            "insert": "Hello world"
+          }
         ],
         "attributes": {
           "subtype": "checkbox",
-          "checkbox": true
+          "checkbox": false
         }
       },
       {
         "type": "text",
         "delta": [
           {
-            "insert": "Hit"
-          },
-          {
-            "insert": "  /  ",
-            "attributes": { "highlightColor": "0xFFFFFF00" }
-          },
-          {
-            "insert": "to see all the types of content you can add - entity, headers, videos, sub pages, etc."
+            "insert": "Hello world"
           }
         ],
         "attributes": {
           "subtype": "checkbox",
-          "checkbox": true
+          "checkbox": false
         }
       },
       {
         "type": "text",
         "delta": [
           {
-            "insert": "Highlight any text, and use the menu that pops up to "
-          },
-          { "insert": "style", "attributes": { "bold": true } },
-          { "insert": " your ", "attributes": { "italic": true } },
-          { "insert": "writing", "attributes": { "strikethrough": true } },
-          { "insert": "." }
+            "insert": "Hello world"
+          }
         ],
         "attributes": {
           "subtype": "checkbox",
-          "checkbox": true
+          "checkbox": false
         }
       },
       {
         "type": "text",
         "delta": [
           {
-            "insert": "Here are the examples:"
+            "insert": "Hello world"
           }
         ],
         "attributes": {
-          "subtype": "heading",
-          "heading": "h3"
+          "subtype": "bullet-list"
         }
       },
       {
@@ -192,7 +177,7 @@
           }
         ],
         "attributes": {
-          "subtype": "bullet-list"
+          "subtype": "quote"
         }
       },
       {
@@ -203,7 +188,7 @@
           }
         ],
         "attributes": {
-          "quote": true
+          "subtype": "quote"
         }
       },
       {
@@ -214,7 +199,7 @@
           }
         ],
         "attributes": {
-          "quote": true
+          "subtype": "quote"
         }
       },
       {

+ 2 - 17
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,
+        )
       ],
     );
   }

+ 2 - 0
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;
   }
 

+ 25 - 141
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<FlowyRichText> 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<FlowyRichText> 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<Rect> 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<FlowyRichText> 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<FlowyRichText> 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<FlowyRichText> 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<RenderBox>();
-    if (renderBox != null) {
-      return renderBox.localToGlobal(Offset.zero) & renderBox.size;
-    }
-    return Rect.zero;
-  }
-
-  TextSpan get _decorateTextSpanWithGlobalStyle => TextSpan(
-        children: _textSpan.children
-            ?.whereType<TextSpan>()
-            .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<TextInsert>()
           .map((insert) => RichTextStyle(
                 attributes: insert.attributes ?? {},

+ 73 - 0
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<QuotedTextNodeWidget> createState() => _QuotedTextNodeWidgetState();
+}
+
+// customize
+
+class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
+    with Selectable, DefaultSelectable {
+  final _richTextKey = GlobalKey(debugLabel: 'quoted_text');
+  final leftPadding = 20.0;
+
+  @override
+  Selectable<StatefulWidget> 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,
+        ),
+      ],
+    );
+  }
+}