فهرست منبع

feat: update RichText render style

Lucas.Xu 2 سال پیش
والد
کامیت
985fe14a8b

+ 3 - 0
frontend/app_flowy/packages/flowy_editor/assets/images/point.svg

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="6" y="6" width="4" height="4" rx="2" fill="#333333"/>
+</svg>

+ 3 - 0
frontend/app_flowy/packages/flowy_editor/assets/images/quote.svg

@@ -0,0 +1,3 @@
+<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
+  <rect width="40" height="160" x="80" y="20" fill="#00BCF0"/>
+</svg>

+ 207 - 0
frontend/app_flowy/packages/flowy_editor/example/assets/example.json

@@ -0,0 +1,207 @@
+{
+  "document": {
+    "type": "editor",
+    "attributes": {},
+    "children": [
+      {
+        "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"
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "🌶 Read Me",
+            "attributes": {
+              "heading": "h1"
+            }
+          }
+        ],
+        "attributes": {
+          "heading": "h1"
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "👋 Welcome to Appflowy",
+            "attributes": {
+              "heading": "h2"
+            }
+          }
+        ],
+        "attributes": {
+          "heading": "h2"
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Here are the basics:",
+            "attributes": {
+              "heading": "h3"
+            }
+          }
+        ],
+        "attributes": {
+          "heading": "h3"
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          { "insert": "Click " },
+          { "insert": "anywhere", "attributes": { "underline": true } },
+          { "insert": " and just typing." }
+        ],
+        "attributes": {
+          "list": "todo",
+          "todo": true
+        }
+      },
+      {
+        "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."
+          }
+        ],
+        "attributes": {
+          "list": "todo",
+          "todo": true
+        }
+      },
+      {
+        "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": "." }
+        ],
+        "attributes": {
+          "list": "todo",
+          "todo": true
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Here are the examples:",
+            "attributes": {
+              "heading": "h3"
+            }
+          }
+        ],
+        "attributes": {
+          "heading": "h3"
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world"
+          }
+        ],
+        "attributes": {
+          "list": "bullet"
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world"
+          }
+        ],
+        "attributes": {
+          "list": "bullet"
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world"
+          }
+        ],
+        "attributes": {
+          "list": "bullet"
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world",
+            "attributes": { "quote": true }
+          }
+        ],
+        "attributes": {
+          "quote": true
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world",
+            "attributes": { "quote": true }
+          }
+        ],
+        "attributes": {
+          "quote": true
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world"
+          }
+        ],
+        "attributes": {
+          "number": 1
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world"
+          }
+        ],
+        "attributes": {
+          "number": 2
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world"
+          }
+        ],
+        "attributes": {
+          "number": 3
+        }
+      }
+    ]
+  }
+}

+ 3 - 3
frontend/app_flowy/packages/flowy_editor/example/lib/main.dart

@@ -112,14 +112,14 @@ class _MyHomePageState extends State<MyHomePage> {
     if (page == 0) {
       return _buildFlowyEditor();
     } else if (page == 1) {
-      return _buildTextfield();
+      return _buildTextField();
     }
     return Container();
   }
 
   Widget _buildFlowyEditor() {
     return FutureBuilder<String>(
-      future: rootBundle.loadString('assets/document.json'),
+      future: rootBundle.loadString('assets/example.json'),
       builder: (context, snapshot) {
         if (!snapshot.hasData) {
           return const Center(
@@ -167,7 +167,7 @@ class _MyHomePageState extends State<MyHomePage> {
     );
   }
 
-  Widget _buildTextfield() {
+  Widget _buildTextField() {
     return const Center(
       child: TextField(),
     );

+ 4 - 1
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart

@@ -83,7 +83,10 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
   Widget _build(BuildContext context) {
     return Column(
       children: [
-        Image.network(src),
+        Image.network(
+          src,
+          height: 150.0,
+        ),
         if (node.children.isNotEmpty)
           Column(
             crossAxisAlignment: CrossAxisAlignment.start,

+ 1 - 0
frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml

@@ -64,6 +64,7 @@ flutter:
   # To add assets to your application, add an assets section, like this:
   assets:
     - document.json
+    - example.json
   #   - images/a_dot_ham.jpeg
 
   # An image asset can refer to one or more resolution-specific "variants", see

+ 23 - 11
frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart

@@ -4,24 +4,36 @@ import 'package:flutter_svg/svg.dart';
 class FlowySvg extends StatelessWidget {
   const FlowySvg({
     Key? key,
-    required this.name,
-    required this.size,
+    this.name,
+    this.size = const Size(20, 20),
     this.color,
+    this.number,
   }) : super(key: key);
 
-  final String name;
+  final String? name;
   final Size size;
   final Color? color;
+  final int? number;
 
   @override
   Widget build(BuildContext context) {
-    return SizedBox.fromSize(
-      size: size,
-      child: SvgPicture.asset(
-        'assets/images/$name.svg',
-        color: color,
-        package: 'flowy_editor',
-      ),
-    );
+    if (name != null) {
+      return SizedBox.fromSize(
+        size: size,
+        child: SvgPicture.asset(
+          'assets/images/$name.svg',
+          color: color,
+          package: 'flowy_editor',
+        ),
+      );
+    } else if (number != null) {
+      final numberText =
+          '<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"><text x="30" y="150" fill="black" font-size="160">$number.</text></svg>';
+      return SizedBox.fromSize(
+        size: size,
+        child: SvgPicture.string(numberText),
+      );
+    }
+    return Container();
   }
 }

+ 61 - 12
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart

@@ -1,8 +1,19 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/document/position.dart';
+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/flowy_editor.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';
 import 'package:flutter/rendering.dart';
-import 'package:flowy_editor/infra/flowy_svg.dart';
 
 class RichTextNodeWidgetBuilder extends NodeWidgetBuilder {
   RichTextNodeWidgetBuilder.create({
@@ -56,8 +67,12 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
       return _buildTodoListRichText(context);
     } else if (attributes.list == 'bullet') {
       return _buildBulletedListRichText(context);
-    } else if (attributes.quotes == true) {
+    } 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);
   }
@@ -151,7 +166,11 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
   }
 
   Widget _buildSingleRichText(BuildContext context) {
-    return Expanded(child: RichText(key: _textKey, text: _textSpan));
+    return SizedBox(
+      width:
+          MediaQuery.of(context).size.width - 20, // FIXME: use the const value
+      child: RichText(key: _textKey, text: _textSpan),
+    );
   }
 
   Widget _buildTodoListRichText(BuildContext context) {
@@ -161,9 +180,8 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
       children: [
         GestureDetector(
           child: FlowySvg(
-            name: name,
             key: _decorationKey,
-            size: const Size.square(20),
+            name: name,
           ),
           onTap: () => TransactionBuilder(_editorState)
             ..updateNode(_textNode, {
@@ -178,9 +196,25 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
 
   Widget _buildBulletedListRichText(BuildContext context) {
     return Row(
-      crossAxisAlignment: CrossAxisAlignment.start,
+      crossAxisAlignment: CrossAxisAlignment.center,
+      children: [
+        FlowySvg(
+          key: _decorationKey,
+          name: 'point',
+        ),
+        _buildRichText(context),
+      ],
+    );
+  }
+
+  Widget _buildNumberListRichText(BuildContext context) {
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.center,
       children: [
-        Icon(key: _decorationKey, Icons.circle),
+        FlowySvg(
+          key: _decorationKey,
+          number: _textNode.attributes.number,
+        ),
         _buildRichText(context),
       ],
     );
@@ -190,17 +224,32 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
     return Row(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
-        Icon(key: _decorationKey, Icons.format_quote),
+        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.
-    if (_textNode.attributes.list != null) {
-      final renderBox =
-          _decorationKey.currentContext?.findRenderObject() as RenderBox;
+    final renderBox = _decorationKey.currentContext
+        ?.findRenderObject()
+        ?.unwrapOrNull<RenderBox>();
+    if (renderBox != null) {
       return renderBox.localToGlobal(Offset.zero) & renderBox.size;
     }
     return Rect.zero;

+ 41 - 9
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart

@@ -8,11 +8,13 @@ class StyleKey {
   static String underline = 'underline';
   static String strikethrough = 'strikethrough';
   static String color = 'color';
+  static String highlightColor = 'highlightColor';
   static String font = 'font';
   static String href = 'href';
   static String heading = 'heading';
-  static String quotes = 'quotes';
+  static String quote = 'quote';
   static String list = 'list';
+  static String number = 'number';
   static String todo = 'todo';
   static String code = 'code';
 }
@@ -45,6 +47,16 @@ extension AttributesExtensions on Attributes {
     return null;
   }
 
+  Color? get hightlightColor {
+    if (containsKey(StyleKey.highlightColor) &&
+        this[StyleKey.highlightColor] is String) {
+      return Color(
+        int.parse(this[StyleKey.highlightColor]),
+      );
+    }
+    return null;
+  }
+
   String? get font {
     // TODO: unspport now.
     return null;
@@ -64,9 +76,9 @@ extension AttributesExtensions on Attributes {
     return null;
   }
 
-  bool get quotes {
-    if (containsKey(StyleKey.quotes) && this[StyleKey.quotes] == true) {
-      return this[StyleKey.quotes];
+  bool get quote {
+    if (containsKey(StyleKey.quote) && this[StyleKey.quote] == true) {
+      return this[StyleKey.quote];
     }
     return false;
   }
@@ -78,6 +90,13 @@ extension AttributesExtensions on Attributes {
     return null;
   }
 
+  int? get number {
+    if (containsKey(StyleKey.number) && this[StyleKey.number] is int) {
+      return this[StyleKey.number];
+    }
+    return null;
+  }
+
   bool get todo {
     if (containsKey(StyleKey.todo) && this[StyleKey.todo] is bool) {
       return this[StyleKey.todo];
@@ -102,7 +121,7 @@ extension AttributesExtensions on Attributes {
 ///
 /// Supported global rendering types:
 ///   heading: h1, h2, h3, h4, h5, h6,
-///   block quotes,
+///   block quote,
 ///   list: ordered list, bulleted list,
 ///   code block
 ///
@@ -124,6 +143,7 @@ class RichTextStyle {
         fontStyle: fontStyle,
         fontSize: fontSize,
         color: textColor,
+        backgroundColor: backgroundColor,
         decoration: textDecoration,
       ),
       recognizer: recognizer,
@@ -131,8 +151,14 @@ class RichTextStyle {
   }
 
   // bold
-  FontWeight get fontWeight =>
-      attributes.bold ? FontWeight.bold : FontWeight.normal;
+  FontWeight get fontWeight {
+    if (attributes.bold) {
+      return FontWeight.bold;
+    } else if (attributes.heading != null) {
+      return FontWeight.bold;
+    }
+    return FontWeight.normal;
+  }
 
   // underline or strikethrough
   TextDecoration get textDecoration {
@@ -152,19 +178,25 @@ class RichTextStyle {
   Color get textColor {
     if (attributes.href != null) {
       return Colors.lightBlue;
+    } else if (attributes.quote) {
+      return Colors.grey;
     }
     return attributes.color ?? Colors.black;
   }
 
+  Color get backgroundColor {
+    return attributes.hightlightColor ?? Colors.transparent;
+  }
+
   // font size
   double get fontSize {
     final heading = attributes.heading;
     if (heading != null) {
       final headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
-      final fontSizes = [30.0, 28.0, 26.0, 24.0, 22.0, 20.0];
+      final fontSizes = [30.0, 25.0, 20.0, 20.0, 20.0, 20.0];
       return fontSizes[headings.indexOf(heading)];
     } else {
-      return 18.0;
+      return 16.0;
     }
   }