diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/copy.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/copy.svg
new file mode 100644
index 0000000000..101cf34205
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/copy.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart
index e72739e246..4bd1cb1972 100644
--- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart
@@ -45,10 +45,8 @@ class _MyHomePageState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
- body: Container(
- alignment: Alignment.topCenter,
- child: _buildEditor(context),
- ),
+ extendBodyBehindAppBar: true,
+ body: _buildEditor(context),
floatingActionButton: _buildExpandableFab(),
);
}
@@ -92,10 +90,11 @@ class _MyHomePageState extends State {
..handler = (message) {
debugPrint(message);
};
- return Container(
- padding: const EdgeInsets.all(20),
+ return SizedBox(
+ width: MediaQuery.of(context).size.width,
child: AppFlowyEditor(
editorState: _editorState,
+ editorStyle: const EditorStyle.defaultStyle(),
),
);
} else {
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart
index 14826ff713..12b3a29252 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart
@@ -2,6 +2,7 @@
library appflowy_editor;
export 'src/infra/log.dart';
+export 'src/render/style/editor_style.dart';
export 'src/document/node.dart';
export 'src/document/path.dart';
export 'src/document/position.dart';
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart
index a17b2fbf98..a4a9869df5 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart
@@ -62,10 +62,17 @@ class StateTree {
}
return false;
}
- for (var i = 0; i < nodes.length; i++) {
- final node = nodes[i];
- insertedNode!.insertAfter(node);
- insertedNode = node;
+ if (path.last <= 0) {
+ for (var i = 0; i < nodes.length; i++) {
+ final node = nodes[i];
+ insertedNode.insertBefore(node);
+ }
+ } else {
+ for (var i = 0; i < nodes.length; i++) {
+ final node = nodes[i];
+ insertedNode!.insertAfter(node);
+ insertedNode = node;
+ }
}
return true;
}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart
index 396b428baf..2750af07a6 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:appflowy_editor/src/infra/log.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
+import 'package:appflowy_editor/src/render/style/editor_style.dart';
import 'package:appflowy_editor/src/service/service.dart';
import 'package:flutter/material.dart';
@@ -58,6 +59,9 @@ class EditorState {
/// Stores the selection menu items.
List selectionMenuItems = [];
+ /// Stores the editor style.
+ EditorStyle editorStyle = const EditorStyle.defaultStyle();
+
final UndoManager undoManager = UndoManager();
Selection? _cursorSelection;
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/url_launcher_extension.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/url_launcher_extension.dart
new file mode 100644
index 0000000000..1c0ea30c82
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/url_launcher_extension.dart
@@ -0,0 +1,14 @@
+import 'package:url_launcher/url_launcher_string.dart';
+
+Future safeLaunchUrl(String? href) async {
+ if (href == null) {
+ return Future.value(false);
+ }
+ final uri = Uri.parse(href);
+ // url_launcher cannot open a link without scheme.
+ final newHref = (uri.scheme.isNotEmpty ? href : 'http://$href').trim();
+ if (await canLaunchUrlString(newHref)) {
+ await launchUrlString(newHref);
+ }
+ return Future.value(true);
+}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart
index 12c13bf2e5..1390b23918 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart
@@ -115,17 +115,18 @@ class TransactionBuilder {
/// Inserts content at a specified index.
/// Optionally, you may specify formatting attributes that are applied to the inserted string.
/// By default, the formatting attributes before the insert position will be used.
- insertText(TextNode node, int index, String content,
- {Attributes? attributes, Attributes? removedAttributes}) {
+ insertText(
+ TextNode node,
+ int index,
+ String content, {
+ Attributes? attributes,
+ }) {
var newAttributes = attributes;
if (index != 0 && attributes == null) {
newAttributes =
node.delta.slice(max(index - 1, 0), index).first.attributes;
if (newAttributes != null) {
newAttributes = Attributes.from(newAttributes);
- if (removedAttributes != null) {
- newAttributes.addAll(removedAttributes);
- }
}
}
textEdit(
@@ -138,7 +139,8 @@ class TransactionBuilder {
),
);
afterSelection = Selection.collapsed(
- Position(path: node.path, offset: index + content.length));
+ Position(path: node.path, offset: index + content.length),
+ );
}
/// Assigns formatting attributes to a range of text.
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart
index e71dc7c79b..4167ca1b38 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart
@@ -33,7 +33,7 @@ class EditorNodeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
- crossAxisAlignment: CrossAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
children: node.children
.map(
(child) =>
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart
index 796e96c250..ad3cf19d53 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart
@@ -17,6 +17,7 @@ class ImageNodeBuilder extends NodeWidgetBuilder {
}
return ImageNodeWidget(
key: context.node.key,
+ node: context.node,
src: src,
width: width,
alignment: _textToAlignment(align),
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart
index 2cc0916b66..a65df11541 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart
@@ -1,10 +1,15 @@
+import 'package:appflowy_editor/src/extensions/object_extensions.dart';
+import 'package:appflowy_editor/src/document/node.dart';
+import 'package:appflowy_editor/src/document/position.dart';
+import 'package:appflowy_editor/src/document/selection.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
+import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:flutter/material.dart';
class ImageNodeWidget extends StatefulWidget {
const ImageNodeWidget({
Key? key,
+ required this.node,
required this.src,
this.width,
required this.alignment,
@@ -14,6 +19,7 @@ class ImageNodeWidget extends StatefulWidget {
required this.onResize,
}) : super(key: key);
+ final Node node;
final String src;
final double? width;
final Alignment alignment;
@@ -26,7 +32,9 @@ class ImageNodeWidget extends StatefulWidget {
State createState() => _ImageNodeWidgetState();
}
-class _ImageNodeWidgetState extends State {
+class _ImageNodeWidgetState extends State with Selectable {
+ final _imageKey = GlobalKey();
+
double? _imageWidth;
double _initial = 0;
double _distance = 0;
@@ -42,7 +50,11 @@ class _ImageNodeWidgetState extends State {
_imageWidth = widget.width;
_imageStreamListener = ImageStreamListener(
(image, _) {
- _imageWidth = image.image.width.toDouble();
+ _imageWidth = _imageKey.currentContext
+ ?.findRenderObject()
+ ?.unwrapOrNull()
+ ?.size
+ .width;
},
);
}
@@ -56,14 +68,54 @@ class _ImageNodeWidgetState extends State {
@override
Widget build(BuildContext context) {
// only support network image.
-
return Container(
- width: defaultMaxTextNodeWidth,
+ key: _imageKey,
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: _buildNetworkImage(context),
);
}
+ @override
+ Position start() {
+ return Position(path: widget.node.path, offset: 0);
+ }
+
+ @override
+ Position end() {
+ return Position(path: widget.node.path, offset: 1);
+ }
+
+ @override
+ Position getPositionInOffset(Offset start) {
+ return end();
+ }
+
+ @override
+ Rect? getCursorRectInPosition(Position position) {
+ return null;
+ }
+
+ @override
+ List getRectsInSelection(Selection selection) {
+ final renderBox = context.findRenderObject() as RenderBox;
+ return [Offset.zero & renderBox.size];
+ }
+
+ @override
+ Selection getSelectionInRange(Offset start, Offset end) {
+ if (start <= end) {
+ return Selection(start: this.start(), end: this.end());
+ } else {
+ return Selection(start: this.end(), end: this.start());
+ }
+ }
+
+ @override
+ Offset localToGlobal(Offset offset) {
+ final renderBox = context.findRenderObject() as RenderBox;
+ return renderBox.localToGlobal(offset);
+ }
+
Widget _buildNetworkImage(BuildContext context) {
return Align(
alignment: widget.alignment,
@@ -87,7 +139,7 @@ class _ImageNodeWidgetState extends State {
loadingBuilder: (context, child, loadingProgress) =>
loadingProgress == null ? child : _buildLoading(context),
errorBuilder: (context, error, stackTrace) {
- _imageWidth ??= defaultMaxTextNodeWidth;
+ // _imageWidth ??= defaultMaxTextNodeWidth;
return _buildError(context);
},
);
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart
index a33adf3b8c..13396a33c4 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart
@@ -6,14 +6,18 @@ class LinkMenu extends StatefulWidget {
Key? key,
this.linkText,
required this.onSubmitted,
+ required this.onOpenLink,
required this.onCopyLink,
required this.onRemoveLink,
+ required this.onFocusChange,
}) : super(key: key);
final String? linkText;
final void Function(String text) onSubmitted;
+ final VoidCallback onOpenLink;
final VoidCallback onCopyLink;
final VoidCallback onRemoveLink;
+ final void Function(bool value) onFocusChange;
@override
State createState() => _LinkMenuState();
@@ -26,15 +30,14 @@ class _LinkMenuState extends State {
@override
void initState() {
super.initState();
-
_textEditingController.text = widget.linkText ?? '';
- _focusNode.requestFocus();
+ _focusNode.addListener(_onFocusChange);
}
@override
void dispose() {
- _focusNode.dispose();
-
+ _textEditingController.dispose();
+ _focusNode.removeListener(_onFocusChange);
super.dispose();
}
@@ -67,6 +70,12 @@ class _LinkMenuState extends State {
if (widget.linkText != null) ...[
_buildIconButton(
iconName: 'link',
+ text: 'Open link',
+ onPressed: widget.onOpenLink,
+ ),
+ _buildIconButton(
+ iconName: 'copy',
+ color: Colors.black,
text: 'Copy link',
onPressed: widget.onCopyLink,
),
@@ -126,11 +135,15 @@ class _LinkMenuState extends State {
Widget _buildIconButton({
required String iconName,
+ Color? color,
required String text,
required VoidCallback onPressed,
}) {
return TextButton.icon(
- icon: FlowySvg(name: iconName),
+ icon: FlowySvg(
+ name: iconName,
+ color: color,
+ ),
style: TextButton.styleFrom(
minimumSize: const Size.fromHeight(40),
padding: EdgeInsets.zero,
@@ -148,4 +161,8 @@ class _LinkMenuState extends State {
onPressed: onPressed,
);
}
+
+ void _onFocusChange() {
+ widget.onFocusChange(_focusNode.hasFocus);
+ }
}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart
index 7d69ff459f..7f0f0363f8 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart
@@ -56,30 +56,27 @@ class _BulletedListTextNodeWidgetState extends State
@override
Widget build(BuildContext context) {
- return SizedBox(
- width: defaultMaxTextNodeWidth,
- child: Padding(
- padding: EdgeInsets.only(bottom: defaultLinePadding),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- FlowySvg(
- key: iconKey,
- width: _iconWidth,
- height: _iconWidth,
- padding: EdgeInsets.only(right: _iconRightPadding),
- name: 'point',
+ return Padding(
+ padding: EdgeInsets.only(bottom: defaultLinePadding),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ FlowySvg(
+ key: iconKey,
+ width: _iconWidth,
+ height: _iconWidth,
+ padding: EdgeInsets.only(right: _iconRightPadding),
+ name: 'point',
+ ),
+ Flexible(
+ child: FlowyRichText(
+ key: _richTextKey,
+ placeholderText: 'List',
+ textNode: widget.textNode,
+ editorState: widget.editorState,
),
- Expanded(
- child: FlowyRichText(
- key: _richTextKey,
- placeholderText: 'List',
- textNode: widget.textNode,
- editorState: widget.editorState,
- ),
- ),
- ],
- ),
+ )
+ ],
),
);
}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart
index 0255a84049..ed6748a43e 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart
@@ -63,41 +63,38 @@ class _CheckboxNodeWidgetState extends State
Widget _buildWithSingle(BuildContext context) {
final check = widget.textNode.attributes.check;
- return SizedBox(
- width: defaultMaxTextNodeWidth,
- child: Padding(
- padding: EdgeInsets.only(bottom: defaultLinePadding),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- GestureDetector(
- key: iconKey,
- child: FlowySvg(
- width: _iconWidth,
- height: _iconWidth,
- padding: EdgeInsets.only(right: _iconRightPadding),
- name: check ? 'check' : 'uncheck',
- ),
- onTap: () {
- TransactionBuilder(widget.editorState)
- ..updateNode(widget.textNode, {
- StyleKey.checkbox: !check,
- })
- ..commit();
- },
+ return Padding(
+ padding: EdgeInsets.only(bottom: defaultLinePadding),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ GestureDetector(
+ key: iconKey,
+ child: FlowySvg(
+ width: _iconWidth,
+ height: _iconWidth,
+ padding: EdgeInsets.only(right: _iconRightPadding),
+ name: check ? 'check' : 'uncheck',
),
- Expanded(
- child: FlowyRichText(
- key: _richTextKey,
- placeholderText: 'To-do',
- textNode: widget.textNode,
- textSpanDecorator: _textSpanDecorator,
- placeholderTextSpanDecorator: _textSpanDecorator,
- editorState: widget.editorState,
- ),
+ onTap: () {
+ TransactionBuilder(widget.editorState)
+ ..updateNode(widget.textNode, {
+ StyleKey.checkbox: !check,
+ })
+ ..commit();
+ },
+ ),
+ Flexible(
+ child: FlowyRichText(
+ key: _richTextKey,
+ placeholderText: 'To-do',
+ textNode: widget.textNode,
+ textSpanDecorator: _textSpanDecorator,
+ placeholderTextSpanDecorator: _textSpanDecorator,
+ editorState: widget.editorState,
),
- ],
- ),
+ ),
+ ],
),
);
}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart
index 39f484c23f..3489c2bb52 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart
@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:ui';
+import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
+import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
@@ -13,7 +15,6 @@ import 'package:appflowy_editor/src/document/text_delta.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
-import 'package:url_launcher/url_launcher_string.dart';
typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan);
@@ -42,7 +43,7 @@ class FlowyRichText extends StatefulWidget {
}
class _FlowyRichTextState extends State with Selectable {
- final _textKey = GlobalKey();
+ var _textKey = GlobalKey();
final _placeholderTextKey = GlobalKey();
final _lineHeight = 1.5;
@@ -53,6 +54,17 @@ class _FlowyRichTextState extends State with Selectable {
RenderParagraph get _placeholderRenderParagraph =>
_placeholderTextKey.currentContext?.findRenderObject() as RenderParagraph;
+ @override
+ void didUpdateWidget(covariant FlowyRichText oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ // https://github.com/flutter/flutter/issues/110342
+ if (_textKey.currentWidget is RichText) {
+ // Force refresh the RichText widget.
+ _textKey = GlobalKey();
+ }
+ }
+
@override
Widget build(BuildContext context) {
return _buildRichText(context);
@@ -182,7 +194,9 @@ class _FlowyRichTextState extends State with Selectable {
return RichText(
key: _textKey,
textHeightBehavior: const TextHeightBehavior(
- applyHeightToFirstAscent: false, applyHeightToLastDescent: false),
+ applyHeightToFirstAscent: false,
+ applyHeightToLastDescent: false,
+ ),
text: widget.textSpanDecorator != null
? widget.textSpanDecorator!(textSpan)
: textSpan,
@@ -193,53 +207,23 @@ class _FlowyRichTextState extends State with Selectable {
var offset = 0;
return TextSpan(
children: widget.textNode.delta.whereType().map((insert) {
- GestureRecognizer? gestureDetector;
+ GestureRecognizer? gestureRecognizer;
if (insert.attributes?[StyleKey.href] != null) {
- final startOffset = offset;
- Timer? timer;
- var tapCount = 0;
- gestureDetector = TapGestureRecognizer()
- ..onTap = () async {
- // implement a simple double tap logic
- tapCount += 1;
- timer?.cancel();
-
- if (tapCount == 2) {
- tapCount = 0;
- final href = insert.attributes![StyleKey.href];
- final uri = Uri.parse(href);
- // url_launcher cannot open a link without scheme.
- final newHref =
- (uri.scheme.isNotEmpty ? href : 'http://$href').trim();
- if (await canLaunchUrlString(newHref)) {
- await launchUrlString(newHref);
- }
- return;
- }
-
- timer = Timer(const Duration(milliseconds: 200), () {
- tapCount = 0;
- // update selection
- final selection = Selection.single(
- path: widget.textNode.path,
- startOffset: startOffset,
- endOffset: startOffset + insert.length,
- );
- widget.editorState.service.selectionService
- .updateSelection(selection);
- WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
- widget.editorState.service.toolbarService
- ?.triggerHandler('appflowy.toolbar.link');
- });
- });
- };
+ gestureRecognizer = _buildTapHrefGestureRecognizer(
+ insert.attributes![StyleKey.href],
+ Selection.single(
+ path: widget.textNode.path,
+ startOffset: offset,
+ endOffset: offset + insert.length,
+ ),
+ );
}
offset += insert.length;
final textSpan = RichTextStyle(
attributes: insert.attributes ?? {},
text: insert.content,
height: _lineHeight,
- gestureRecognizer: gestureDetector,
+ gestureRecognizer: gestureRecognizer,
).toTextSpan();
return textSpan;
}).toList(growable: false),
@@ -255,4 +239,34 @@ class _FlowyRichTextState extends State with Selectable {
height: _lineHeight,
).toTextSpan()
]);
+
+ GestureRecognizer _buildTapHrefGestureRecognizer(
+ String href, Selection selection) {
+ Timer? timer;
+ var tapCount = 0;
+ final tapGestureRecognizer = TapGestureRecognizer()
+ ..onTap = () async {
+ // implement a simple double tap logic
+ tapCount += 1;
+ timer?.cancel();
+
+ if (tapCount == 2) {
+ tapCount = 0;
+ safeLaunchUrl(href);
+ return;
+ }
+
+ timer = Timer(const Duration(milliseconds: 200), () {
+ tapCount = 0;
+ WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+ showLinkMenu(
+ context,
+ widget.editorState,
+ customSelection: selection,
+ );
+ });
+ });
+ };
+ return tapGestureRecognizer;
+ }
}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart
index 050b330f8b..fff25dd2d5 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart
@@ -63,16 +63,13 @@ class _HeadingTextNodeWidgetState extends State
top: _topPadding,
bottom: defaultLinePadding,
),
- child: SizedBox(
- width: defaultMaxTextNodeWidth,
- child: FlowyRichText(
- key: _richTextKey,
- placeholderText: 'Heading',
- placeholderTextSpanDecorator: _placeholderTextSpanDecorator,
- textSpanDecorator: _textSpanDecorator,
- textNode: widget.textNode,
- editorState: widget.editorState,
- ),
+ child: FlowyRichText(
+ key: _richTextKey,
+ placeholderText: 'Heading',
+ placeholderTextSpanDecorator: _placeholderTextSpanDecorator,
+ textSpanDecorator: _textSpanDecorator,
+ textNode: widget.textNode,
+ editorState: widget.editorState,
),
);
}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart
index c1062e1c3c..36cf91bdce 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart
@@ -58,28 +58,25 @@ class _NumberListTextNodeWidgetState extends State
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: defaultLinePadding),
- child: SizedBox(
- width: defaultMaxTextNodeWidth,
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- FlowySvg(
- key: iconKey,
- width: _iconWidth,
- height: _iconWidth,
- padding: EdgeInsets.only(right: _iconRightPadding),
- number: widget.textNode.attributes.number,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ FlowySvg(
+ key: iconKey,
+ width: _iconWidth,
+ height: _iconWidth,
+ padding: EdgeInsets.only(right: _iconRightPadding),
+ number: widget.textNode.attributes.number,
+ ),
+ Flexible(
+ child: FlowyRichText(
+ key: _richTextKey,
+ placeholderText: 'List',
+ textNode: widget.textNode,
+ editorState: widget.editorState,
),
- Expanded(
- child: FlowyRichText(
- key: _richTextKey,
- placeholderText: 'List',
- textNode: widget.textNode,
- editorState: widget.editorState,
- ),
- ),
- ],
- ),
+ ),
+ ],
));
}
}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart
index 78c6653904..9c2366d1cb 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart
@@ -55,30 +55,27 @@ class _QuotedTextNodeWidgetState extends State
@override
Widget build(BuildContext context) {
- return SizedBox(
- width: defaultMaxTextNodeWidth,
- child: Padding(
- padding: EdgeInsets.only(bottom: defaultLinePadding),
- child: IntrinsicHeight(
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- FlowySvg(
- key: iconKey,
- width: _iconWidth,
- padding: EdgeInsets.only(right: _iconRightPadding),
- name: 'quote',
+ return Padding(
+ padding: EdgeInsets.only(bottom: defaultLinePadding),
+ child: IntrinsicHeight(
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ FlowySvg(
+ key: iconKey,
+ width: _iconWidth,
+ padding: EdgeInsets.only(right: _iconRightPadding),
+ name: 'quote',
+ ),
+ Flexible(
+ child: FlowyRichText(
+ key: _richTextKey,
+ placeholderText: 'Quote',
+ textNode: widget.textNode,
+ editorState: widget.editorState,
),
- Expanded(
- child: FlowyRichText(
- key: _richTextKey,
- placeholderText: 'Quote',
- textNode: widget.textNode,
- editorState: widget.editorState,
- ),
- ),
- ],
- ),
+ ),
+ ],
),
),
);
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart
index d8dcfb91f6..b9a3e2f314 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart
@@ -52,15 +52,12 @@ class _RichTextNodeWidgetState extends State
@override
Widget build(BuildContext context) {
- return SizedBox(
- width: defaultMaxTextNodeWidth,
- child: Padding(
- padding: EdgeInsets.only(bottom: defaultLinePadding),
- child: FlowyRichText(
- key: _richTextKey,
- textNode: widget.textNode,
- editorState: widget.editorState,
- ),
+ return Padding(
+ padding: EdgeInsets.only(bottom: defaultLinePadding),
+ child: FlowyRichText(
+ key: _richTextKey,
+ textNode: widget.textNode,
+ editorState: widget.editorState,
),
);
}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart
index efcdd3790f..6270127610 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart
@@ -61,7 +61,6 @@ class StyleKey {
}
// TODO: customize
-double defaultMaxTextNodeWidth = 780.0;
double defaultLinePadding = 8.0;
double baseFontSize = 16.0;
String defaultHighlightColor = '0x6000BCF0';
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart
new file mode 100644
index 0000000000..e691ea689e
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart
@@ -0,0 +1,20 @@
+import 'package:flutter/material.dart';
+
+/// Editor style configuration
+class EditorStyle {
+ const EditorStyle({
+ required this.padding,
+ });
+
+ const EditorStyle.defaultStyle()
+ : padding = const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0);
+
+ /// The margin of the document context from the editor.
+ final EdgeInsets padding;
+
+ EditorStyle copyWith({EdgeInsets? padding}) {
+ return EditorStyle(
+ padding: padding ?? this.padding,
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart
index 107ae23b6f..9a1b2f1c02 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart
@@ -1,4 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
@@ -132,7 +133,7 @@ List defaultToolbarItems = [
tooltipsMessage: 'Link',
icon: const FlowySvg(name: 'toolbar/link'),
validator: _onlyShowInSingleTextSelection,
- handler: (editorState, context) => _showLinkMenu(editorState, context),
+ handler: (editorState, context) => showLinkMenu(context, editorState),
),
ToolbarItem(
id: 'appflowy.toolbar.highlight',
@@ -157,7 +158,12 @@ ToolbarShowValidator _showInTextSelection = (editorState) {
OverlayEntry? _linkMenuOverlay;
EditorState? _editorState;
-void _showLinkMenu(EditorState editorState, BuildContext context) {
+bool _changeSelectionInner = false;
+void showLinkMenu(
+ BuildContext context,
+ EditorState editorState, {
+ Selection? customSelection,
+}) {
final rects = editorState.service.selectionService.selectionRects;
var maxBottom = 0.0;
late Rect matchRect;
@@ -173,16 +179,19 @@ void _showLinkMenu(EditorState editorState, BuildContext context) {
// Since the link menu will only show in single text selection,
// We get the text node directly instead of judging details again.
- final selection =
- editorState.service.selectionService.currentSelection.value!;
+ final selection = customSelection ??
+ editorState.service.selectionService.currentSelection.value;
+ final node = editorState.service.selectionService.currentSelectedNodes;
+ if (selection == null || node.isEmpty || node.first is! TextNode) {
+ return;
+ }
final index =
selection.isBackward ? selection.start.offset : selection.end.offset;
final length = (selection.start.offset - selection.end.offset).abs();
- final node = editorState.service.selectionService.currentSelectedNodes.first
- as TextNode;
+ final textNode = node.first as TextNode;
String? linkText;
- if (node.allSatisfyLinkInSelection(selection)) {
- linkText = node.getAttributeInSelection(selection, StyleKey.href);
+ if (textNode.allSatisfyLinkInSelection(selection)) {
+ linkText = textNode.getAttributeInSelection(selection, StyleKey.href);
}
_linkMenuOverlay = OverlayEntry(builder: (context) {
return Positioned(
@@ -191,9 +200,12 @@ void _showLinkMenu(EditorState editorState, BuildContext context) {
child: Material(
child: LinkMenu(
linkText: linkText,
+ onOpenLink: () async {
+ await safeLaunchUrl(linkText);
+ },
onSubmitted: (text) {
TransactionBuilder(editorState)
- ..formatText(node, index, length, {StyleKey.href: text})
+ ..formatText(textNode, index, length, {StyleKey.href: text})
..commit();
_dismissLinkMenu();
},
@@ -203,10 +215,17 @@ void _showLinkMenu(EditorState editorState, BuildContext context) {
},
onRemoveLink: () {
TransactionBuilder(editorState)
- ..formatText(node, index, length, {StyleKey.href: null})
+ ..formatText(textNode, index, length, {StyleKey.href: null})
..commit();
_dismissLinkMenu();
},
+ onFocusChange: (value) {
+ if (value && customSelection != null) {
+ _changeSelectionInner = true;
+ editorState.service.selectionService
+ .updateSelection(customSelection);
+ }
+ },
),
),
);
@@ -214,12 +233,24 @@ void _showLinkMenu(EditorState editorState, BuildContext context) {
Overlay.of(context)?.insert(_linkMenuOverlay!);
editorState.service.scrollService?.disable();
- editorState.service.keyboardService?.disable();
editorState.service.selectionService.currentSelection
.addListener(_dismissLinkMenu);
}
void _dismissLinkMenu() {
+ // workaround: SelectionService has been released after hot reload.
+ final isSelectionDisposed =
+ _editorState?.service.selectionServiceKey.currentState == null;
+ if (isSelectionDisposed) {
+ return;
+ }
+ if (_editorState?.service.selectionService.currentSelection.value == null) {
+ return;
+ }
+ if (_changeSelectionInner) {
+ _changeSelectionInner = false;
+ return;
+ }
_linkMenuOverlay?.remove();
_linkMenuOverlay = null;
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart
index 2781471b46..3a8d75560b 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart
@@ -1,5 +1,6 @@
import 'package:appflowy_editor/src/render/image/image_node_builder.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
+import 'package:appflowy_editor/src/render/style/editor_style.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/default_key_event_handlers.dart';
import 'package:flutter/material.dart';
@@ -36,6 +37,7 @@ class AppFlowyEditor extends StatefulWidget {
this.customBuilders = const {},
this.keyEventHandlers = const [],
this.selectionMenuItems = const [],
+ this.editorStyle = const EditorStyle.defaultStyle(),
}) : super(key: key);
final EditorState editorState;
@@ -48,6 +50,8 @@ class AppFlowyEditor extends StatefulWidget {
final List selectionMenuItems;
+ final EditorStyle editorStyle;
+
@override
State createState() => _AppFlowyEditorState();
}
@@ -60,6 +64,7 @@ class _AppFlowyEditorState extends State {
super.initState();
editorState.selectionMenuItems = widget.selectionMenuItems;
+ editorState.editorStyle = widget.editorStyle;
editorState.service.renderPluginService = _createRenderPlugin();
}
@@ -68,6 +73,8 @@ class _AppFlowyEditorState extends State {
super.didUpdateWidget(oldWidget);
if (editorState.service != oldWidget.editorState.service) {
+ editorState.selectionMenuItems = widget.selectionMenuItems;
+ editorState.editorStyle = widget.editorStyle;
editorState.service.renderPluginService = _createRenderPlugin();
}
}
@@ -76,27 +83,31 @@ class _AppFlowyEditorState extends State {
Widget build(BuildContext context) {
return AppFlowyScroll(
key: editorState.service.scrollServiceKey,
- child: AppFlowySelection(
- key: editorState.service.selectionServiceKey,
- editorState: editorState,
- child: AppFlowyInput(
- key: editorState.service.inputServiceKey,
+ child: Padding(
+ padding: widget.editorStyle.padding,
+ child: AppFlowySelection(
+ key: editorState.service.selectionServiceKey,
editorState: editorState,
- child: AppFlowyKeyboard(
- key: editorState.service.keyboardServiceKey,
- handlers: [
- ...defaultKeyEventHandlers,
- ...widget.keyEventHandlers,
- ],
+ child: AppFlowyInput(
+ key: editorState.service.inputServiceKey,
editorState: editorState,
- child: FlowyToolbar(
- key: editorState.service.toolbarServiceKey,
+ child: AppFlowyKeyboard(
+ key: editorState.service.keyboardServiceKey,
+ handlers: [
+ ...defaultKeyEventHandlers,
+ ...widget.keyEventHandlers,
+ ],
editorState: editorState,
- child: editorState.service.renderPluginService.buildPluginWidget(
- NodeWidgetContext(
- context: context,
- node: editorState.document.root,
- editorState: editorState,
+ child: FlowyToolbar(
+ key: editorState.service.toolbarServiceKey,
+ editorState: editorState,
+ child:
+ editorState.service.renderPluginService.buildPluginWidget(
+ NodeWidgetContext(
+ context: context,
+ node: editorState.document.root,
+ editorState: editorState,
+ ),
),
),
),
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart
index 96f0777544..a92fae1b95 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart
@@ -1,5 +1,4 @@
import 'package:appflowy_editor/src/infra/log.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -150,9 +149,6 @@ class _AppFlowyInputState extends State
textNode,
delta.insertionOffset,
delta.textInserted,
- removedAttributes: {
- StyleKey.href: null,
- },
)
..commit();
} else {
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart
similarity index 90%
rename from frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart
rename to frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart
index d2a3d51e64..0eeaf654de 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart
@@ -11,10 +11,16 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
var nodes = editorState.service.selectionService.currentSelectedNodes;
nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false);
selection = selection.isBackward ? selection : selection.reversed;
- // make sure all nodes is [TextNode].
final textNodes = nodes.whereType().toList();
+ final nonTextNodes =
+ nodes.where((node) => node is! TextNode).toList(growable: false);
final transactionBuilder = TransactionBuilder(editorState);
+
+ if (nonTextNodes.isNotEmpty) {
+ transactionBuilder.deleteNodes(nonTextNodes);
+ }
+
if (textNodes.length == 1) {
final textNode = textNodes.first;
final index = textNode.delta.prevRunePosition(selection.start.offset);
@@ -68,10 +74,15 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
}
}
} else {
- _deleteNodes(transactionBuilder, textNodes, selection);
+ if (textNodes.isNotEmpty) {
+ _deleteTextNodes(transactionBuilder, textNodes, selection);
+ }
}
if (transactionBuilder.operations.isNotEmpty) {
+ if (nonTextNodes.isNotEmpty) {
+ transactionBuilder.afterSelection = Selection.collapsed(selection.start);
+ }
transactionBuilder.commit();
}
@@ -121,7 +132,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
}
}
} else {
- _deleteNodes(transactionBuilder, textNodes, selection);
+ _deleteTextNodes(transactionBuilder, textNodes, selection);
}
transactionBuilder.commit();
@@ -129,7 +140,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
return KeyEventResult.handled;
}
-void _deleteNodes(TransactionBuilder transactionBuilder,
+void _deleteTextNodes(TransactionBuilder transactionBuilder,
List textNodes, Selection selection) {
final first = textNodes.first;
final last = textNodes.last;
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart
index 468eda4e98..a8cbdee3ab 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart
@@ -1,6 +1,6 @@
import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
-import 'package:appflowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart';
+import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart';
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart
index 1867574993..dee3f42725 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart
@@ -129,6 +129,10 @@ class _AppFlowyKeyboardState extends State
void _onFocusChange(bool value) {
Log.keyboard.debug('on keyboard event focus change $value');
+ isFocus = value;
+ if (!value) {
+ widget.editorState.service.selectionService.clearCursor();
+ }
}
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart
index c5e351059c..6f6897596f 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart
@@ -57,6 +57,9 @@ abstract class AppFlowySelectionService {
/// Clears the selection area, cursor area and the popup list area.
void clearSelection();
+ /// Clears the cursor area.
+ void clearCursor();
+
/// Returns the [Node]s in [Selection].
List getNodesInSelection(Selection selection);
@@ -205,16 +208,23 @@ class _AppFlowySelectionState extends State
currentSelectedNodes = [];
currentSelection.value = null;
+ clearCursor();
// clear selection areas
_selectionAreas
..forEach((overlay) => overlay.remove())
..clear();
// clear cursor areas
+
+ // hide toolbar
+ editorState.service.toolbarService?.hide();
+ }
+
+ @override
+ void clearCursor() {
+ // clear cursor areas
_cursorAreas
..forEach((overlay) => overlay.remove())
..clear();
- // hide toolbar
- editorState.service.toolbarService?.hide();
}
@override
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart
index e26a186387..8dba7dcb8e 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart
@@ -90,7 +90,7 @@ class _FlowyToolbarState extends State
.where((item) => item.validator(widget.editorState))
.toList(growable: false)
..sort((a, b) => a.type.compareTo(b.type));
- if (items.isEmpty) {
+ if (filterItems.isEmpty) {
return [];
}
final List dividedItems = [filterItems.first];
diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart
index a815d91875..e3a5a7d0c5 100644
--- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart
@@ -80,7 +80,7 @@ class EditorWidgetTester {
} else {
_editorState.service.selectionService.updateSelection(selection);
}
- await tester.pumpAndSettle();
+ await tester.pump(const Duration(milliseconds: 200));
expect(_editorState.service.selectionService.currentSelection.value,
selection);
diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart
index 9121fa1868..a9732d8a20 100644
--- a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart
@@ -49,9 +49,10 @@ void main() async {
final editorRect = tester.getRect(editorFinder);
final leftImageRect = tester.getRect(imageFinder.at(0));
- expect(leftImageRect.left, editorRect.left);
+ expect(leftImageRect.left, editor.editorState.editorStyle.padding.left);
final rightImageRect = tester.getRect(imageFinder.at(2));
- expect(rightImageRect.right, editorRect.right);
+ expect(rightImageRect.right,
+ editorRect.right - editor.editorState.editorStyle.padding.right);
final centerImageRect = tester.getRect(imageFinder.at(1));
expect(centerImageRect.left,
(leftImageRect.left + rightImageRect.left) / 2.0);
@@ -73,8 +74,8 @@ void main() async {
leftImage.onAlign(Alignment.centerRight);
await tester.pump(const Duration(milliseconds: 100));
expect(
- tester.getRect(imageFinder.at(0)).left,
- rightImageRect.left,
+ tester.getRect(imageFinder.at(0)).right,
+ rightImageRect.right,
);
});
});
diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart
index d2f774d33f..a566b7ec07 100644
--- a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart
@@ -1,3 +1,6 @@
+import 'dart:collection';
+
+import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/image/image_node_widget.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@@ -20,6 +23,14 @@ void main() async {
final widget = ImageNodeWidget(
src: src,
+ node: Node(
+ type: 'image',
+ children: LinkedList(),
+ attributes: {
+ 'image_src': src,
+ 'align': 'center',
+ },
+ ),
alignment: Alignment.center,
onCopy: () {
onCopyHit = true;
diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart
index 7b4541033b..cef16a1cec 100644
--- a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart
@@ -12,8 +12,10 @@ void main() async {
const link = 'appflowy.io';
var submittedText = '';
final linkMenu = LinkMenu(
+ onOpenLink: () {},
onCopyLink: () {},
onRemoveLink: () {},
+ onFocusChange: (value) {},
onSubmitted: (text) {
submittedText = text;
},
diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart
index f039c227d9..afd89ddee9 100644
--- a/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart
@@ -10,8 +10,8 @@ void main() async {
TestWidgetsFlutterBinding.ensureInitialized();
});
- group('delete_text_handler.dart', () {
- testWidgets('Presses backspace key in empty document', (tester) async {
+ group('checkbox_text_handler.dart', () {
+ testWidgets('Click checkbox icon', (tester) async {
// Before
//
// [BIUS]Welcome to Appflowy 😁[BIUS]
diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart
similarity index 73%
rename from frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart
rename to frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart
index 1e7bf4e842..1976ec3250 100644
--- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart
@@ -1,7 +1,9 @@
import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/render/image/image_node_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
+import 'package:network_image_mock/network_image_mock.dart';
import '../../infra/test_editor.dart';
void main() async {
@@ -9,7 +11,7 @@ void main() async {
TestWidgetsFlutterBinding.ensureInitialized();
});
- group('delete_text_handler.dart', () {
+ group('backspace_handler.dart', () {
testWidgets('Presses backspace key in empty document', (tester) async {
// Before
//
@@ -167,6 +169,129 @@ void main() async {
testWidgets('Presses delete key in styled text (quote)', (tester) async {
await _deleteStyledTextByDelete(tester, StyleKey.quote);
});
+
+ // Before
+ //
+ // Welcome to Appflowy 😁
+ // Welcome to Appflowy 😁
+ // [Image]
+ // Welcome to Appflowy 😁
+ // Welcome to Appflowy 😁
+ //
+ // After
+ //
+ // Welcome to Appflowy 😁
+ // Welcome to Appflowy 😁
+ //
+ testWidgets('Deletes the image surrounded by text', (tester) async {
+ mockNetworkImagesFor(() async {
+ const text = 'Welcome to Appflowy 😁';
+ const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg';
+ final editor = tester.editor
+ ..insertTextNode(text)
+ ..insertTextNode(text)
+ ..insertImageNode(src)
+ ..insertTextNode(text)
+ ..insertTextNode(text);
+ await editor.startTesting();
+
+ expect(editor.documentLength, 5);
+ expect(find.byType(ImageNodeWidget), findsOneWidget);
+
+ await editor.updateSelection(
+ Selection(
+ start: Position(path: [1], offset: 0),
+ end: Position(path: [3], offset: text.length),
+ ),
+ );
+
+ await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+ expect(editor.documentLength, 3);
+ expect(find.byType(ImageNodeWidget), findsNothing);
+ expect(
+ editor.documentSelection,
+ Selection.single(path: [1], startOffset: 0),
+ );
+ });
+ });
+
+ testWidgets('Deletes the first image, and selection is backward',
+ (tester) async {
+ await _deleteFirstImage(tester, true);
+ });
+
+ testWidgets('Deletes the first image, and selection is not backward',
+ (tester) async {
+ await _deleteFirstImage(tester, false);
+ });
+
+ testWidgets('Deletes the last image and selection is backward',
+ (tester) async {
+ await _deleteLastImage(tester, true);
+ });
+
+ testWidgets('Deletes the last image and selection is not backward',
+ (tester) async {
+ await _deleteLastImage(tester, false);
+ });
+}
+
+Future _deleteFirstImage(WidgetTester tester, bool isBackward) async {
+ mockNetworkImagesFor(() async {
+ const text = 'Welcome to Appflowy 😁';
+ const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg';
+ final editor = tester.editor
+ ..insertImageNode(src)
+ ..insertTextNode(text)
+ ..insertTextNode(text);
+ await editor.startTesting();
+
+ expect(editor.documentLength, 3);
+ expect(find.byType(ImageNodeWidget), findsOneWidget);
+
+ final start = Position(path: [0], offset: 0);
+ final end = Position(path: [1], offset: 1);
+ await editor.updateSelection(
+ Selection(
+ start: isBackward ? start : end,
+ end: isBackward ? end : start,
+ ),
+ );
+
+ await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+ expect(editor.documentLength, 2);
+ expect(find.byType(ImageNodeWidget), findsNothing);
+ expect(editor.documentSelection, Selection.collapsed(start));
+ });
+}
+
+Future _deleteLastImage(WidgetTester tester, bool isBackward) async {
+ mockNetworkImagesFor(() async {
+ const text = 'Welcome to Appflowy 😁';
+ const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg';
+ final editor = tester.editor
+ ..insertTextNode(text)
+ ..insertTextNode(text)
+ ..insertImageNode(src);
+ await editor.startTesting();
+
+ expect(editor.documentLength, 3);
+ expect(find.byType(ImageNodeWidget), findsOneWidget);
+
+ final start = Position(path: [1], offset: 0);
+ final end = Position(path: [2], offset: 1);
+ await editor.updateSelection(
+ Selection(
+ start: isBackward ? start : end,
+ end: isBackward ? end : start,
+ ),
+ );
+
+ await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+ expect(editor.documentLength, 2);
+ expect(find.byType(ImageNodeWidget), findsNothing);
+ expect(editor.documentSelection, Selection.collapsed(start));
+ });
}
Future _deleteStyledTextByBackspace(
diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart
index ee21dfa455..5bfe1ada67 100644
--- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart
@@ -116,6 +116,27 @@ void main() async {
(tester) async {
_testMultipleSelection(tester, false);
});
+
+ testWidgets('Presses enter key in the first line', (tester) async {
+ // Before
+ //
+ // Welcome to Appflowy 😁
+ //
+ // After
+ //
+ // [Empty Line]
+ // Welcome to Appflowy 😁
+ //
+ const text = 'Welcome to Appflowy 😁';
+ final editor = tester.editor..insertTextNode(text);
+ await editor.startTesting();
+ await editor.updateSelection(
+ Selection.single(path: [0], startOffset: 0),
+ );
+ await editor.pressLogicKey(LogicalKeyboardKey.enter);
+ expect(editor.documentLength, 2);
+ expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text);
+ });
});
}