feat: support RTL and auto direction (#191)

This commit is contained in:
Mohammad Zolfaghari 2023-07-12 06:21:38 +03:30 • committed by GitHub
commit f8da435ad5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 1148 additions and 473 deletions

View file

@ -246,4 +246,24 @@ Widget build(BuildContext context) {
} }
``` ```
![After](./images/customizing_a_theme_after.png) ![After](./images/customizing_a_theme_after.png)
## Changing default text direction
The text direction is LTR by default, but you can change it by wrapping AppFlowyEditor with Directionality.
In code snippet below we set the default direction to RTL.
```dart
@override
Widget build(BuildContext context) {
return Scaffold(
body: Directionality(
textDirection: TextDirection.rtl,
child: AppFlowyEditor(
editorState: EditorState.blank(),
),
),
);
}
```

View file

@ -45,7 +45,8 @@ class Editor extends StatelessWidget {
numberedListItem, numberedListItem,
linkItem, linkItem,
buildTextColorItem(), buildTextColorItem(),
buildHighlightColorItem() buildHighlightColorItem(),
...textDirectionItems
], ],
editorState: editorState, editorState: editorState,
scrollController: scrollController, scrollController: scrollController,

View file

@ -199,7 +199,8 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
.toList(growable: false); .toList(growable: false);
} }
if (attributes.isNotEmpty) { if (attributes.isNotEmpty) {
map['data'] = attributes; // filter the null value
map['data'] = attributes..removeWhere((_, value) => value == null);
} }
return map; return map;
} }

View file

@ -1,9 +1,7 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const blockComponentBackgroundColor = 'bgColor'; mixin BlockComponentBackgroundColorMixin {
mixin BackgroundColorMixin {
Node get node; Node get node;
Color get backgroundColor { Color get backgroundColor {

View file

@ -5,14 +5,25 @@ import 'package:flutter/material.dart';
class BlockComponentConfiguration { class BlockComponentConfiguration {
const BlockComponentConfiguration({ const BlockComponentConfiguration({
this.padding = _padding, this.padding = _padding,
this.indentPadding = _indentPadding,
this.placeholderText = _placeholderText, this.placeholderText = _placeholderText,
this.textStyle = _textStyle, this.textStyle = _textStyle,
this.placeholderTextStyle = _placeholderTextStyle, this.placeholderTextStyle = _placeholderTextStyle,
}); });
/// The padding of a block component. /// The padding of a block component.
///
/// It works only for the block component itself, not for the children.
final EdgeInsets Function(Node node) padding; final EdgeInsets Function(Node node) padding;
/// The padding of a block component.
///
/// It works only for the block that needs to be indented.
final EdgeInsets Function(
Node node,
TextDirection textDirection,
) indentPadding;
/// The text style of a block component. /// The text style of a block component.
final TextStyle Function(Node node) textStyle; final TextStyle Function(Node node) textStyle;
@ -57,6 +68,15 @@ EdgeInsets _padding(Node node) {
return const EdgeInsets.symmetric(vertical: 4.0); return const EdgeInsets.symmetric(vertical: 4.0);
} }
EdgeInsets _indentPadding(Node node, TextDirection textDirection) {
switch (textDirection) {
case TextDirection.ltr:
return const EdgeInsets.only(left: 30.0);
case TextDirection.rtl:
return const EdgeInsets.only(right: 30.0);
}
}
TextStyle _textStyle(Node node) { TextStyle _textStyle(Node node) {
return const TextStyle(); return const TextStyle();
} }

View file

@ -0,0 +1,29 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
class BlockComponentPadding extends StatelessWidget {
const BlockComponentPadding({
super.key,
required this.node,
required this.padding,
this.indentPadding = EdgeInsets.zero,
required this.child,
});
final Node node;
final Widget child;
final EdgeInsets padding;
final EdgeInsets indentPadding;
@override
Widget build(BuildContext context) {
final level = node.level.toDouble();
return Padding(
padding: padding,
child: Padding(
padding: indentPadding * level,
child: child,
),
);
}
}

View file

@ -39,14 +39,16 @@ CommandShortcutEventHandler _convertToParagraphCommandHandler = (editorState) {
if (index >= 0) { if (index >= 0) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
final textDirection = node.attributes[blockComponentTextDirection];
final transaction = editorState.transaction; final transaction = editorState.transaction;
transaction transaction
..insertNode( ..insertNode(
node.path, node.path,
paragraphNode( paragraphNode(
attributes: { attributes: {
'delta': delta.toJson(), ParagraphBlockKeys.delta: delta.toJson(),
}, },
textDirection: textDirection,
children: node.children, children: node.children,
), ),
deepCopy: true, deepCopy: true,

View file

@ -0,0 +1,69 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
/// This regular expression matches either a single character from specific RTL (right-to-left) script categories or a digit or character from various other script categories.
/// It can be useful for identifying or manipulating text with mixed directional properties.
final _regex = RegExp(
r"(?:([\p{sc=Arabic}\p{sc=Hebrew}\p{sc=Syriac}\p{sc=Thaana}])|([0-9\p{sc=Armenian}\p{sc=Bengali}\p{sc=Bopomofo}\p{sc=Braille}\p{sc=Buhid}\p{sc=Canadian_Aboriginal}\p{sc=Cherokee}\p{sc=Cyrillic}\p{sc=Devanagari}\p{sc=Ethiopic}\p{sc=Georgian}\p{sc=Greek}\p{sc=Gujarati}\p{sc=Gurmukhi}\p{sc=Han}\p{sc=Hangul}\p{sc=Hanunoo}\p{sc=Hiragana}\p{sc=Inherited}\p{sc=Kannada}\p{sc=Katakana}\p{sc=Khmer}\p{sc=Lao}\p{sc=Latin}\p{sc=Limbu}\p{sc=Malayalam}\p{sc=Mongolian}\p{sc=Myanmar}\p{sc=Ogham}\p{sc=Oriya}\p{sc=Runic}\p{sc=Sinhala}\p{sc=Tagalog}\p{sc=Tagbanwa}\p{sc=Tamil}\p{sc=Telugu}\p{sc=Thai}\p{sc=Tibetan}\p{sc=Yi}]))",
unicode: true,
);
mixin BlockComponentTextDirectionMixin {
Node get node;
/// Calculate the text direction of a block component.
TextDirection calculateTextDirection({
TextDirection? defaultTextDirection,
}) {
defaultTextDirection = defaultTextDirection ?? TextDirection.ltr;
// if the block component has a text direction attribute, use it
final value = node.attributes[blockComponentTextDirection] as String?;
if (value != null && value != blockComponentTextDirectionAuto) {
return value.toTextDirection(fallback: defaultTextDirection);
}
// if the block component doesn't has a text direction attribute, but has a
// parent, use the text direction of the parent
final previousNodeContainsTextDirection = node.previousNodeWhere(
(element) => element.attributes.containsKey(blockComponentTextDirection),
);
if (value == blockComponentTextDirectionAuto &&
previousNodeContainsTextDirection != null) {
final String previousValue = previousNodeContainsTextDirection
.attributes[blockComponentTextDirection];
defaultTextDirection =
previousValue.toTextDirection(fallback: defaultTextDirection);
}
// if the value is null or the text is null or empty,
// use the default text direction
final text = node.delta?.toPlainText();
if (value == null || text == null || text.isEmpty) {
return defaultTextDirection;
}
// if the value is auto and the text isn't null or empty,
// calculate the text direction by the text
final matches = _regex.firstMatch(text);
if (matches != null) {
if (matches.group(1) != null) {
return TextDirection.rtl;
} else if (matches.group(2) != null) {
return TextDirection.ltr;
}
}
return defaultTextDirection;
}
}
extension on String {
TextDirection toTextDirection({required TextDirection fallback}) {
if (this == blockComponentTextDirectionLTR) {
return TextDirection.ltr;
} else if (this == blockComponentTextDirectionRTL) {
return TextDirection.rtl;
}
return fallback;
}
}

View file

@ -3,12 +3,10 @@ import 'package:flutter/material.dart';
class NestedListWidget extends StatelessWidget { class NestedListWidget extends StatelessWidget {
const NestedListWidget({ const NestedListWidget({
super.key, super.key,
this.padding = const EdgeInsets.only(left: 30.0),
required this.child, required this.child,
required this.children, required this.children,
}); });
final EdgeInsets padding;
final Widget child; final Widget child;
final List<Widget> children; final List<Widget> children;
@ -20,14 +18,11 @@ class NestedListWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
child, child,
Padding( Column(
padding: padding, mainAxisSize: MainAxisSize.max,
child: Column( mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, children: children,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
), ),
], ],
); );

View file

@ -0,0 +1,24 @@
/// the delta of the block component
///
/// its value is a string of json format, e.g. '{"insert":"Hello World"}'
/// for more information, please refer to https://quilljs.com/docs/delta/
const blockComponentDelta = 'delta';
/// the background of the block component
///
/// its value is a string of hex code, e.g. '#00000000'
const blockComponentBackgroundColor = 'bgColor';
/// the text direction of the block component
///
/// its value must be one of the following:
/// - [blockComponentTextDirectionLTR] or 'ltr': left to right
/// - [blockComponentTextDirectionRTL] or 'rtl': right to left
/// - [blockComponentTextDirectionAuto] or auto: depends on the text
///
/// only works for the block with text,
/// e.g. paragraph, heading, quote, to-do list, bulleted list, numbered list
const blockComponentTextDirection = 'textDirection';
const blockComponentTextDirectionAuto = 'auto';
const blockComponentTextDirectionLTR = 'ltr';
const blockComponentTextDirectionRTL = 'rtl';

View file

@ -37,6 +37,7 @@ export 'base_component/convert_to_paragraph_command.dart';
export 'base_component/insert_newline_in_type_command.dart'; export 'base_component/insert_newline_in_type_command.dart';
export 'base_component/indent_command.dart'; export 'base_component/indent_command.dart';
export 'base_component/outdent_command.dart'; export 'base_component/outdent_command.dart';
export 'base_component/block_component_padding.dart';
export 'base_component/widget/full_screen_overlay_entry.dart'; export 'base_component/widget/full_screen_overlay_entry.dart';
export 'base_component/widget/ignore_parent_pointer.dart'; export 'base_component/widget/ignore_parent_pointer.dart';
@ -48,8 +49,10 @@ export 'rich_text/appflowy_rich_text_keys.dart';
export 'base_component/block_component_configuration.dart'; export 'base_component/block_component_configuration.dart';
export 'base_component/text_style_configuration.dart'; export 'base_component/text_style_configuration.dart';
export 'base_component/background_color_mixin.dart'; export 'base_component/background_color_mixin.dart';
export 'base_component/text_direction_mixin.dart';
export 'base_component/markdown_format_helper.dart'; export 'base_component/markdown_format_helper.dart';
export 'base_component/widget/nested_list_widget.dart'; export 'base_component/widget/nested_list_widget.dart';
export 'base_component/block_component_action_wrapper.dart'; export 'base_component/block_component_action_wrapper.dart';
export 'standard_block_components.dart'; export 'standard_block_components.dart';
export 'base_component_keys.dart';

View file

@ -6,18 +6,27 @@ class BulletedListBlockKeys {
BulletedListBlockKeys._(); BulletedListBlockKeys._();
static const String type = 'bulleted_list'; static const String type = 'bulleted_list';
static const String delta = blockComponentDelta;
static const String backgroundColor = blockComponentBackgroundColor;
static const String textDirection = blockComponentTextDirection;
} }
Node bulletedListNode({ Node bulletedListNode({
Delta? delta, Delta? delta,
String? textDirection,
Attributes? attributes, Attributes? attributes,
Iterable<Node>? children, Iterable<Node>? children,
}) { }) {
attributes ??= {'delta': (delta ?? Delta()).toJson()}; attributes ??= {BulletedListBlockKeys.delta: (delta ?? Delta()).toJson()};
return Node( return Node(
type: BulletedListBlockKeys.type, type: BulletedListBlockKeys.type,
attributes: { attributes: {
...attributes, ...attributes,
if (textDirection != null)
BulletedListBlockKeys.textDirection: textDirection,
}, },
children: children ?? [], children: children ?? [],
); );
@ -77,8 +86,9 @@ class _BulletedListBlockComponentWidgetState
SelectableMixin, SelectableMixin,
DefaultSelectableMixin, DefaultSelectableMixin,
BlockComponentConfigurable, BlockComponentConfigurable,
BackgroundColorMixin, BlockComponentBackgroundColorMixin,
NestedBlockComponentStatefulWidgetMixin { NestedBlockComponentStatefulWidgetMixin,
BlockComponentTextDirectionMixin {
@override @override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
@ -91,14 +101,23 @@ class _BulletedListBlockComponentWidgetState
@override @override
Node get node => widget.node; Node get node => widget.node;
String? lastStartText;
TextDirection? lastDirection;
@override @override
Widget buildComponent(BuildContext context) { Widget buildComponent(BuildContext context) {
final textDirection = calculateTextDirection(
defaultTextDirection: Directionality.maybeOf(context),
);
Widget child = Container( Widget child = Container(
color: backgroundColor, color: backgroundColor,
width: double.infinity,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
textDirection: textDirection,
children: [ children: [
widget.iconBuilder != null widget.iconBuilder != null
? widget.iconBuilder!(context, node) ? widget.iconBuilder!(context, node)
@ -119,6 +138,7 @@ class _BulletedListBlockComponentWidgetState
textSpan.updateTextStyle( textSpan.updateTextStyle(
placeholderTextStyle, placeholderTextStyle,
), ),
textDirection: textDirection,
), ),
), ),
], ],
@ -133,7 +153,13 @@ class _BulletedListBlockComponentWidgetState
); );
} }
return child; final indentPadding = configuration.indentPadding(node, textDirection);
return BlockComponentPadding(
node: node,
padding: padding,
indentPadding: indentPadding,
child: child,
);
} }
} }

View file

@ -68,7 +68,14 @@ class DividerBlockComponentWidget extends BlockComponentStatefulWidget {
} }
class _DividerBlockComponentWidgetState class _DividerBlockComponentWidgetState
extends State<DividerBlockComponentWidget> with SelectableMixin { extends State<DividerBlockComponentWidget>
with SelectableMixin, BlockComponentConfigurable {
@override
BlockComponentConfiguration get configuration => widget.configuration;
@override
Node get node => widget.node;
final dividerKey = GlobalKey(); final dividerKey = GlobalKey();
RenderBox get _renderBox => context.findRenderObject() as RenderBox; RenderBox get _renderBox => context.findRenderObject() as RenderBox;
@ -92,7 +99,11 @@ class _DividerBlockComponentWidgetState
); );
} }
return child; return BlockComponentPadding(
node: node,
padding: padding,
child: child,
);
} }
@override @override
@ -138,4 +149,9 @@ class _DividerBlockComponentWidgetState
@override @override
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset); Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
@override
TextDirection textDirection() {
return TextDirection.ltr;
}
} }

View file

@ -13,22 +13,27 @@ class HeadingBlockKeys {
/// The value is a int. /// The value is a int.
static const String level = 'level'; static const String level = 'level';
static const String delta = 'delta'; static const String delta = blockComponentDelta;
static const backgroundColor = blockComponentBackgroundColor; static const String backgroundColor = blockComponentBackgroundColor;
static const String textDirection = blockComponentTextDirection;
} }
Node headingNode({ Node headingNode({
required int level, required int level,
Delta? delta, Delta? delta,
String? textDirection,
Attributes? attributes, Attributes? attributes,
}) { }) {
assert(level >= 1 && level <= 6);
attributes ??= {'delta': (delta ?? Delta()).toJson()}; attributes ??= {'delta': (delta ?? Delta()).toJson()};
return Node( return Node(
type: HeadingBlockKeys.type, type: HeadingBlockKeys.type,
attributes: { attributes: {
HeadingBlockKeys.level: level,
...attributes, ...attributes,
HeadingBlockKeys.level: level,
if (textDirection != null) HeadingBlockKeys.textDirection: textDirection,
}, },
); );
} }
@ -92,7 +97,8 @@ class _HeadingBlockComponentWidgetState
SelectableMixin, SelectableMixin,
DefaultSelectableMixin, DefaultSelectableMixin,
BlockComponentConfigurable, BlockComponentConfigurable,
BackgroundColorMixin { BlockComponentBackgroundColorMixin,
BlockComponentTextDirectionMixin {
@override @override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
@ -111,8 +117,13 @@ class _HeadingBlockComponentWidgetState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textDirection = calculateTextDirection(
defaultTextDirection: Directionality.maybeOf(context),
);
Widget child = Container( Widget child = Container(
color: backgroundColor, color: backgroundColor,
width: double.infinity,
child: AppFlowyRichText( child: AppFlowyRichText(
key: forwardKey, key: forwardKey,
node: widget.node, node: widget.node,
@ -130,6 +141,7 @@ class _HeadingBlockComponentWidgetState
.updateTextStyle( .updateTextStyle(
widget.textStyleBuilder?.call(level) ?? defaultTextStyle(level), widget.textStyleBuilder?.call(level) ?? defaultTextStyle(level),
), ),
textDirection: textDirection,
), ),
); );
@ -141,7 +153,13 @@ class _HeadingBlockComponentWidgetState
); );
} }
return child; final indentPadding = configuration.indentPadding(node, textDirection);
return BlockComponentPadding(
node: node,
padding: padding,
indentPadding: indentPadding,
child: child,
);
} }
TextStyle? defaultTextStyle(int level) { TextStyle? defaultTextStyle(int level) {

View file

@ -113,7 +113,13 @@ class ImageBlockComponentWidget extends BlockComponentStatefulWidget {
} }
class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget> class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
with SelectableMixin { with SelectableMixin, BlockComponentConfigurable {
@override
BlockComponentConfiguration get configuration => widget.configuration;
@override
Node get node => widget.node;
final imageKey = GlobalKey(); final imageKey = GlobalKey();
RenderBox get _renderBox => context.findRenderObject() as RenderBox; RenderBox get _renderBox => context.findRenderObject() as RenderBox;
@ -185,7 +191,11 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
); );
} }
return child; return BlockComponentPadding(
node: node,
padding: padding,
child: child,
);
} }
@override @override

View file

@ -9,12 +9,19 @@ class NumberedListBlockKeys {
static const String type = 'numbered_list'; static const String type = 'numbered_list';
static const String number = 'number'; static const String number = 'number';
static const String delta = blockComponentDelta;
static const String backgroundColor = blockComponentBackgroundColor;
static const String textDirection = blockComponentTextDirection;
} }
Node numberedListNode({ Node numberedListNode({
Delta? delta, Delta? delta,
Attributes? attributes, Attributes? attributes,
int? number, int? number,
String? textDirection,
Iterable<Node>? children, Iterable<Node>? children,
}) { }) {
attributes ??= { attributes ??= {
@ -25,6 +32,8 @@ Node numberedListNode({
type: NumberedListBlockKeys.type, type: NumberedListBlockKeys.type,
attributes: { attributes: {
...attributes, ...attributes,
if (textDirection != null)
NumberedListBlockKeys.textDirection: textDirection,
}, },
children: children ?? [], children: children ?? [],
); );
@ -84,8 +93,9 @@ class _NumberedListBlockComponentWidgetState
SelectableMixin, SelectableMixin,
DefaultSelectableMixin, DefaultSelectableMixin,
BlockComponentConfigurable, BlockComponentConfigurable,
BackgroundColorMixin, BlockComponentBackgroundColorMixin,
NestedBlockComponentStatefulWidgetMixin { NestedBlockComponentStatefulWidgetMixin,
BlockComponentTextDirectionMixin {
@override @override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
@ -100,18 +110,25 @@ class _NumberedListBlockComponentWidgetState
@override @override
Widget buildComponent(BuildContext context) { Widget buildComponent(BuildContext context) {
final textDirection = calculateTextDirection(
defaultTextDirection: Directionality.maybeOf(context),
);
Widget child = Container( Widget child = Container(
color: backgroundColor, color: backgroundColor,
width: double.infinity,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
textDirection: textDirection,
children: [ children: [
widget.iconBuilder != null widget.iconBuilder != null
? widget.iconBuilder!(context, node) ? widget.iconBuilder!(context, node)
: _NumberedListIcon( : _NumberedListIcon(
node: node, node: node,
textStyle: textStyle, textStyle: textStyle,
direction: textDirection,
), ),
Flexible( Flexible(
child: AppFlowyRichText( child: AppFlowyRichText(
@ -126,6 +143,7 @@ class _NumberedListBlockComponentWidgetState
textSpan.updateTextStyle( textSpan.updateTextStyle(
placeholderTextStyle, placeholderTextStyle,
), ),
textDirection: textDirection,
), ),
), ),
], ],
@ -140,7 +158,13 @@ class _NumberedListBlockComponentWidgetState
); );
} }
return child; final indentPadding = configuration.indentPadding(node, textDirection);
return BlockComponentPadding(
node: node,
padding: padding,
indentPadding: indentPadding,
child: child,
);
} }
} }
@ -148,10 +172,12 @@ class _NumberedListIcon extends StatelessWidget {
const _NumberedListIcon({ const _NumberedListIcon({
required this.node, required this.node,
required this.textStyle, required this.textStyle,
required this.direction,
}); });
final Node node; final Node node;
final TextStyle textStyle; final TextStyle textStyle;
final TextDirection direction;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -166,6 +192,7 @@ class _NumberedListIcon extends StatelessWidget {
applyHeightToLastDescent: false, applyHeightToLastDescent: false,
), ),
TextSpan(text: '$level.', style: text.combine(textStyle)), TextSpan(text: '$level.', style: text.combine(textStyle)),
textDirection: direction,
), ),
); );
} }

View file

@ -7,10 +7,17 @@ class QuoteBlockKeys {
const QuoteBlockKeys._(); const QuoteBlockKeys._();
static const String type = 'quote'; static const String type = 'quote';
static const String delta = blockComponentDelta;
static const String backgroundColor = blockComponentBackgroundColor;
static const String textDirection = blockComponentTextDirection;
} }
Node quoteNode({ Node quoteNode({
Delta? delta, Delta? delta,
String? textDirection,
Attributes? attributes, Attributes? attributes,
Iterable<Node>? children, Iterable<Node>? children,
}) { }) {
@ -19,6 +26,7 @@ Node quoteNode({
type: QuoteBlockKeys.type, type: QuoteBlockKeys.type,
attributes: { attributes: {
...attributes, ...attributes,
if (textDirection != null) QuoteBlockKeys.textDirection: textDirection,
}, },
children: children ?? [], children: children ?? [],
); );
@ -77,7 +85,8 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
SelectableMixin, SelectableMixin,
DefaultSelectableMixin, DefaultSelectableMixin,
BlockComponentConfigurable, BlockComponentConfigurable,
BackgroundColorMixin { BlockComponentBackgroundColorMixin,
BlockComponentTextDirectionMixin {
@override @override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
@ -94,13 +103,19 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textDirection = calculateTextDirection(
defaultTextDirection: Directionality.maybeOf(context),
);
Widget child = Container( Widget child = Container(
color: backgroundColor, color: backgroundColor,
width: double.infinity,
child: IntrinsicHeight( child: IntrinsicHeight(
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
textDirection: textDirection,
children: [ children: [
widget.iconBuilder != null widget.iconBuilder != null
? widget.iconBuilder!(context, node) ? widget.iconBuilder!(context, node)
@ -118,6 +133,7 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
textSpan.updateTextStyle( textSpan.updateTextStyle(
placeholderTextStyle, placeholderTextStyle,
), ),
textDirection: textDirection,
), ),
), ),
], ],
@ -133,7 +149,13 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
); );
} }
return child; final indentPadding = configuration.indentPadding(node, textDirection);
return BlockComponentPadding(
node: node,
padding: padding,
indentPadding: indentPadding,
child: child,
);
} }
} }

View file

@ -33,6 +33,7 @@ class AppFlowyRichText extends StatefulWidget {
this.textSpanDecorator, this.textSpanDecorator,
this.placeholderText = ' ', this.placeholderText = ' ',
this.placeholderTextSpanDecorator, this.placeholderTextSpanDecorator,
this.textDirection = TextDirection.ltr,
this.textSpanDecoratorForCustomAttributes, this.textSpanDecoratorForCustomAttributes,
required this.node, required this.node,
required this.editorState, required this.editorState,
@ -69,6 +70,7 @@ class AppFlowyRichText extends StatefulWidget {
/// You can use this to customize the text span for custom attributes /// You can use this to customize the text span for custom attributes
/// or override the existing one. /// or override the existing one.
final TextSpanDecoratorForAttribute? textSpanDecoratorForCustomAttributes; final TextSpanDecoratorForAttribute? textSpanDecoratorForCustomAttributes;
final TextDirection textDirection;
@override @override
State<AppFlowyRichText> createState() => _AppFlowyRichTextState(); State<AppFlowyRichText> createState() => _AppFlowyRichTextState();
@ -128,6 +130,14 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
Rect.zero, Rect.zero,
) ?? ) ??
Offset.zero; Offset.zero;
if (textDirection() == TextDirection.rtl) {
if (widget.placeholderText.trim().isNotEmpty) {
cursorOffset = cursorOffset.translate(
_placeholderRenderParagraph?.size.width ?? 0,
0,
);
}
}
} }
if (widget.cursorHeight != null && cursorHeight != null) { if (widget.cursorHeight != null && cursorHeight != null) {
cursorOffset = Offset( cursorOffset = Offset(
@ -208,6 +218,11 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
return _renderParagraph.localToGlobal(offset); return _renderParagraph.localToGlobal(offset);
} }
@override
TextDirection textDirection() {
return widget.textDirection;
}
Widget _buildPlaceholderText(BuildContext context) { Widget _buildPlaceholderText(BuildContext context) {
final textSpan = getPlaceholderTextSpan(); final textSpan = getPlaceholderTextSpan();
return RichText( return RichText(
@ -219,6 +234,7 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
text: widget.placeholderTextSpanDecorator != null text: widget.placeholderTextSpanDecorator != null
? widget.placeholderTextSpanDecorator!(textSpan) ? widget.placeholderTextSpanDecorator!(textSpan)
: textSpan, : textSpan,
textDirection: textDirection(),
); );
} }
@ -233,6 +249,7 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
text: widget.textSpanDecorator != null text: widget.textSpanDecorator != null
? widget.textSpanDecorator!(textSpan) ? widget.textSpanDecorator!(textSpan)
: textSpan, : textSpan,
textDirection: textDirection(),
); );
} }
@ -361,7 +378,7 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
} }
} }
extension FlowyRichTextAttributes on Attributes { extension AppFlowyRichTextAttributes on Attributes {
bool get bold => this[AppFlowyRichTextKeys.bold] == true; bool get bold => this[AppFlowyRichTextKeys.bold] == true;
bool get italic => this[AppFlowyRichTextKeys.italic] == true; bool get italic => this[AppFlowyRichTextKeys.italic] == true;

View file

@ -45,4 +45,6 @@ mixin DefaultSelectableMixin {
Position start() => forward.start(); Position start() => forward.start();
Position end() => forward.end(); Position end() => forward.end();
TextDirection textDirection() => forward.textDirection();
} }

View file

@ -6,14 +6,17 @@ class ParagraphBlockKeys {
static const String type = 'paragraph'; static const String type = 'paragraph';
static const String delta = 'delta'; static const String delta = blockComponentDelta;
static const String backgroundColor = blockComponentBackgroundColor; static const String backgroundColor = blockComponentBackgroundColor;
static const String textDirection = blockComponentTextDirection;
} }
Node paragraphNode({ Node paragraphNode({
String? text, String? text,
Delta? delta, Delta? delta,
String? textDirection,
Attributes? attributes, Attributes? attributes,
Iterable<Node> children = const [], Iterable<Node> children = const [],
}) { }) {
@ -24,6 +27,8 @@ Node paragraphNode({
type: ParagraphBlockKeys.type, type: ParagraphBlockKeys.type,
attributes: { attributes: {
...attributes, ...attributes,
if (textDirection != null)
ParagraphBlockKeys.textDirection: textDirection,
}, },
children: children, children: children,
); );
@ -77,8 +82,9 @@ class _TextBlockComponentWidgetState extends State<TextBlockComponentWidget>
SelectableMixin, SelectableMixin,
DefaultSelectableMixin, DefaultSelectableMixin,
BlockComponentConfigurable, BlockComponentConfigurable,
BackgroundColorMixin, BlockComponentBackgroundColorMixin,
NestedBlockComponentStatefulWidgetMixin { NestedBlockComponentStatefulWidgetMixin,
BlockComponentTextDirectionMixin {
@override @override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
@ -91,21 +97,39 @@ class _TextBlockComponentWidgetState extends State<TextBlockComponentWidget>
@override @override
Node get node => widget.node; Node get node => widget.node;
String? lastStartText;
TextDirection? lastDirection;
@override @override
Widget buildComponent(BuildContext context) { Widget buildComponent(BuildContext context) {
final textDirection = calculateTextDirection(
defaultTextDirection: Directionality.maybeOf(context),
);
Widget child = Container( Widget child = Container(
color: backgroundColor, color: backgroundColor,
child: AppFlowyRichText( width: double.infinity,
key: forwardKey, child: Column(
node: widget.node, crossAxisAlignment: CrossAxisAlignment.start,
editorState: editorState, mainAxisAlignment: MainAxisAlignment.start,
placeholderText: placeholderText, mainAxisSize: MainAxisSize.min,
textSpanDecorator: (textSpan) => textSpan.updateTextStyle( textDirection: textDirection,
textStyle, children: [
), AppFlowyRichText(
placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle( key: forwardKey,
placeholderTextStyle, node: widget.node,
), editorState: editorState,
placeholderText: placeholderText,
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
textStyle,
),
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(
placeholderTextStyle,
),
textDirection: textDirection,
),
],
), ),
); );
if (showActions) { if (showActions) {
@ -115,6 +139,13 @@ class _TextBlockComponentWidgetState extends State<TextBlockComponentWidget>
child: child, child: child,
); );
} }
return child;
final indentPadding = configuration.indentPadding(node, textDirection);
return BlockComponentPadding(
node: node,
padding: padding,
indentPadding: indentPadding,
child: child,
);
} }
} }

Some files were not shown because too many files have changed in this diff Show more