diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart
index 641a985577..3ac7293a2c 100644
--- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart
+++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart
@@ -46,6 +46,13 @@ class Selection {
(start.path <= end.path && !pathEquals(start.path, end.path)) ||
(isSingle && start.offset < end.offset);
+ Selection normalize() {
+ if (isForward) {
+ return Selection(start: end, end: start);
+ }
+ return this;
+ }
+
Selection get reversed => copyWith(start: end, end: start);
Selection collapse({bool atStart = false}) {
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart
index e16237c0fa..4a15b54859 100644
--- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart
+++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart
@@ -17,7 +17,10 @@ const String tagList = "li";
const String tagParagraph = "p";
const String tagImage = "img";
const String tagAnchor = "a";
+const String tagItalic = "i";
const String tagBold = "b";
+const String tagUnderline = "u";
+const String tagDel = "del";
const String tagStrong = "strong";
const String tagSpan = "span";
const String tagCode = "code";
@@ -54,7 +57,10 @@ class HTMLToNodesConverter {
if (child.localName == tagAnchor ||
child.localName == tagSpan ||
child.localName == tagCode ||
- child.localName == tagStrong) {
+ child.localName == tagStrong ||
+ child.localName == tagUnderline ||
+ child.localName == tagItalic ||
+ child.localName == tagDel) {
_handleRichTextElement(delta, child);
} else if (child.localName == tagBold) {
// Google docs wraps the the content inside the `` tag.
@@ -128,7 +134,7 @@ class HTMLToNodesConverter {
if (tuples.length < 2) {
continue;
}
- result[tuples[0]] = tuples[1];
+ result[tuples[0].trim()] = tuples[1].trim();
}
return result;
@@ -142,12 +148,21 @@ class HTMLToNodesConverter {
final fontWeightStr = cssMap["font-weight"];
if (fontWeightStr != null) {
- int? weight = int.tryParse(fontWeightStr);
- if (weight != null && weight > 500) {
- attrs["bold"] = true;
+ if (fontWeightStr == "bold") {
+ attrs[StyleKey.bold] = true;
+ } else {
+ int? weight = int.tryParse(fontWeightStr);
+ if (weight != null && weight > 500) {
+ attrs[StyleKey.bold] = true;
+ }
}
}
+ final textDecorationStr = cssMap["text-decoration"];
+ if (textDecorationStr != null) {
+ _assignTextDecorations(attrs, textDecorationStr);
+ }
+
final backgroundColorStr = cssMap["background-color"];
final backgroundColor = _tryParseCssColorString(backgroundColorStr);
if (backgroundColor != null) {
@@ -155,9 +170,24 @@ class HTMLToNodesConverter {
'0x${backgroundColor.value.toRadixString(16)}';
}
+ if (cssMap["font-style"] == "italic") {
+ attrs[StyleKey.italic] = true;
+ }
+
return attrs.isEmpty ? null : attrs;
}
+ _assignTextDecorations(Attributes attrs, String decorationStr) {
+ final decorations = decorationStr.split(" ");
+ for (final d in decorations) {
+ if (d == "line-through") {
+ attrs[StyleKey.strikethrough] = true;
+ } else if (d == "underline") {
+ attrs[StyleKey.underline] = true;
+ }
+ }
+ }
+
/// Try to parse the `rgba(red, greed, blue, alpha)`
/// from the string.
Color? _tryParseCssColorString(String? colorString) {
@@ -202,7 +232,13 @@ class HTMLToNodesConverter {
}
delta.insert(element.text, attributes);
} else if (element.localName == tagStrong || element.localName == tagBold) {
- delta.insert(element.text, {"bold": true});
+ delta.insert(element.text, {StyleKey.bold: true});
+ } else if (element.localName == tagUnderline) {
+ delta.insert(element.text, {StyleKey.underline: true});
+ } else if (element.localName == tagItalic) {
+ delta.insert(element.text, {StyleKey.italic: true});
+ } else if (element.localName == tagDel) {
+ delta.insert(element.text, {StyleKey.strikethrough: true});
} else {
delta.insert(element.text);
}
@@ -397,6 +433,18 @@ class NodesToHTMLConverter {
checked: textNode.attributes["checkbox"] == true);
}
+ String _textDecorationsFromAttributes(Attributes attributes) {
+ var textDecoration = [];
+ if (attributes[StyleKey.strikethrough] == true) {
+ textDecoration.add("line-through");
+ }
+ if (attributes[StyleKey.underline] == true) {
+ textDecoration.add("underline");
+ }
+
+ return textDecoration.join(" ");
+ }
+
String _attributesToCssStyle(Map attributes) {
final cssMap = {};
if (attributes[StyleKey.backgroundColor] != null) {
@@ -414,6 +462,15 @@ class NodesToHTMLConverter {
if (attributes[StyleKey.bold] == true) {
cssMap["font-weight"] = "bold";
}
+
+ final textDecoration = _textDecorationsFromAttributes(attributes);
+ if (textDecoration.isNotEmpty) {
+ cssMap["text-decoration"] = textDecoration;
+ }
+
+ if (attributes[StyleKey.italic] == true) {
+ cssMap["font-style"] = "italic";
+ }
return _cssMapToCssStyle(cssMap);
}
@@ -427,6 +484,22 @@ class NodesToHTMLConverter {
});
}
+ /// Convert the rich text to HTML
+ ///
+ /// Use `` for bold only.
+ /// Use `` for italic only.
+ /// Use `` for strikethrough only.
+ /// Use `` for underline only.
+ ///
+ /// If the text has multiple styles, use a ``
+ /// to mix the styles.
+ ///
+ /// A CSS style string is used to describe the styles.
+ /// The HTML will be:
+ ///
+ /// ```html
+ /// Text
+ /// ```
html.Element _deltaToHtml(Delta delta,
{String? subType, int? end, bool? checked}) {
if (end != null) {
@@ -454,6 +527,21 @@ class NodesToHTMLConverter {
final strong = html.Element.tag(tagStrong);
strong.append(html.Text(op.content));
childNodes.add(strong);
+ } else if (attributes.length == 1 &&
+ attributes[StyleKey.underline] == true) {
+ final strong = html.Element.tag(tagUnderline);
+ strong.append(html.Text(op.content));
+ childNodes.add(strong);
+ } else if (attributes.length == 1 &&
+ attributes[StyleKey.italic] == true) {
+ final strong = html.Element.tag(tagItalic);
+ strong.append(html.Text(op.content));
+ childNodes.add(strong);
+ } else if (attributes.length == 1 &&
+ attributes[StyleKey.strikethrough] == true) {
+ final strong = html.Element.tag(tagDel);
+ strong.append(html.Text(op.content));
+ childNodes.add(strong);
} else {
final span = html.Element.tag(tagSpan);
final cssString = _attributesToCssStyle(attributes);
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart
index a5f392a4eb..363b84967a 100644
--- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart
+++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart
@@ -6,10 +6,11 @@ import 'package:flutter/services.dart';
import 'package:rich_clipboard/rich_clipboard.dart';
_handleCopy(EditorState editorState) async {
- final selection = editorState.cursorSelection;
+ var selection = editorState.cursorSelection;
if (selection == null || selection.isCollapsed) {
return;
}
+ selection = selection.normalize();
if (pathEquals(selection.start.path, selection.end.path)) {
final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!;
if (nodeAtPath.type == "text") {