diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/point.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/point.svg new file mode 100644 index 0000000000..be88518d0d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/point.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/quote.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/quote.svg new file mode 100644 index 0000000000..0f3d33f6d3 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/quote.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json new file mode 100644 index 0000000000..a552175146 --- /dev/null +++ b/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 + } + } + ] + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index b8b836cc4d..6105703fa0 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -112,14 +112,14 @@ class _MyHomePageState extends State { if (page == 0) { return _buildFlowyEditor(); } else if (page == 1) { - return _buildTextfield(); + return _buildTextField(); } return Container(); } Widget _buildFlowyEditor() { return FutureBuilder( - 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 { ); } - Widget _buildTextfield() { + Widget _buildTextField() { return const Center( child: TextField(), ); 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 aaca3148c2..e33ff83e2f 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 @@ -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, diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml index 11df9b36ee..9a80a73a0a 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml +++ b/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 diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart index 5e3861e6f4..136b5db4bc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart +++ b/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 = + '$number.'; + return SizedBox.fromSize( + size: size, + child: SvgPicture.string(numberText), + ); + } + return Container(); } } 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 c0266a15bc..d90e739d97 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 @@ -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 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 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 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 with Selectable { Widget _buildBulletedListRichText(BuildContext context) { return Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(key: _decorationKey, Icons.circle), + 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), ], ); @@ -190,17 +224,32 @@ class _FlowyRichTextState extends State 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(); + if (renderBox != null) { return renderBox.localToGlobal(Offset.zero) & renderBox.size; } return Rect.zero; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index a1fd8b57a1..07d4bf4429 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/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; } }