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": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"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": {
|
"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,
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
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,
|
type: jType,
|
||||||
children: children,
|
children: children,
|
||||||
attributes: jAttributes,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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