feat: text delta to text span

This commit is contained in:
Vincent Chan 2022-07-18 15:49:39 +08:00
parent 29aafbaac4
commit 4b7c997083
5 changed files with 154 additions and 50 deletions

View File

@ -7,39 +7,46 @@
"children": [ "children": [
{ {
"type": "text", "type": "text",
"delta": [
{ "insert": "With " },
{ "insert": "AppFlowy", "attributes": { "href": "https://www.appflowy.io/" } },
{ "insert": ", you can build detailed lists of to-dos for different projects while tracking the status of each one" }
],
"attributes": { "attributes": {
"subtype": "with-checkbox", "subtype": "with-checkbox",
"font-size": 30, "font-size": 30,
"content": "aaaaaaaaaaaaaaaaaaaaaaaa",
"checkbox": false "checkbox": false
} }
}, },
{ {
"type": "text", "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": { "attributes": {
"subtype": "with-checkbox", "subtype": "with-checkbox",
"text-type": "heading1", "text-type": "heading1",
"font-size": 30, "font-size": 30,
"content": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"checkbox": false "checkbox": false
} }
}, },
{ {
"type": "text", "type": "text",
"delta": [{ "insert": "Design and modify AppFlowy your way with an open core codebase." }],
"attributes": { "attributes": {
"text-type": "heading1", "text-type": "heading1",
"font-size": 30, "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"
} }
}, },
{ {
"type": "text", "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": { "attributes": {
"text-type": "heading1", "text-type": "heading1",
"font-size": 30, "font-size": 30,

View File

@ -1,3 +1,5 @@
import 'package:flowy_editor/document/text_delta.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/flowy_editor.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -10,7 +12,7 @@ class TextNodeBuilder extends NodeWidgetBuilder {
required super.editorState, required super.editorState,
}) : super.create() { }) : super.create() {
nodeValidator = ((node) { 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 { class _TextNodeWidget extends StatefulWidget {
final Node node; final Node node;
final EditorState editorState; final EditorState editorState;
@ -49,8 +100,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
implements TextInputClient { implements TextInputClient {
Node get node => widget.node; Node get node => widget.node;
EditorState get editorState => widget.editorState; EditorState get editorState => widget.editorState;
String get content => node.attributes['content'] as String; TextEditingValue get textEditingValue => const TextEditingValue();
TextEditingValue get textEditingValue => TextEditingValue(text: content);
TextInputConnection? _textInputConnection; TextInputConnection? _textInputConnection;
@ -60,13 +110,13 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
value: node, value: node,
builder: (_, __) => Consumer<Node>( builder: (_, __) => Consumer<Node>(
builder: ((context, value, child) { builder: ((context, value, child) {
final textNode = value as TextNode;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SelectableText.rich( SelectableText.rich(
TextSpan( TextSpan(
text: content, children: textNode.toTextSpans(),
style: node.attributes.toTextStyle(),
), ),
onTap: () { onTap: () {
_textInputConnection?.close(); _textInputConnection?.close();

View File

@ -1,5 +1,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/path.dart';
import 'package:flowy_editor/document/text_delta.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import './attributes.dart'; import './attributes.dart';
@ -49,11 +50,23 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
); );
} }
final node = Node( Node node;
type: jType,
children: children, if (jType == "text") {
attributes: jAttributes, 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) { for (final child in children) {
child.parent = node; child.parent = node;
@ -144,3 +157,14 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
return parent!._path([index, ...previous]); 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,
});
}

View File

@ -60,10 +60,8 @@ class TextRetain extends TextOperation {
int _length; int _length;
final Attributes? _attributes; final Attributes? _attributes;
TextRetain({ TextRetain(length, [Attributes? attributes])
required length, : _length = length,
attributes,
}) : _length = length,
_attributes = attributes; _attributes = attributes;
@override @override
@ -103,9 +101,7 @@ class TextRetain extends TextOperation {
class TextDelete extends TextOperation { class TextDelete extends TextOperation {
int _length; int _length;
TextDelete({ TextDelete(int length) : _length = length;
required int length,
}) : _length = length;
@override @override
bool get isEmpty { bool get isEmpty {
@ -167,7 +163,7 @@ class _OpIterator {
length ??= _maxInt; length ??= _maxInt;
if (_index >= _operations.length) { if (_index >= _operations.length) {
return TextRetain(length: _maxInt); return TextRetain(_maxInt);
} }
final nextOp = _operations[_index]; final nextOp = _operations[_index];
@ -182,15 +178,13 @@ class _OpIterator {
_offset += length; _offset += length;
} }
if (nextOp is TextDelete) { if (nextOp is TextDelete) {
return TextDelete( return TextDelete(length);
length: length,
);
} }
if (nextOp is TextRetain) { if (nextOp is TextRetain) {
return TextRetain( return TextRetain(
length: length, length,
attributes: nextOp.attributes, nextOp.attributes,
); );
} }
@ -201,7 +195,7 @@ class _OpIterator {
); );
} }
return TextRetain(length: _maxInt); return TextRetain(_maxInt);
} }
List<TextOperation> rest() { 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 // basically copy from: https://github.com/quilljs/delta
class Delta { class Delta {
final List<TextOperation> operations; 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([List<TextOperation>? ops]) : operations = ops ?? <TextOperation>[];
Delta add(TextOperation textOp) { Delta add(TextOperation textOp) {
@ -288,12 +324,12 @@ class Delta {
} }
Delta retain(int length, [Attributes? attributes]) { Delta retain(int length, [Attributes? attributes]) {
final op = TextRetain(length: length, attributes: attributes); final op = TextRetain(length, attributes);
return add(op); return add(op);
} }
Delta delete(int length) { Delta delete(int length) {
final op = TextDelete(length: length); final op = TextDelete(length);
return add(op); return add(op);
} }
@ -341,7 +377,7 @@ class Delta {
if (otherOp is TextRetain && otherOp.length > 0) { if (otherOp is TextRetain && otherOp.length > 0) {
TextOperation? newOp; TextOperation? newOp;
if (thisOp is TextRetain) { if (thisOp is TextRetain) {
newOp = TextRetain(length: length, attributes: attributes); newOp = TextRetain(length, attributes);
} else if (thisOp is TextInsert) { } else if (thisOp is TextInsert) {
newOp = TextInsert(thisOp.content, attributes); newOp = TextInsert(thisOp.content, attributes);
} }

View File

@ -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,
});
}