mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: update RichText render style
This commit is contained in:
parent
c5560caf3c
commit
985fe14a8b
@ -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>
|
After Width: | Height: | Size: 166 B |
@ -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>
|
After Width: | Height: | Size: 135 B |
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -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(),
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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: [
|
||||
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<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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user