feat: support RTL and auto direction (#191)
This commit is contained in:
parent
3d31f1c8c2
commit
f8da435ad5
76 changed files with 1148 additions and 473 deletions
|
|
@ -246,4 +246,24 @@ Widget build(BuildContext context) {
|
|||
}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
## 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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ class Editor extends StatelessWidget {
|
|||
numberedListItem,
|
||||
linkItem,
|
||||
buildTextColorItem(),
|
||||
buildHighlightColorItem()
|
||||
buildHighlightColorItem(),
|
||||
...textDirectionItems
|
||||
],
|
||||
editorState: editorState,
|
||||
scrollController: scrollController,
|
||||
|
|
|
|||
|
|
@ -199,7 +199,8 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||
.toList(growable: false);
|
||||
}
|
||||
if (attributes.isNotEmpty) {
|
||||
map['data'] = attributes;
|
||||
// filter the null value
|
||||
map['data'] = attributes..removeWhere((_, value) => value == null);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const blockComponentBackgroundColor = 'bgColor';
|
||||
|
||||
mixin BackgroundColorMixin {
|
||||
mixin BlockComponentBackgroundColorMixin {
|
||||
Node get node;
|
||||
|
||||
Color get backgroundColor {
|
||||
|
|
|
|||
|
|
@ -5,14 +5,25 @@ import 'package:flutter/material.dart';
|
|||
class BlockComponentConfiguration {
|
||||
const BlockComponentConfiguration({
|
||||
this.padding = _padding,
|
||||
this.indentPadding = _indentPadding,
|
||||
this.placeholderText = _placeholderText,
|
||||
this.textStyle = _textStyle,
|
||||
this.placeholderTextStyle = _placeholderTextStyle,
|
||||
});
|
||||
|
||||
/// The padding of a block component.
|
||||
///
|
||||
/// It works only for the block component itself, not for the children.
|
||||
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.
|
||||
final TextStyle Function(Node node) textStyle;
|
||||
|
||||
|
|
@ -57,6 +68,15 @@ EdgeInsets _padding(Node node) {
|
|||
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) {
|
||||
return const TextStyle();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -39,14 +39,16 @@ CommandShortcutEventHandler _convertToParagraphCommandHandler = (editorState) {
|
|||
if (index >= 0) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final textDirection = node.attributes[blockComponentTextDirection];
|
||||
final transaction = editorState.transaction;
|
||||
transaction
|
||||
..insertNode(
|
||||
node.path,
|
||||
paragraphNode(
|
||||
attributes: {
|
||||
'delta': delta.toJson(),
|
||||
ParagraphBlockKeys.delta: delta.toJson(),
|
||||
},
|
||||
textDirection: textDirection,
|
||||
children: node.children,
|
||||
),
|
||||
deepCopy: true,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,10 @@ import 'package:flutter/material.dart';
|
|||
class NestedListWidget extends StatelessWidget {
|
||||
const NestedListWidget({
|
||||
super.key,
|
||||
this.padding = const EdgeInsets.only(left: 30.0),
|
||||
required this.child,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
final EdgeInsets padding;
|
||||
final Widget child;
|
||||
final List<Widget> children;
|
||||
|
||||
|
|
@ -20,14 +18,11 @@ class NestedListWidget extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
child,
|
||||
Padding(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
24
lib/src/editor/block_component/base_component_keys.dart
Normal file
24
lib/src/editor/block_component/base_component_keys.dart
Normal 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';
|
||||
|
|
@ -37,6 +37,7 @@ export 'base_component/convert_to_paragraph_command.dart';
|
|||
export 'base_component/insert_newline_in_type_command.dart';
|
||||
export 'base_component/indent_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/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/text_style_configuration.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/widget/nested_list_widget.dart';
|
||||
export 'base_component/block_component_action_wrapper.dart';
|
||||
|
||||
export 'standard_block_components.dart';
|
||||
export 'base_component_keys.dart';
|
||||
|
|
|
|||
|
|
@ -6,18 +6,27 @@ class BulletedListBlockKeys {
|
|||
BulletedListBlockKeys._();
|
||||
|
||||
static const String type = 'bulleted_list';
|
||||
|
||||
static const String delta = blockComponentDelta;
|
||||
|
||||
static const String backgroundColor = blockComponentBackgroundColor;
|
||||
|
||||
static const String textDirection = blockComponentTextDirection;
|
||||
}
|
||||
|
||||
Node bulletedListNode({
|
||||
Delta? delta,
|
||||
String? textDirection,
|
||||
Attributes? attributes,
|
||||
Iterable<Node>? children,
|
||||
}) {
|
||||
attributes ??= {'delta': (delta ?? Delta()).toJson()};
|
||||
attributes ??= {BulletedListBlockKeys.delta: (delta ?? Delta()).toJson()};
|
||||
return Node(
|
||||
type: BulletedListBlockKeys.type,
|
||||
attributes: {
|
||||
...attributes,
|
||||
if (textDirection != null)
|
||||
BulletedListBlockKeys.textDirection: textDirection,
|
||||
},
|
||||
children: children ?? [],
|
||||
);
|
||||
|
|
@ -77,8 +86,9 @@ class _BulletedListBlockComponentWidgetState
|
|||
SelectableMixin,
|
||||
DefaultSelectableMixin,
|
||||
BlockComponentConfigurable,
|
||||
BackgroundColorMixin,
|
||||
NestedBlockComponentStatefulWidgetMixin {
|
||||
BlockComponentBackgroundColorMixin,
|
||||
NestedBlockComponentStatefulWidgetMixin,
|
||||
BlockComponentTextDirectionMixin {
|
||||
@override
|
||||
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
|
||||
|
||||
|
|
@ -91,14 +101,23 @@ class _BulletedListBlockComponentWidgetState
|
|||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
String? lastStartText;
|
||||
TextDirection? lastDirection;
|
||||
|
||||
@override
|
||||
Widget buildComponent(BuildContext context) {
|
||||
final textDirection = calculateTextDirection(
|
||||
defaultTextDirection: Directionality.maybeOf(context),
|
||||
);
|
||||
|
||||
Widget child = Container(
|
||||
color: backgroundColor,
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
textDirection: textDirection,
|
||||
children: [
|
||||
widget.iconBuilder != null
|
||||
? widget.iconBuilder!(context, node)
|
||||
|
|
@ -119,6 +138,7 @@ class _BulletedListBlockComponentWidgetState
|
|||
textSpan.updateTextStyle(
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,14 @@ class DividerBlockComponentWidget extends BlockComponentStatefulWidget {
|
|||
}
|
||||
|
||||
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();
|
||||
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
|
||||
|
||||
|
|
@ -92,7 +99,11 @@ class _DividerBlockComponentWidgetState
|
|||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
return BlockComponentPadding(
|
||||
node: node,
|
||||
padding: padding,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -138,4 +149,9 @@ class _DividerBlockComponentWidgetState
|
|||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
|
||||
|
||||
@override
|
||||
TextDirection textDirection() {
|
||||
return TextDirection.ltr;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,22 +13,27 @@ class HeadingBlockKeys {
|
|||
/// The value is a int.
|
||||
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({
|
||||
required int level,
|
||||
Delta? delta,
|
||||
String? textDirection,
|
||||
Attributes? attributes,
|
||||
}) {
|
||||
assert(level >= 1 && level <= 6);
|
||||
attributes ??= {'delta': (delta ?? Delta()).toJson()};
|
||||
return Node(
|
||||
type: HeadingBlockKeys.type,
|
||||
attributes: {
|
||||
HeadingBlockKeys.level: level,
|
||||
...attributes,
|
||||
HeadingBlockKeys.level: level,
|
||||
if (textDirection != null) HeadingBlockKeys.textDirection: textDirection,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -92,7 +97,8 @@ class _HeadingBlockComponentWidgetState
|
|||
SelectableMixin,
|
||||
DefaultSelectableMixin,
|
||||
BlockComponentConfigurable,
|
||||
BackgroundColorMixin {
|
||||
BlockComponentBackgroundColorMixin,
|
||||
BlockComponentTextDirectionMixin {
|
||||
@override
|
||||
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
|
||||
|
||||
|
|
@ -111,8 +117,13 @@ class _HeadingBlockComponentWidgetState
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textDirection = calculateTextDirection(
|
||||
defaultTextDirection: Directionality.maybeOf(context),
|
||||
);
|
||||
|
||||
Widget child = Container(
|
||||
color: backgroundColor,
|
||||
width: double.infinity,
|
||||
child: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
node: widget.node,
|
||||
|
|
@ -130,6 +141,7 @@ class _HeadingBlockComponentWidgetState
|
|||
.updateTextStyle(
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -113,7 +113,13 @@ class ImageBlockComponentWidget extends BlockComponentStatefulWidget {
|
|||
}
|
||||
|
||||
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();
|
||||
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
|
||||
|
|
|
|||
|
|
@ -9,12 +9,19 @@ class NumberedListBlockKeys {
|
|||
static const String type = 'numbered_list';
|
||||
|
||||
static const String number = 'number';
|
||||
|
||||
static const String delta = blockComponentDelta;
|
||||
|
||||
static const String backgroundColor = blockComponentBackgroundColor;
|
||||
|
||||
static const String textDirection = blockComponentTextDirection;
|
||||
}
|
||||
|
||||
Node numberedListNode({
|
||||
Delta? delta,
|
||||
Attributes? attributes,
|
||||
int? number,
|
||||
String? textDirection,
|
||||
Iterable<Node>? children,
|
||||
}) {
|
||||
attributes ??= {
|
||||
|
|
@ -25,6 +32,8 @@ Node numberedListNode({
|
|||
type: NumberedListBlockKeys.type,
|
||||
attributes: {
|
||||
...attributes,
|
||||
if (textDirection != null)
|
||||
NumberedListBlockKeys.textDirection: textDirection,
|
||||
},
|
||||
children: children ?? [],
|
||||
);
|
||||
|
|
@ -84,8 +93,9 @@ class _NumberedListBlockComponentWidgetState
|
|||
SelectableMixin,
|
||||
DefaultSelectableMixin,
|
||||
BlockComponentConfigurable,
|
||||
BackgroundColorMixin,
|
||||
NestedBlockComponentStatefulWidgetMixin {
|
||||
BlockComponentBackgroundColorMixin,
|
||||
NestedBlockComponentStatefulWidgetMixin,
|
||||
BlockComponentTextDirectionMixin {
|
||||
@override
|
||||
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
|
||||
|
||||
|
|
@ -100,18 +110,25 @@ class _NumberedListBlockComponentWidgetState
|
|||
|
||||
@override
|
||||
Widget buildComponent(BuildContext context) {
|
||||
final textDirection = calculateTextDirection(
|
||||
defaultTextDirection: Directionality.maybeOf(context),
|
||||
);
|
||||
|
||||
Widget child = Container(
|
||||
color: backgroundColor,
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
textDirection: textDirection,
|
||||
children: [
|
||||
widget.iconBuilder != null
|
||||
? widget.iconBuilder!(context, node)
|
||||
: _NumberedListIcon(
|
||||
node: node,
|
||||
textStyle: textStyle,
|
||||
direction: textDirection,
|
||||
),
|
||||
Flexible(
|
||||
child: AppFlowyRichText(
|
||||
|
|
@ -126,6 +143,7 @@ class _NumberedListBlockComponentWidgetState
|
|||
textSpan.updateTextStyle(
|
||||
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({
|
||||
required this.node,
|
||||
required this.textStyle,
|
||||
required this.direction,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final TextStyle textStyle;
|
||||
final TextDirection direction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -166,6 +192,7 @@ class _NumberedListIcon extends StatelessWidget {
|
|||
applyHeightToLastDescent: false,
|
||||
),
|
||||
TextSpan(text: '$level.', style: text.combine(textStyle)),
|
||||
textDirection: direction,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,17 @@ class QuoteBlockKeys {
|
|||
const QuoteBlockKeys._();
|
||||
|
||||
static const String type = 'quote';
|
||||
|
||||
static const String delta = blockComponentDelta;
|
||||
|
||||
static const String backgroundColor = blockComponentBackgroundColor;
|
||||
|
||||
static const String textDirection = blockComponentTextDirection;
|
||||
}
|
||||
|
||||
Node quoteNode({
|
||||
Delta? delta,
|
||||
String? textDirection,
|
||||
Attributes? attributes,
|
||||
Iterable<Node>? children,
|
||||
}) {
|
||||
|
|
@ -19,6 +26,7 @@ Node quoteNode({
|
|||
type: QuoteBlockKeys.type,
|
||||
attributes: {
|
||||
...attributes,
|
||||
if (textDirection != null) QuoteBlockKeys.textDirection: textDirection,
|
||||
},
|
||||
children: children ?? [],
|
||||
);
|
||||
|
|
@ -77,7 +85,8 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
|||
SelectableMixin,
|
||||
DefaultSelectableMixin,
|
||||
BlockComponentConfigurable,
|
||||
BackgroundColorMixin {
|
||||
BlockComponentBackgroundColorMixin,
|
||||
BlockComponentTextDirectionMixin {
|
||||
@override
|
||||
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
|
||||
|
||||
|
|
@ -94,13 +103,19 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textDirection = calculateTextDirection(
|
||||
defaultTextDirection: Directionality.maybeOf(context),
|
||||
);
|
||||
|
||||
Widget child = Container(
|
||||
color: backgroundColor,
|
||||
width: double.infinity,
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
textDirection: textDirection,
|
||||
children: [
|
||||
widget.iconBuilder != null
|
||||
? widget.iconBuilder!(context, node)
|
||||
|
|
@ -118,6 +133,7 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
|||
textSpan.updateTextStyle(
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class AppFlowyRichText extends StatefulWidget {
|
|||
this.textSpanDecorator,
|
||||
this.placeholderText = ' ',
|
||||
this.placeholderTextSpanDecorator,
|
||||
this.textDirection = TextDirection.ltr,
|
||||
this.textSpanDecoratorForCustomAttributes,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
|
|
@ -69,6 +70,7 @@ class AppFlowyRichText extends StatefulWidget {
|
|||
/// You can use this to customize the text span for custom attributes
|
||||
/// or override the existing one.
|
||||
final TextSpanDecoratorForAttribute? textSpanDecoratorForCustomAttributes;
|
||||
final TextDirection textDirection;
|
||||
|
||||
@override
|
||||
State<AppFlowyRichText> createState() => _AppFlowyRichTextState();
|
||||
|
|
@ -128,6 +130,14 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
Rect.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) {
|
||||
cursorOffset = Offset(
|
||||
|
|
@ -208,6 +218,11 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
return _renderParagraph.localToGlobal(offset);
|
||||
}
|
||||
|
||||
@override
|
||||
TextDirection textDirection() {
|
||||
return widget.textDirection;
|
||||
}
|
||||
|
||||
Widget _buildPlaceholderText(BuildContext context) {
|
||||
final textSpan = getPlaceholderTextSpan();
|
||||
return RichText(
|
||||
|
|
@ -219,6 +234,7 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
text: widget.placeholderTextSpanDecorator != null
|
||||
? widget.placeholderTextSpanDecorator!(textSpan)
|
||||
: textSpan,
|
||||
textDirection: textDirection(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -233,6 +249,7 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
text: widget.textSpanDecorator != null
|
||||
? widget.textSpanDecorator!(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 italic => this[AppFlowyRichTextKeys.italic] == true;
|
||||
|
|
|
|||
|
|
@ -45,4 +45,6 @@ mixin DefaultSelectableMixin {
|
|||
Position start() => forward.start();
|
||||
|
||||
Position end() => forward.end();
|
||||
|
||||
TextDirection textDirection() => forward.textDirection();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,17 @@ class ParagraphBlockKeys {
|
|||
|
||||
static const String type = 'paragraph';
|
||||
|
||||
static const String delta = 'delta';
|
||||
static const String delta = blockComponentDelta;
|
||||
|
||||
static const String backgroundColor = blockComponentBackgroundColor;
|
||||
|
||||
static const String textDirection = blockComponentTextDirection;
|
||||
}
|
||||
|
||||
Node paragraphNode({
|
||||
String? text,
|
||||
Delta? delta,
|
||||
String? textDirection,
|
||||
Attributes? attributes,
|
||||
Iterable<Node> children = const [],
|
||||
}) {
|
||||
|
|
@ -24,6 +27,8 @@ Node paragraphNode({
|
|||
type: ParagraphBlockKeys.type,
|
||||
attributes: {
|
||||
...attributes,
|
||||
if (textDirection != null)
|
||||
ParagraphBlockKeys.textDirection: textDirection,
|
||||
},
|
||||
children: children,
|
||||
);
|
||||
|
|
@ -77,8 +82,9 @@ class _TextBlockComponentWidgetState extends State<TextBlockComponentWidget>
|
|||
SelectableMixin,
|
||||
DefaultSelectableMixin,
|
||||
BlockComponentConfigurable,
|
||||
BackgroundColorMixin,
|
||||
NestedBlockComponentStatefulWidgetMixin {
|
||||
BlockComponentBackgroundColorMixin,
|
||||
NestedBlockComponentStatefulWidgetMixin,
|
||||
BlockComponentTextDirectionMixin {
|
||||
@override
|
||||
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
|
||||
|
||||
|
|
@ -91,21 +97,39 @@ class _TextBlockComponentWidgetState extends State<TextBlockComponentWidget>
|
|||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
String? lastStartText;
|
||||
TextDirection? lastDirection;
|
||||
|
||||
@override
|
||||
Widget buildComponent(BuildContext context) {
|
||||
final textDirection = calculateTextDirection(
|
||||
defaultTextDirection: Directionality.maybeOf(context),
|
||||
);
|
||||
|
||||
Widget child = Container(
|
||||
color: backgroundColor,
|
||||
child: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
placeholderText: placeholderText,
|
||||
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
textStyle,
|
||||
),
|
||||
placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
placeholderTextStyle,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
textDirection: textDirection,
|
||||
children: [
|
||||
AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
placeholderText: placeholderText,
|
||||
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
textStyle,
|
||||
),
|
||||
placeholderTextSpanDecorator: (textSpan) =>
|
||||
textSpan.updateTextStyle(
|
||||
placeholderTextStyle,
|
||||
),
|
||||
textDirection: textDirection,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (showActions) {
|
||||
|
|
@ -115,6 +139,13 @@ class _TextBlockComponentWidgetState extends State<TextBlockComponentWidget>
|
|||
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
Loading…
Reference in a new issue