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") {