mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: text delta to text span
This commit is contained in:
parent
29aafbaac4
commit
4b7c997083
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user