From ee25f032db3d45dba1e841bdf449008c84cf3464 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 1 Aug 2022 15:34:22 +0800 Subject: [PATCH 1/9] feat: copy & paste key event handlers --- .../lib/service/editor_service.dart | 18 ++++++++++-------- .../copy_paste_handler.dart | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index b62fe1bb15..39382d3a9c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -7,17 +7,18 @@ import 'package:flowy_editor/render/editor/editor_entry.dart'; import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart'; import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/service/input_service.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/copy_paste_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/service/selection_service.dart'; import 'package:flowy_editor/render/rich_text/heading_text.dart'; import 'package:flowy_editor/render/rich_text/number_list_text.dart'; import 'package:flowy_editor/render/rich_text/quoted_text.dart'; -import 'package:flowy_editor/service/input_service.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flowy_editor/service/selection_service.dart'; import 'package:flowy_editor/service/toolbar_service.dart'; NodeWidgetBuilders defaultBuilders = { @@ -35,6 +36,7 @@ List defaultKeyEventHandler = [ slashShortcutHandler, flowyDeleteNodesHandler, arrowKeysHandler, + copyPasteKeysHandler, enterInEdgeOfTextNodeHandler, updateTextStyleByCommandXHandler, ]; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart new file mode 100644 index 0000000000..52e2603dbd --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart @@ -0,0 +1,19 @@ +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +FlowyKeyEventHandler copyPasteKeysHandler = (editorState, event) { + if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyC) { + debugPrint("copy"); + return KeyEventResult.handled; + } + if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyV) { + debugPrint("paste"); + return KeyEventResult.handled; + } + if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyX) { + debugPrint("cut"); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +}; From 9ceced464817b2df1a1df77c67cd52857001da68 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 1 Aug 2022 16:14:56 +0800 Subject: [PATCH 2/9] feat: parse html --- .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + .../flowy_editor/example/macos/Podfile.lock | 6 ++ .../flowy_editor/example/pubspec.lock | 86 ++++++++++++++++++- .../lib/infra/html_converter.dart | 29 +++++++ .../copy_paste_handler.dart | 41 ++++++++- .../packages/flowy_editor/pubspec.yaml | 2 + 8 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc index f6f23bfe97..00fd3bc03f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc @@ -6,9 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) rich_clipboard_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "RichClipboardPlugin"); + rich_clipboard_plugin_register_with_registrar(rich_clipboard_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake index f16b4c3421..0342e3868a 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + rich_clipboard_linux url_launcher_linux ) diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift index 8236f5728c..0dc858f3c7 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,10 @@ import FlutterMacOS import Foundation +import rich_clipboard_macos import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile.lock b/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile.lock index 4f162e68af..93389ef3ec 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile.lock +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile.lock @@ -1,20 +1,26 @@ PODS: - FlutterMacOS (1.0.0) + - rich_clipboard_macos (0.0.1): + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) + - rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral + rich_clipboard_macos: + :path: Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock index cfadcb8242..8a0f4ea223 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock @@ -43,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.2" cupertino_icons: dependency: "direct main" description: @@ -57,6 +64,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" flowy_editor: dependency: "direct main" description: @@ -93,6 +107,13 @@ packages: description: flutter source: sdk version: "0.0.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.0" js: dependency: transitive description: @@ -177,6 +198,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.0.3" + rich_clipboard: + dependency: transitive + description: + name: rich_clipboard + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + rich_clipboard_android: + dependency: transitive + description: + name: rich_clipboard_android + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + rich_clipboard_ios: + dependency: transitive + description: + name: rich_clipboard_ios + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + rich_clipboard_linux: + dependency: transitive + description: + name: rich_clipboard_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + rich_clipboard_macos: + dependency: transitive + description: + name: rich_clipboard_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + rich_clipboard_platform_interface: + dependency: transitive + description: + name: rich_clipboard_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + rich_clipboard_web: + dependency: transitive + description: + name: rich_clipboard_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + rich_clipboard_windows: + dependency: transitive + description: + name: rich_clipboard_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" sky_engine: dependency: transitive description: flutter @@ -287,6 +364,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.6.1" xml: dependency: transitive description: @@ -296,4 +380,4 @@ packages: version: "6.1.0" sdks: dart: ">=2.17.0 <3.0.0" - flutter: ">=2.11.0-0.1.pre" + flutter: ">=3.0.0" diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart new file mode 100644 index 0000000000..e1920555af --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -0,0 +1,29 @@ +import 'dart:collection'; + +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:html/parser.dart' show parse; +import 'package:html/dom.dart' as html; + +class HTMLConverter { + final html.Document _document; + + HTMLConverter(String htmlString) : _document = parse(htmlString); + + List toNodes() { + final result = []; + final delta = Delta(); + + final bodyChildren = _document.body?.children ?? []; + for (final child in bodyChildren) { + delta.insert(child.text); + } + + if (delta.operations.isNotEmpty) { + result.add(TextNode( + type: "text", children: LinkedList(), attributes: {}, delta: delta)); + } + + return result; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart index 52e2603dbd..dd20f39ca9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart @@ -1,18 +1,53 @@ +import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/infra/html_converter.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:rich_clipboard/rich_clipboard.dart'; + +_handleCopy() async { + debugPrint('copy'); +} + +_pasteHTML(EditorState editorState, String html) { + final converter = HTMLConverter(html); + final nodes = converter.toNodes(); + final selection = editorState.cursorSelection; + if (selection == null) { + return; + } + + final tb = TransactionBuilder(editorState); + for (final node in nodes) { + tb.insertNode(selection.end.path, node); + } + tb.commit(); +} + +_handlePaste(EditorState editorState) async { + final data = await RichClipboard.getData(); + if (data.html != null) { + _pasteHTML(editorState, data.html!); + return; + } + debugPrint('paste ${data.text ?? ''}'); +} + +_handleCut() { + debugPrint('cut'); +} FlowyKeyEventHandler copyPasteKeysHandler = (editorState, event) { if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyC) { - debugPrint("copy"); + _handleCopy(); return KeyEventResult.handled; } if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyV) { - debugPrint("paste"); + _handlePaste(editorState); return KeyEventResult.handled; } if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyX) { - debugPrint("cut"); + _handleCut(); return KeyEventResult.handled; } return KeyEventResult.ignored; diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index db0eef5296..e3a6aab187 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -11,6 +11,8 @@ dependencies: flutter: sdk: flutter + rich_clipboard: ^1.0.0 + html: ^0.15.0 flutter_svg: ^1.1.1+1 provider: ^6.0.3 From 40c3f07be431606136b603cfdff266d193f0b666 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 1 Aug 2022 18:20:10 +0800 Subject: [PATCH 3/9] feat: use patch nodes --- .../copy_paste_handler.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart index dd20f39ca9..01149af9ca 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart @@ -17,10 +17,14 @@ _pasteHTML(EditorState editorState, String html) { return; } - final tb = TransactionBuilder(editorState); - for (final node in nodes) { - tb.insertNode(selection.end.path, node); + final path = [...selection.end.path]; + if (path.isEmpty) { + return; } + path[path.length - 1]++; + + final tb = TransactionBuilder(editorState); + tb.insertNodes(path, nodes); tb.commit(); } From d28321167184c0ce041fca5d21c9c563dec1ed75 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 2 Aug 2022 11:30:52 +0800 Subject: [PATCH 4/9] feat: paste multi lines text --- .../flowy_editor/lib/document/node.dart | 7 +++-- .../lib/infra/html_converter.dart | 5 +--- .../copy_paste_handler.dart | 28 ++++++++++++++++++- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 3a7ad36456..c3f75c5c9c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -176,10 +176,11 @@ class TextNode extends Node { TextNode({ required super.type, - required super.children, - required super.attributes, required Delta delta, - }) : _delta = delta; + LinkedList? children, + Attributes? attributes, + }) : _delta = delta, + super(children: children ?? LinkedList(), attributes: attributes ?? {}); TextNode.empty() : _delta = Delta([TextInsert(' ')]), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart index e1920555af..40687ca160 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -1,5 +1,3 @@ -import 'dart:collection'; - import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:html/parser.dart' show parse; @@ -20,8 +18,7 @@ class HTMLConverter { } if (delta.operations.isNotEmpty) { - result.add(TextNode( - type: "text", children: LinkedList(), attributes: {}, delta: delta)); + result.add(TextNode(type: "text", delta: delta)); } return result; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart index 01149af9ca..ca9debb2b0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart @@ -34,7 +34,33 @@ _handlePaste(EditorState editorState) async { _pasteHTML(editorState, data.html!); return; } - debugPrint('paste ${data.text ?? ''}'); + if (data.text != null) { + _handlePastePlainText(editorState, data.text!); + return; + } +} + +_handlePastePlainText(EditorState editorState, String plainText) { + final selection = editorState.cursorSelection; + if (selection == null) { + return; + } + + final path = [...selection.end.path]; + if (path.isEmpty) { + return; + } + path[path.length - 1]++; + + final lines = + plainText.split("\n").map((e) => e.replaceAll(RegExp(r'\r'), "")); + final nodes = lines + .map((e) => TextNode(type: "text", delta: Delta().insert(e))) + .toList(); + + final tb = TransactionBuilder(editorState); + tb.insertNodes(path, nodes); + tb.commit(); } _handleCut() { From 67fd06366e8830cfb59d7787761f49ae39cb69e5 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 2 Aug 2022 15:19:17 +0800 Subject: [PATCH 5/9] feat: handle HTMLElement --- .../example/lib/plugin/image_node_widget.dart | 3 +- .../lib/infra/html_converter.dart | 94 +++++++++++++++++-- 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 6a01fb6430..417d1ce11c 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -90,8 +90,7 @@ class _ImageNodeWidgetState extends State with Selectable { @override Position getPositionInOffset(Offset start) { - // TODO: implement getPositionInOffset - throw UnimplementedError(); + return Position(path: node.path, offset: 0); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart index 40687ca160..e39d082607 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:html/parser.dart' show parse; @@ -10,17 +12,97 @@ class HTMLConverter { List toNodes() { final result = []; - final delta = Delta(); final bodyChildren = _document.body?.children ?? []; for (final child in bodyChildren) { - delta.insert(child.text); - } - - if (delta.operations.isNotEmpty) { - result.add(TextNode(type: "text", delta: delta)); + _handleElement(result, child); } return result; } + + _handleElement(List nodes, html.Element element) { + if (element.localName == "h1") { + _handleHeadingElement(nodes, element, "h1"); + } else if (element.localName == "h2") { + _handleHeadingElement(nodes, element, "h2"); + } else if (element.localName == "h3") { + _handleHeadingElement(nodes, element, "h3"); + } else if (element.localName == "ul") { + _handleUnorderedList(nodes, element); + } else if (element.localName == "li") { + _handleListElement(nodes, element); + } else if (element.localName == "p") { + _handleParagraph(nodes, element); + } else { + final delta = Delta(); + delta.insert(element.text); + if (delta.operations.isNotEmpty) { + nodes.add(TextNode(type: "text", delta: delta)); + } + } + } + + _handleParagraph(List nodes, html.Element element) { + for (final child in element.children) { + if (child.localName == "a") { + _handleAnchorLink(nodes, child); + } + } + + final delta = Delta(); + delta.insert(element.text); + if (delta.operations.isNotEmpty) { + nodes.add(TextNode(type: "text", delta: delta)); + } + } + + _handleAnchorLink(List nodes, html.Element element) { + for (final child in element.children) { + if (child.localName == "img") { + _handleImage(nodes, child); + return; + } + } + } + + _handleImage(List nodes, html.Element element) { + final src = element.attributes["src"]; + final attributes = {}; + if (src != null) { + attributes["image_src"] = src; + } + nodes.add( + Node(type: "image", attributes: attributes, children: LinkedList())); + } + + _handleUnorderedList(List nodes, html.Element element) { + element.children.forEach((child) { + _handleListElement(nodes, child); + }); + } + + _handleHeadingElement( + List nodes, + html.Element element, + String headingStyle, + ) { + final delta = Delta(); + delta.insert(element.text); + if (delta.operations.isNotEmpty) { + nodes.add(TextNode( + type: "text", + attributes: {"subtype": "heading", "heading": headingStyle}, + delta: delta)); + } + } + + _handleListElement(List nodes, html.Element element) { + final delta = Delta(); + delta.insert(element.text); + if (delta.operations.isNotEmpty) { + nodes.add(TextNode( + type: "text", attributes: {"subtype": "bullet-list"}, delta: delta)); + } + } } From 4e3e9d1a2c0a805e83647a86471377f943baa1bf Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 2 Aug 2022 16:54:10 +0800 Subject: [PATCH 6/9] feat: paste hyper link --- .../lib/infra/html_converter.dart | 58 +++++++++++++------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart index e39d082607..608020e327 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -2,6 +2,7 @@ import 'dart:collection'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flutter/foundation.dart'; import 'package:html/parser.dart' show parse; import 'package:html/dom.dart' as html; @@ -12,10 +13,24 @@ class HTMLConverter { List toNodes() { final result = []; + final delta = Delta(); - final bodyChildren = _document.body?.children ?? []; - for (final child in bodyChildren) { - _handleElement(result, child); + for (final child in _document.body?.nodes.toList() ?? []) { + if (child is html.Element) { + if (child.localName == "span") { + delta.insert(child.text); + } else if (child.localName == "strong") { + delta.insert(child.text, {"bold": true}); + } else { + _handleElement(result, child); + } + } else { + delta.insert(child.text ?? ""); + } + } + + if (delta.operations.isNotEmpty) { + result.add(TextNode(type: "text", delta: delta)); } return result; @@ -44,34 +59,43 @@ class HTMLConverter { } _handleParagraph(List nodes, html.Element element) { - for (final child in element.children) { - if (child.localName == "a") { - _handleAnchorLink(nodes, child); + final image = element.querySelector("img"); + if (image != null) { + _handleImage(nodes, image); + return; + } + + var delta = Delta(); + + for (final child in element.nodes.toList()) { + if (child is html.Element) { + if (child.localName == "a") { + final hyperLink = child.attributes["href"]; + Map? attributes; + if (hyperLink != null) { + attributes = {"href": hyperLink}; + } + delta.insert(child.text, attributes); + } else { + delta.insert(child.text); + } + } else { + delta.insert(child.text ?? ""); } } - final delta = Delta(); - delta.insert(element.text); if (delta.operations.isNotEmpty) { nodes.add(TextNode(type: "text", delta: delta)); } } - _handleAnchorLink(List nodes, html.Element element) { - for (final child in element.children) { - if (child.localName == "img") { - _handleImage(nodes, child); - return; - } - } - } - _handleImage(List nodes, html.Element element) { final src = element.attributes["src"]; final attributes = {}; if (src != null) { attributes["image_src"] = src; } + debugPrint("insert image: $src"); nodes.add( Node(type: "image", attributes: attributes, children: LinkedList())); } From aba84a3ccdbde3c047c5c0781ecde94161e0ecd6 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 3 Aug 2022 11:29:25 +0800 Subject: [PATCH 7/9] feat: paste inside the TextNode --- .../lib/operation/transaction_builder.dart | 4 ++ .../copy_paste_handler.dart | 66 +++++++++++++++---- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 88e0c00890..8fa67687c2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -80,6 +80,10 @@ class TransactionBuilder { add(TextEditOperation(path, delta, inverted)); } + setAfterSelection(Selection sel) { + afterSelection = sel; + } + mergeText(TextNode firstNode, TextNode secondNode, {int? firstOffset, int secondOffset = 0}) { final firstLength = firstNode.delta.length; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart index ca9debb2b0..4eea454605 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart @@ -46,21 +46,61 @@ _handlePastePlainText(EditorState editorState, String plainText) { return; } - final path = [...selection.end.path]; - if (path.isEmpty) { - return; - } - path[path.length - 1]++; - - final lines = - plainText.split("\n").map((e) => e.replaceAll(RegExp(r'\r'), "")); - final nodes = lines - .map((e) => TextNode(type: "text", delta: Delta().insert(e))) + final lines = plainText + .split("\n") + .map((e) => e.replaceAll(RegExp(r'\r'), "")) .toList(); - final tb = TransactionBuilder(editorState); - tb.insertNodes(path, nodes); - tb.commit(); + if (lines.isEmpty) { + return; + } else if (lines.length == 1) { + final node = + editorState.document.nodeAtPath(selection.end.path)! as TextNode; + final beginOffset = selection.end.offset; + TransactionBuilder(editorState) + ..textEdit(node, () => Delta().retain(beginOffset).insert(lines[0])) + ..setAfterSelection(Selection.collapsed(Position( + path: selection.end.path, offset: beginOffset + lines[0].length))) + ..commit(); + } else { + final firstLine = lines[0]; + final beginOffset = selection.end.offset; + final remains = lines.sublist(1); + + final path = [...selection.end.path]; + if (path.isEmpty) { + return; + } + + final node = + editorState.document.nodeAtPath(selection.end.path)! as TextNode; + final insertedLineSuffix = node.delta.slice(beginOffset); + + path[path.length - 1]++; + var index = 0; + final tb = TransactionBuilder(editorState); + final nodes = remains.map((e) { + if (index++ == remains.length - 1) { + return TextNode( + type: "text", + delta: Delta().insert(e).addAll(insertedLineSuffix.operations)); + } + return TextNode(type: "text", delta: Delta().insert(e)); + }).toList(); + // insert first line + tb.textEdit( + node, + () => Delta() + .retain(beginOffset) + .insert(firstLine) + .delete(node.delta.length - beginOffset)); + // insert remains + tb.insertNodes(path, nodes); + tb.commit(); + + editorState.updateCursorSelection(Selection.collapsed( + Position(path: nodes.last.path, offset: lines.last.length))); + } } _handleCut() { From e73465170ae0625faff5b9f4a6402776bbbf32de Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 3 Aug 2022 15:30:00 +0800 Subject: [PATCH 8/9] feat: paste html rich text inside text --- .../lib/infra/html_converter.dart | 51 ++++++++++++------- .../copy_paste_handler.dart | 27 ++++++++-- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart index 608020e327..ece4f6b9f4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -15,12 +15,13 @@ class HTMLConverter { final result = []; final delta = Delta(); - for (final child in _document.body?.nodes.toList() ?? []) { + final childNodes = _document.body?.nodes.toList() ?? []; + for (final child in childNodes) { if (child is html.Element) { - if (child.localName == "span") { - delta.insert(child.text); - } else if (child.localName == "strong") { - delta.insert(child.text, {"bold": true}); + if (child.localName == "a" || + child.localName == "span" || + child.localName == "strong") { + _handleRichTextElement(delta, child); } else { _handleElement(result, child); } @@ -59,6 +60,25 @@ class HTMLConverter { } _handleParagraph(List nodes, html.Element element) { + _handleRichText(nodes, element); + } + + _handleRichTextElement(Delta delta, html.Element element) { + if (element.localName == "span") { + delta.insert(element.text); + } else if (element.localName == "a") { + final hyperLink = element.attributes["href"]; + Map? attributes; + if (hyperLink != null) { + attributes = {"href": hyperLink}; + } + delta.insert(element.text, attributes); + } else if (element.localName == "strong") { + delta.insert(element.text, {"bold": true}); + } + } + + _handleRichText(List nodes, html.Element element) { final image = element.querySelector("img"); if (image != null) { _handleImage(nodes, image); @@ -69,13 +89,10 @@ class HTMLConverter { for (final child in element.nodes.toList()) { if (child is html.Element) { - if (child.localName == "a") { - final hyperLink = child.attributes["href"]; - Map? attributes; - if (hyperLink != null) { - attributes = {"href": hyperLink}; - } - delta.insert(child.text, attributes); + if (child.localName == "a" || + child.localName == "span" || + child.localName == "strong") { + _handleRichTextElement(delta, element); } else { delta.insert(child.text); } @@ -122,11 +139,11 @@ class HTMLConverter { } _handleListElement(List nodes, html.Element element) { - final delta = Delta(); - delta.insert(element.text); - if (delta.operations.isNotEmpty) { - nodes.add(TextNode( - type: "text", attributes: {"subtype": "bullet-list"}, delta: delta)); + final childNodes = element.nodes.toList(); + for (final child in childNodes) { + if (child is html.Element) { + _handleRichText(nodes, child); + } } } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart index 4eea454605..d7eda6b64a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart @@ -10,8 +10,6 @@ _handleCopy() async { } _pasteHTML(EditorState editorState, String html) { - final converter = HTMLConverter(html); - final nodes = converter.toNodes(); final selection = editorState.cursorSelection; if (selection == null) { return; @@ -21,9 +19,31 @@ _pasteHTML(EditorState editorState, String html) { if (path.isEmpty) { return; } - path[path.length - 1]++; + + final converter = HTMLConverter(html); + final nodes = converter.toNodes(); + + if (nodes.isEmpty) { + return; + } else if (nodes.length == 1) { + final firstNode = nodes[0]; + final nodeAtPath = editorState.document.nodeAtPath(path)!; + final tb = TransactionBuilder(editorState); + final startOffset = selection.start.offset; + if (nodeAtPath.type == "text" && firstNode.type == "text") { + final textNodeAtPath = nodeAtPath as TextNode; + final firstTextNode = firstNode as TextNode; + tb.textEdit(textNodeAtPath, + () => Delta().retain(startOffset).concat(firstTextNode.delta)); + tb.setAfterSelection(Selection.collapsed(Position( + path: path, offset: startOffset + firstTextNode.delta.length))); + } + tb.commit(); + return; + } final tb = TransactionBuilder(editorState); + path[path.length - 1]++; tb.insertNodes(path, nodes); tb.commit(); } @@ -98,6 +118,7 @@ _handlePastePlainText(EditorState editorState, String plainText) { tb.insertNodes(path, nodes); tb.commit(); + // fixme: don't set the cursor manually editorState.updateCursorSelection(Selection.collapsed( Position(path: nodes.last.path, offset: lines.last.length))); } From 290435b0eea0a8a6cb4acd76c50c39ab39197540 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 3 Aug 2022 16:06:07 +0800 Subject: [PATCH 9/9] feat: paste text inside text --- .../copy_paste_handler.dart | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart index d7eda6b64a..6e6e30dbf4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart @@ -37,12 +37,47 @@ _pasteHTML(EditorState editorState, String html) { () => Delta().retain(startOffset).concat(firstTextNode.delta)); tb.setAfterSelection(Selection.collapsed(Position( path: path, offset: startOffset + firstTextNode.delta.length))); + tb.commit(); } + } + + _pasteMultipleLinesInText(editorState, path, selection.start.offset, nodes); +} + +_pasteMultipleLinesInText( + EditorState editorState, List path, int offset, List nodes) { + final tb = TransactionBuilder(editorState); + + final firstNode = nodes[0]; + final nodeAtPath = editorState.document.nodeAtPath(path)!; + + if (nodeAtPath.type == "text" && firstNode.type == "text") { + // split and merge + final textNodeAtPath = nodeAtPath as TextNode; + final firstTextNode = firstNode as TextNode; + final remain = textNodeAtPath.delta.slice(offset); + + tb.textEdit( + textNodeAtPath, + () => Delta() + .retain(offset) + .delete(remain.length) + .concat(firstTextNode.delta)); + + path[path.length - 1]++; + final tailNodes = nodes.sublist(1); + if (tailNodes.last.type == "text") { + final tailTextNode = tailNodes.last as TextNode; + tailTextNode.delta = tailTextNode.delta.concat(remain); + } else if (remain.length > 0) { + tailNodes.add(TextNode(type: "text", delta: remain)); + } + + tb.insertNodes(path, tailNodes); tb.commit(); return; } - final tb = TransactionBuilder(editorState); path[path.length - 1]++; tb.insertNodes(path, nodes); tb.commit();