feat: add checkbox style

This commit is contained in:
Lucas.Xu 2022-07-28 23:04:51 +08:00
parent 51bc965029
commit fce8ea1e80
5 changed files with 157 additions and 228 deletions

View File

@ -6,7 +6,7 @@
{
"type": "image",
"attributes": {
"image_src": "https://images.pexels.com/photos/2253275/pexels-photo-2253275.jpeg?cs=srgb&dl=pexels-helena-lopes-2253275.jpg&fm=jpg"
"image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png"
}
},
{
@ -37,57 +37,7 @@
"type": "text",
"delta": [
{
"insert": "Here are the plugin demos:"
}
],
"attributes": {
"subtype": "heading",
"heading": "h3"
}
},
{
"type": "text",
"delta": [
{
"insert": "Checkbox example ......"
}
],
"attributes": {
"subtype": "checkbox",
"checkbox": false
},
"children": [
{
"type": "text",
"delta": [
{
"insert": "AAA Checkbox example ......\nAAA Checkbox example ......"
}
],
"attributes": {
"subtype": "checkbox",
"checkbox": false
}
},
{
"type": "text",
"delta": [
{
"insert": "BBB Checkbox example ......"
}
],
"attributes": {
"subtype": "checkbox",
"checkbox": true
}
}
]
},
{
"type": "text",
"delta": [
{
"insert": "Raw text example ......"
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowys five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
}
]
},
@ -109,11 +59,7 @@
{ "insert": "Click " },
{ "insert": "anywhere", "attributes": { "underline": true } },
{ "insert": " and just typing." }
],
"attributes": {
"subtype": "checkbox",
"checkbox": true
}
]
},
{
"type": "text",
@ -128,11 +74,7 @@
{
"insert": "to see all the types of content you can add - entity, headers, videos, sub pages, etc."
}
],
"attributes": {
"subtype": "checkbox",
"checkbox": true
}
]
},
{
"type": "text",
@ -144,17 +86,13 @@
{ "insert": " your ", "attributes": { "italic": true } },
{ "insert": "writing", "attributes": { "strikethrough": true } },
{ "insert": "." }
],
"attributes": {
"subtype": "checkbox",
"checkbox": true
}
]
},
{
"type": "text",
"delta": [
{
"insert": "Here are the examples:"
"insert": "Here are the plugins:"
}
],
"attributes": {
@ -162,6 +100,42 @@
"heading": "h3"
}
},
{
"type": "text",
"delta": [
{
"insert": "Hello world"
}
],
"attributes": {
"subtype": "checkbox",
"checkbox": false
}
},
{
"type": "text",
"delta": [
{
"insert": "Hello world"
}
],
"attributes": {
"subtype": "checkbox",
"checkbox": false
}
},
{
"type": "text",
"delta": [
{
"insert": "Hello world"
}
],
"attributes": {
"subtype": "checkbox",
"checkbox": false
}
},
{
"type": "text",
"delta": [
@ -203,7 +177,7 @@
}
],
"attributes": {
"quote": true
"subtype": "quote"
}
},
{
@ -214,7 +188,18 @@
}
],
"attributes": {
"quote": true
"subtype": "quote"
}
},
{
"type": "text",
"delta": [
{
"insert": "Hello world"
}
],
"attributes": {
"subtype": "quote"
}
},
{

View File

@ -85,23 +85,8 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
children: [
Image.network(
src,
height: 150.0,
),
if (node.children.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: node.children
.map(
(e) => editorState.renderPlugins.buildWidget(
context: NodeWidgetContext(
buildContext: context,
node: e,
editorState: editorState,
),
),
)
.toList(),
),
width: MediaQuery.of(context).size.width,
)
],
);
}

View File

@ -4,6 +4,7 @@ import 'package:flowy_editor/render/rich_text/checkbox_text.dart';
import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
import 'package:flowy_editor/render/rich_text/heading_text.dart';
import 'package:flowy_editor/render/rich_text/number_list_text.dart';
import 'package:flowy_editor/render/rich_text/quoted_text.dart';
import 'package:flowy_editor/service/service.dart';
import 'package:flutter/material.dart';
@ -53,6 +54,7 @@ class EditorState {
'text/bullet-list', BulletedListTextNodeWidgetBuilder.create);
renderPlugins.register(
'text/number-list', NumberListTextNodeWidgetBuilder.create);
renderPlugins.register('text/quote', QuotedTextNodeWidgetBuilder.create);
undoManager.state = this;
}

View File

@ -4,12 +4,9 @@ import 'package:flowy_editor/document/selection.dart';
import 'package:flowy_editor/document/text_delta.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/document/path.dart';
import 'package:flowy_editor/operation/transaction_builder.dart';
import 'package:flowy_editor/render/node_widget_builder.dart';
import 'package:flowy_editor/render/render_plugins.dart';
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
import 'package:flowy_editor/infra/flowy_svg.dart';
import 'package:flowy_editor/extensions/object_extensions.dart';
import 'package:flowy_editor/render/selection/selectable.dart';
import 'package:flutter/material.dart';
@ -56,37 +53,21 @@ class FlowyRichText extends StatefulWidget {
class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
final _textKey = GlobalKey();
final _decorationKey = GlobalKey();
EditorState get _editorState => widget.editorState;
TextNode get _textNode => widget.textNode;
RenderParagraph get _renderParagraph =>
_textKey.currentContext?.findRenderObject() as RenderParagraph;
@override
Widget build(BuildContext context) {
final attributes = _textNode.attributes;
// TODO: use factory method ??
if (attributes.list == 'todo') {
// return _buildTodoListRichText(context);
} else if (attributes.list == 'bullet') {
// return _buildBulletedListRichText(context);
} else if (attributes.quote == true) {
return _buildQuotedRichText(context);
} else if (attributes.heading != null) {
// return _buildHeadingRichText(context);
} else if (attributes.number != null) {
// return _buildNumberListRichText(context);
}
return _buildRichText(context);
}
@override
Position start() => Position(path: _textNode.path, offset: 0);
Position start() => Position(path: widget.textNode.path, offset: 0);
@override
Position end() =>
Position(path: _textNode.path, offset: _textNode.toRawString().length);
Position end() => Position(
path: widget.textNode.path, offset: widget.textNode.toRawString().length);
@override
Rect getCursorRectInPosition(Position position) {
@ -108,23 +89,22 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
Position getPositionInOffset(Offset start) {
final offset = _renderParagraph.globalToLocal(start);
final baseOffset = _renderParagraph.getPositionForOffset(offset).offset;
return Position(path: _textNode.path, offset: baseOffset);
return Position(path: widget.textNode.path, offset: baseOffset);
}
@override
List<Rect> getRectsInSelection(Selection selection) {
assert(pathEquals(selection.start.path, selection.end.path) &&
pathEquals(selection.start.path, _textNode.path));
pathEquals(selection.start.path, widget.textNode.path));
final textSelection = TextSelection(
baseOffset: selection.start.offset,
extentOffset: selection.end.offset,
);
final baseRect = frontWidgetRect();
return _renderParagraph.getBoxesForSelection(textSelection).map((box) {
final rect = box.toRect();
return rect.translate(baseRect.centerRight.dx, 0);
}).toList();
return _renderParagraph
.getBoxesForSelection(textSelection)
.map((box) => box.toRect())
.toList();
}
@override
@ -134,7 +114,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
final baseOffset = _renderParagraph.getPositionForOffset(localStart).offset;
final extentOffset = _renderParagraph.getPositionForOffset(localEnd).offset;
return Selection.single(
path: _textNode.path,
path: widget.textNode.path,
startOffset: baseOffset,
endOffset: extentOffset,
);
@ -144,18 +124,29 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
return _buildSingleRichText(context);
}
Widget _buildSingleRichText(BuildContext context) {
final textSpan = _textSpan;
return RichText(
key: _textKey,
text: widget.textSpanDecorator != null
? widget.textSpanDecorator!(textSpan)
: textSpan,
);
}
// unused now.
Widget _buildRichTextWithChildren(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSingleRichText(context),
..._textNode.children
...widget.textNode.children
.map(
(child) => _editorState.renderPlugins.buildWidget(
(child) => widget.editorState.renderPlugins.buildWidget(
context: NodeWidgetContext(
buildContext: context,
node: child,
editorState: _editorState,
editorState: widget.editorState,
),
),
)
@ -164,115 +155,8 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
);
}
Widget _buildSingleRichText(BuildContext context) {
return RichText(
key: _textKey,
text: widget.textSpanDecorator != null
? widget.textSpanDecorator!(_decorateTextSpanWithGlobalStyle)
: _decorateTextSpanWithGlobalStyle,
);
}
Widget _buildTodoListRichText(BuildContext context) {
final name = _textNode.attributes.todo ? 'check' : 'uncheck';
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
child: FlowySvg(
key: _decorationKey,
name: name,
),
onTap: () => TransactionBuilder(_editorState)
..updateNode(_textNode, {
'todo': !_textNode.attributes.todo,
})
..commit(),
),
_buildRichText(context),
],
);
}
Widget _buildBulletedListRichText(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FlowySvg(
key: _decorationKey,
name: 'point',
),
_buildRichText(context),
],
);
}
Widget _buildNumberListRichText(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FlowySvg(
key: _decorationKey,
number: _textNode.attributes.number,
),
_buildRichText(context),
],
);
}
Widget _buildQuotedRichText(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowySvg(
key: _decorationKey,
name: 'quote',
),
_buildRichText(context),
],
);
}
Widget _buildHeadingRichText(BuildContext context) {
// TODO: customize
return Column(
children: [
const Padding(padding: EdgeInsets.only(top: 5)),
_buildRichText(context),
const Padding(padding: EdgeInsets.only(top: 5)),
],
);
}
Rect frontWidgetRect() {
// FIXME: find a more elegant way to solve this situation.
final renderBox = _decorationKey.currentContext
?.findRenderObject()
?.unwrapOrNull<RenderBox>();
if (renderBox != null) {
return renderBox.localToGlobal(Offset.zero) & renderBox.size;
}
return Rect.zero;
}
TextSpan get _decorateTextSpanWithGlobalStyle => TextSpan(
children: _textSpan.children
?.whereType<TextSpan>()
.map(
(span) => TextSpan(
text: span.text,
style: span.style?.copyWith(
fontSize: _textNode.attributes.fontSize,
color: _textNode.attributes.quoteColor,
),
recognizer: span.recognizer,
),
)
.toList(),
);
TextSpan get _textSpan => TextSpan(
children: _textNode.delta.operations
children: widget.textNode.delta.operations
.whereType<TextInsert>()
.map((insert) => RichTextStyle(
attributes: insert.attributes ?? {},

View File

@ -0,0 +1,73 @@
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/infra/flowy_svg.dart';
import 'package:flowy_editor/render/node_widget_builder.dart';
import 'package:flowy_editor/render/rich_text/default_selectable.dart';
import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
import 'package:flowy_editor/render/selection/selectable.dart';
import 'package:flutter/material.dart';
class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder {
QuotedTextNodeWidgetBuilder.create({
required super.editorState,
required super.node,
required super.key,
}) : super.create();
@override
Widget build(BuildContext context) {
return QuotedTextNodeWidget(
key: key,
textNode: node as TextNode,
editorState: editorState,
);
}
}
class QuotedTextNodeWidget extends StatefulWidget {
const QuotedTextNodeWidget({
Key? key,
required this.textNode,
required this.editorState,
}) : super(key: key);
final TextNode textNode;
final EditorState editorState;
@override
State<QuotedTextNodeWidget> createState() => _QuotedTextNodeWidgetState();
}
// customize
class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
with Selectable, DefaultSelectable {
final _richTextKey = GlobalKey(debugLabel: 'quoted_text');
final leftPadding = 20.0;
@override
Selectable<StatefulWidget> get forward =>
_richTextKey.currentState as Selectable;
@override
Offset get baseOffset {
return Offset(leftPadding, 0);
}
@override
Widget build(BuildContext context) {
return Row(
children: [
const FlowySvg(
name: 'quote',
),
FlowyRichText(
key: _richTextKey,
textNode: widget.textNode,
editorState: widget.editorState,
),
],
);
}
}