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": [
{
"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": {
"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,

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: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();

View File

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

View File

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

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