فهرست منبع

feat: text delta to text span

Vincent Chan 2 سال پیش
والد
کامیت
4b7c997083

+ 17 - 10
frontend/app_flowy/packages/flowy_editor/example/assets/document.json

@@ -7,39 +7,46 @@
       "children": [
         {
           "type": "text",
+          "delta": [
+            { "insert": "With " },
+            { "insert": "AppFlowy", "attributes": { "href": "https://www.appflowy.io/" } },
+            { "insert": ", you can build detailed lists of to-do’s for different projects while tracking the status of each one" }
+          ],
           "attributes": {
             "subtype": "with-checkbox",
             "font-size": 30,
-            "content": "aaaaaaaaaaaaaaaaaaaaaaaa",
             "checkbox": false
           }
         },
         {
           "type": "text",
+          "delta": [
+            { "insert": "You can " },
+            { "insert": "host", "attributes": { "italic": true } },
+            { "insert": " " },
+            { "insert": "AppFlowy", "attributes": { "bold": true } },
+            { "insert": " " },
+            { "insert": "wherever you want", "attributes": { "underline": true }},
+            { "insert": "; no vendor lock-in." }
+          ],
           "attributes": {
             "subtype": "with-checkbox",
             "text-type": "heading1",
             "font-size": 30,
-            "content": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
             "checkbox": false
           }
         },
         {
           "type": "text",
+          "delta": [{ "insert": "Design and modify AppFlowy your way with an open core codebase." }],
           "attributes": {
             "text-type": "heading1",
-            "font-size": 30,
-            "content": "cccccccccccccccccccccc"
-          }
-        },
-        {
-          "type": "image",
-          "attributes": {
-            "image_src": "https://images.pexels.com/photos/12499889/pexels-photo-12499889.jpeg?fm=jpg&w=640&h=427"
+            "font-size": 30
           }
         },
         {
           "type": "text",
+          "delta": [{ "insert": "AppFlowy is built with Flutter and Rust. What does this mean? Faster development, better native experience, and more reliable performance." }],
           "attributes": {
             "text-type": "heading1",
             "font-size": 30,

+ 55 - 5
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart

@@ -1,3 +1,5 @@
+import 'package:flowy_editor/document/text_delta.dart';
+import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flutter/services.dart';
@@ -10,7 +12,7 @@ class TextNodeBuilder extends NodeWidgetBuilder {
     required super.editorState,
   }) : super.create() {
     nodeValidator = ((node) {
-      return node.type == 'text' && node.attributes.containsKey('content');
+      return node.type == 'text';
     });
   }
 
@@ -31,6 +33,55 @@ extension on Attributes {
   }
 }
 
+TextSpan _textInsertToTextSpan(TextInsert textInsert) {
+  FontWeight? fontWeight;
+  FontStyle? fontStyle;
+  TextDecoration? decoration;
+  GestureRecognizer? gestureRecognizer;
+  Color? color;
+  final attributes = textInsert.attributes;
+  if (attributes?['bold'] == true) {
+    fontWeight = FontWeight.bold;
+  }
+  if (attributes?['italic'] == true) {
+    fontStyle = FontStyle.italic;
+  }
+  if (attributes?["underline"] == true) {
+    decoration = TextDecoration.underline;
+  }
+  if (attributes?["href"] is String) {
+    color = const Color.fromARGB(255, 55, 120, 245);
+    decoration = TextDecoration.underline;
+    gestureRecognizer = TapGestureRecognizer()
+      ..onTap = () {
+        // TODO: open the link
+      };
+  }
+  return TextSpan(
+      text: textInsert.content,
+      style: TextStyle(
+        fontWeight: fontWeight,
+        fontStyle: fontStyle,
+        decoration: decoration,
+        color: color,
+      ),
+      recognizer: gestureRecognizer);
+}
+
+extension on TextNode {
+  List<TextSpan> toTextSpans() {
+    final result = <TextSpan>[];
+
+    for (final op in delta.operations) {
+      if (op is TextInsert) {
+        result.add(_textInsertToTextSpan(op));
+      }
+    }
+
+    return result;
+  }
+}
+
 class _TextNodeWidget extends StatefulWidget {
   final Node node;
   final EditorState editorState;
@@ -49,8 +100,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
     implements TextInputClient {
   Node get node => widget.node;
   EditorState get editorState => widget.editorState;
-  String get content => node.attributes['content'] as String;
-  TextEditingValue get textEditingValue => TextEditingValue(text: content);
+  TextEditingValue get textEditingValue => const TextEditingValue();
 
   TextInputConnection? _textInputConnection;
 
@@ -60,13 +110,13 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
       value: node,
       builder: (_, __) => Consumer<Node>(
         builder: ((context, value, child) {
+          final textNode = value as TextNode;
           return Column(
             crossAxisAlignment: CrossAxisAlignment.start,
             children: [
               SelectableText.rich(
                 TextSpan(
-                  text: content,
-                  style: node.attributes.toTextStyle(),
+                  children: textNode.toTextSpans(),
                 ),
                 onTap: () {
                   _textInputConnection?.close();

+ 29 - 5
frontend/app_flowy/packages/flowy_editor/lib/document/node.dart

@@ -1,5 +1,6 @@
 import 'dart:collection';
 import 'package:flowy_editor/document/path.dart';
+import 'package:flowy_editor/document/text_delta.dart';
 import 'package:flutter/material.dart';
 import './attributes.dart';
 
@@ -49,11 +50,23 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
       );
     }
 
-    final node = Node(
-      type: jType,
-      children: children,
-      attributes: jAttributes,
-    );
+    Node node;
+
+    if (jType == "text") {
+      final jDelta = json['delta'] as List<dynamic>?;
+      final delta = jDelta == null ? Delta() : Delta.fromJson(jDelta);
+      node = TextNode(
+          type: jType,
+          children: children,
+          attributes: jAttributes,
+          delta: delta);
+    } else {
+      node = Node(
+        type: jType,
+        children: children,
+        attributes: jAttributes,
+      );
+    }
 
     for (final child in children) {
       child.parent = node;
@@ -144,3 +157,14 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
     return parent!._path([index, ...previous]);
   }
 }
+
+class TextNode extends Node {
+  final Delta delta;
+
+  TextNode({
+    required super.type,
+    required super.children,
+    required super.attributes,
+    required this.delta,
+  });
+}

+ 53 - 17
frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart

@@ -60,10 +60,8 @@ class TextRetain extends TextOperation {
   int _length;
   final Attributes? _attributes;
 
-  TextRetain({
-    required length,
-    attributes,
-  })  : _length = length,
+  TextRetain(length, [Attributes? attributes])
+      : _length = length,
         _attributes = attributes;
 
   @override
@@ -103,9 +101,7 @@ class TextRetain extends TextOperation {
 class TextDelete extends TextOperation {
   int _length;
 
-  TextDelete({
-    required int length,
-  }) : _length = length;
+  TextDelete(int length) : _length = length;
 
   @override
   bool get isEmpty {
@@ -167,7 +163,7 @@ class _OpIterator {
     length ??= _maxInt;
 
     if (_index >= _operations.length) {
-      return TextRetain(length: _maxInt);
+      return TextRetain(_maxInt);
     }
 
     final nextOp = _operations[_index];
@@ -182,15 +178,13 @@ class _OpIterator {
       _offset += length;
     }
     if (nextOp is TextDelete) {
-      return TextDelete(
-        length: length,
-      );
+      return TextDelete(length);
     }
 
     if (nextOp is TextRetain) {
       return TextRetain(
-        length: length,
-        attributes: nextOp.attributes,
+        length,
+        nextOp.attributes,
       );
     }
 
@@ -201,7 +195,7 @@ class _OpIterator {
       );
     }
 
-    return TextRetain(length: _maxInt);
+    return TextRetain(_maxInt);
   }
 
   List<TextOperation> rest() {
@@ -221,10 +215,52 @@ class _OpIterator {
   }
 }
 
+Attributes? _attributesFromJSON(Map<String, dynamic>? json) {
+  if (json == null) {
+    return null;
+  }
+  final result = <String, dynamic>{};
+
+  for (final entry in json.entries) {
+    result[entry.key] = entry.value;
+  }
+
+  return result;
+}
+
+TextOperation? _textOperationFromJson(Map<String, dynamic> json) {
+  TextOperation? result;
+
+  if (json['insert'] is String) {
+    result = TextInsert(json['insert'] as String,
+        _attributesFromJSON(json['attributes'] as Map<String, dynamic>?));
+  } else if (json['retain'] is int) {
+    result = TextRetain(json['retain'] as int,
+        _attributesFromJSON(json['attributes'] as Map<String, Object>?));
+  } else if (json['delete'] is int) {
+    result = TextDelete(json['delete'] as int);
+  }
+
+  return result;
+}
+
 // basically copy from: https://github.com/quilljs/delta
 class Delta {
   final List<TextOperation> operations;
 
+  factory Delta.fromJson(List<dynamic> list) {
+    final operations = <TextOperation>[];
+
+    for (final obj in list) {
+      final op = _textOperationFromJson(obj as Map<String, dynamic>);
+      if (op != null) {
+        operations.add(op);
+      }
+    }
+
+    return Delta(operations);
+  }
+
   Delta([List<TextOperation>? ops]) : operations = ops ?? <TextOperation>[];
 
   Delta add(TextOperation textOp) {
@@ -288,12 +324,12 @@ class Delta {
   }
 
   Delta retain(int length, [Attributes? attributes]) {
-    final op = TextRetain(length: length, attributes: attributes);
+    final op = TextRetain(length, attributes);
     return add(op);
   }
 
   Delta delete(int length) {
-    final op = TextDelete(length: length);
+    final op = TextDelete(length);
     return add(op);
   }
 
@@ -341,7 +377,7 @@ class Delta {
         if (otherOp is TextRetain && otherOp.length > 0) {
           TextOperation? newOp;
           if (thisOp is TextRetain) {
-            newOp = TextRetain(length: length, attributes: attributes);
+            newOp = TextRetain(length, attributes);
           } else if (thisOp is TextInsert) {
             newOp = TextInsert(thisOp.content, attributes);
           }

+ 0 - 13
frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart

@@ -1,13 +0,0 @@
-import './text_delta.dart';
-import './node.dart';
-
-class TextNode extends Node {
-  final Delta delta;
-
-  TextNode({
-    required super.type,
-    required super.children,
-    required super.attributes,
-    required this.delta,
-  });
-}