From 4b7c997083912c519fe6e4b941df6f11f720164b Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 18 Jul 2022 15:49:39 +0800 Subject: [PATCH] feat: text delta to text span --- .../flowy_editor/example/assets/document.json | 27 ++++--- .../example/lib/plugin/text_node_widget.dart | 60 ++++++++++++++-- .../flowy_editor/lib/document/node.dart | 34 +++++++-- .../flowy_editor/lib/document/text_delta.dart | 70 ++++++++++++++----- .../flowy_editor/lib/document/text_node.dart | 13 ---- 5 files changed, 154 insertions(+), 50 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index 2ddedf70b3..04a9e0cda8 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/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, diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 59e466c92e..7ce67fcdf6 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/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 toTextSpans() { + final result = []; + + 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( 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(); 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 cf706df856..ba4cb10525 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/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 { ); } - final node = Node( - type: jType, - children: children, - attributes: jAttributes, - ); + Node node; + + if (jType == "text") { + final jDelta = json['delta'] as List?; + 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 { 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, + }); +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index bbf8e20a68..b87ca71b95 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/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 rest() { @@ -221,10 +215,52 @@ class _OpIterator { } } +Attributes? _attributesFromJSON(Map? json) { + if (json == null) { + return null; + } + final result = {}; + + for (final entry in json.entries) { + result[entry.key] = entry.value; + } + + return result; +} + +TextOperation? _textOperationFromJson(Map json) { + TextOperation? result; + + if (json['insert'] is String) { + result = TextInsert(json['insert'] as String, + _attributesFromJSON(json['attributes'] as Map?)); + } else if (json['retain'] is int) { + result = TextRetain(json['retain'] as int, + _attributesFromJSON(json['attributes'] as Map?)); + } 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 operations; + factory Delta.fromJson(List list) { + final operations = []; + + for (final obj in list) { + final op = _textOperationFromJson(obj as Map); + if (op != null) { + operations.add(op); + } + } + + return Delta(operations); + } + Delta([List? ops]) : operations = ops ?? []; 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); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart deleted file mode 100644 index 535431b615..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_node.dart +++ /dev/null @@ -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, - }); -}