mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: add checkbox style
This commit is contained in:
parent
51bc965029
commit
fce8ea1e80
@ -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. AppFlowy’s 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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 ?? {},
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user