feat: support customizing editor edges

This commit is contained in:
Lucas.Xu 2022-08-29 15:56:33 +08:00
commit 061a7d2af4
14 changed files with 172 additions and 157 deletions

View file

@ -46,13 +46,7 @@ class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
body: Center( body: _buildEditor(context),
child: Container(
width: 780,
alignment: Alignment.topCenter,
child: _buildEditor(context),
),
),
floatingActionButton: _buildExpandableFab(), floatingActionButton: _buildExpandableFab(),
); );
} }
@ -100,6 +94,7 @@ class _MyHomePageState extends State<MyHomePage> {
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
child: AppFlowyEditor( child: AppFlowyEditor(
editorState: _editorState, editorState: _editorState,
editorStyle: const EditorStyle.defaultStyle(),
), ),
); );
} else { } else {

View file

@ -2,6 +2,7 @@
library appflowy_editor; library appflowy_editor;
export 'src/infra/log.dart'; export 'src/infra/log.dart';
export 'src/render/style/editor_style.dart';
export 'src/document/node.dart'; export 'src/document/node.dart';
export 'src/document/path.dart'; export 'src/document/path.dart';
export 'src/document/position.dart'; export 'src/document/position.dart';

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:appflowy_editor/src/infra/log.dart'; import 'package:appflowy_editor/src/infra/log.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
import 'package:appflowy_editor/src/render/style/editor_style.dart';
import 'package:appflowy_editor/src/service/service.dart'; import 'package:appflowy_editor/src/service/service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -58,6 +59,9 @@ class EditorState {
/// Stores the selection menu items. /// Stores the selection menu items.
List<SelectionMenuItem> selectionMenuItems = []; List<SelectionMenuItem> selectionMenuItems = [];
/// Stores the editor style.
EditorStyle editorStyle = const EditorStyle.defaultStyle();
final UndoManager undoManager = UndoManager(); final UndoManager undoManager = UndoManager();
Selection? _cursorSelection; Selection? _cursorSelection;

View file

@ -1,10 +1,8 @@
import 'dart:math'; import 'package:appflowy_editor/src/extensions/object_extensions.dart';
import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/document/position.dart'; import 'package:appflowy_editor/src/document/position.dart';
import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/document/selection.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -35,6 +33,8 @@ class ImageNodeWidget extends StatefulWidget {
} }
class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable { class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable {
final _imageKey = GlobalKey();
double? _imageWidth; double? _imageWidth;
double _initial = 0; double _initial = 0;
double _distance = 0; double _distance = 0;
@ -50,8 +50,11 @@ class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable {
_imageWidth = widget.width; _imageWidth = widget.width;
_imageStreamListener = ImageStreamListener( _imageStreamListener = ImageStreamListener(
(image, _) { (image, _) {
_imageWidth = _imageWidth = _imageKey.currentContext
min(defaultMaxTextNodeWidth, image.image.width.toDouble()); ?.findRenderObject()
?.unwrapOrNull<RenderBox>()
?.size
.width;
}, },
); );
} }
@ -65,9 +68,8 @@ class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// only support network image. // only support network image.
return Container( return Container(
width: defaultMaxTextNodeWidth, key: _imageKey,
padding: const EdgeInsets.only(top: 8, bottom: 8), padding: const EdgeInsets.only(top: 8, bottom: 8),
child: _buildNetworkImage(context), child: _buildNetworkImage(context),
); );
@ -137,7 +139,7 @@ class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable {
loadingBuilder: (context, child, loadingProgress) => loadingBuilder: (context, child, loadingProgress) =>
loadingProgress == null ? child : _buildLoading(context), loadingProgress == null ? child : _buildLoading(context),
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
_imageWidth ??= defaultMaxTextNodeWidth; // _imageWidth ??= defaultMaxTextNodeWidth;
return _buildError(context); return _buildError(context);
}, },
); );

View file

@ -56,30 +56,27 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Padding(
constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), padding: EdgeInsets.only(bottom: defaultLinePadding),
child: Padding( child: Row(
padding: EdgeInsets.only(bottom: defaultLinePadding), crossAxisAlignment: CrossAxisAlignment.start,
child: Row( children: [
crossAxisAlignment: CrossAxisAlignment.start, FlowySvg(
children: [ key: iconKey,
FlowySvg( width: _iconWidth,
key: iconKey, height: _iconWidth,
width: _iconWidth, padding: EdgeInsets.only(right: _iconRightPadding),
height: _iconWidth, name: 'point',
padding: EdgeInsets.only(right: _iconRightPadding), ),
name: 'point', Flexible(
child: FlowyRichText(
key: _richTextKey,
placeholderText: 'List',
textNode: widget.textNode,
editorState: widget.editorState,
), ),
Flexible( )
child: FlowyRichText( ],
key: _richTextKey,
placeholderText: 'List',
textNode: widget.textNode,
editorState: widget.editorState,
),
)
],
),
), ),
); );
} }

View file

@ -63,41 +63,38 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
Widget _buildWithSingle(BuildContext context) { Widget _buildWithSingle(BuildContext context) {
final check = widget.textNode.attributes.check; final check = widget.textNode.attributes.check;
return SizedBox( return Padding(
width: defaultMaxTextNodeWidth, padding: EdgeInsets.only(bottom: defaultLinePadding),
child: Padding( child: Row(
padding: EdgeInsets.only(bottom: defaultLinePadding), crossAxisAlignment: CrossAxisAlignment.start,
child: Row( children: [
crossAxisAlignment: CrossAxisAlignment.start, GestureDetector(
children: [ key: iconKey,
GestureDetector( child: FlowySvg(
key: iconKey, width: _iconWidth,
child: FlowySvg( height: _iconWidth,
width: _iconWidth, padding: EdgeInsets.only(right: _iconRightPadding),
height: _iconWidth, name: check ? 'check' : 'uncheck',
padding: EdgeInsets.only(right: _iconRightPadding),
name: check ? 'check' : 'uncheck',
),
onTap: () {
TransactionBuilder(widget.editorState)
..updateNode(widget.textNode, {
StyleKey.checkbox: !check,
})
..commit();
},
), ),
Flexible( onTap: () {
child: FlowyRichText( TransactionBuilder(widget.editorState)
key: _richTextKey, ..updateNode(widget.textNode, {
placeholderText: 'To-do', StyleKey.checkbox: !check,
textNode: widget.textNode, })
textSpanDecorator: _textSpanDecorator, ..commit();
placeholderTextSpanDecorator: _textSpanDecorator, },
editorState: widget.editorState, ),
), Flexible(
child: FlowyRichText(
key: _richTextKey,
placeholderText: 'To-do',
textNode: widget.textNode,
textSpanDecorator: _textSpanDecorator,
placeholderTextSpanDecorator: _textSpanDecorator,
editorState: widget.editorState,
), ),
], ),
), ],
), ),
); );
} }

View file

@ -63,16 +63,13 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
top: _topPadding, top: _topPadding,
bottom: defaultLinePadding, bottom: defaultLinePadding,
), ),
child: Container( child: FlowyRichText(
constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), key: _richTextKey,
child: FlowyRichText( placeholderText: 'Heading',
key: _richTextKey, placeholderTextSpanDecorator: _placeholderTextSpanDecorator,
placeholderText: 'Heading', textSpanDecorator: _textSpanDecorator,
placeholderTextSpanDecorator: _placeholderTextSpanDecorator, textNode: widget.textNode,
textSpanDecorator: _textSpanDecorator, editorState: widget.editorState,
textNode: widget.textNode,
editorState: widget.editorState,
),
), ),
); );
} }

View file

@ -58,28 +58,25 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: EdgeInsets.only(bottom: defaultLinePadding), padding: EdgeInsets.only(bottom: defaultLinePadding),
child: SizedBox( child: Row(
width: defaultMaxTextNodeWidth, crossAxisAlignment: CrossAxisAlignment.start,
child: Row( children: [
crossAxisAlignment: CrossAxisAlignment.start, FlowySvg(
children: [ key: iconKey,
FlowySvg( width: _iconWidth,
key: iconKey, height: _iconWidth,
width: _iconWidth, padding: EdgeInsets.only(right: _iconRightPadding),
height: _iconWidth, number: widget.textNode.attributes.number,
padding: EdgeInsets.only(right: _iconRightPadding), ),
number: widget.textNode.attributes.number, Flexible(
child: FlowyRichText(
key: _richTextKey,
placeholderText: 'List',
textNode: widget.textNode,
editorState: widget.editorState,
), ),
Flexible( ),
child: FlowyRichText( ],
key: _richTextKey,
placeholderText: 'List',
textNode: widget.textNode,
editorState: widget.editorState,
),
),
],
),
)); ));
} }
} }

View file

@ -55,30 +55,27 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return Padding(
width: defaultMaxTextNodeWidth, padding: EdgeInsets.only(bottom: defaultLinePadding),
child: Padding( child: IntrinsicHeight(
padding: EdgeInsets.only(bottom: defaultLinePadding), child: Row(
child: IntrinsicHeight( crossAxisAlignment: CrossAxisAlignment.stretch,
child: Row( children: [
crossAxisAlignment: CrossAxisAlignment.stretch, FlowySvg(
children: [ key: iconKey,
FlowySvg( width: _iconWidth,
key: iconKey, padding: EdgeInsets.only(right: _iconRightPadding),
width: _iconWidth, name: 'quote',
padding: EdgeInsets.only(right: _iconRightPadding), ),
name: 'quote', Flexible(
child: FlowyRichText(
key: _richTextKey,
placeholderText: 'Quote',
textNode: widget.textNode,
editorState: widget.editorState,
), ),
Flexible( ),
child: FlowyRichText( ],
key: _richTextKey,
placeholderText: 'Quote',
textNode: widget.textNode,
editorState: widget.editorState,
),
),
],
),
), ),
), ),
); );

View file

@ -52,15 +52,12 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Padding(
constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), padding: EdgeInsets.only(bottom: defaultLinePadding),
child: Padding( child: FlowyRichText(
padding: EdgeInsets.only(bottom: defaultLinePadding), key: _richTextKey,
child: FlowyRichText( textNode: widget.textNode,
key: _richTextKey, editorState: widget.editorState,
textNode: widget.textNode,
editorState: widget.editorState,
),
), ),
); );
} }

View file

@ -61,7 +61,6 @@ class StyleKey {
} }
// TODO: customize // TODO: customize
double defaultMaxTextNodeWidth = 780.0;
double defaultLinePadding = 8.0; double defaultLinePadding = 8.0;
double baseFontSize = 16.0; double baseFontSize = 16.0;
String defaultHighlightColor = '0x6000BCF0'; String defaultHighlightColor = '0x6000BCF0';

View file

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
/// Editor style configuration
class EditorStyle {
const EditorStyle({
required this.padding,
});
const EditorStyle.defaultStyle()
: padding = const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0);
/// The margin of the document context from the editor.
final EdgeInsets padding;
EditorStyle copyWith({EdgeInsets? padding}) {
return EditorStyle(
padding: padding ?? this.padding,
);
}
}

View file

@ -1,5 +1,6 @@
import 'package:appflowy_editor/src/render/image/image_node_builder.dart'; import 'package:appflowy_editor/src/render/image/image_node_builder.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
import 'package:appflowy_editor/src/render/style/editor_style.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/default_key_event_handlers.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/default_key_event_handlers.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -36,6 +37,7 @@ class AppFlowyEditor extends StatefulWidget {
this.customBuilders = const {}, this.customBuilders = const {},
this.keyEventHandlers = const [], this.keyEventHandlers = const [],
this.selectionMenuItems = const [], this.selectionMenuItems = const [],
this.editorStyle = const EditorStyle.defaultStyle(),
}) : super(key: key); }) : super(key: key);
final EditorState editorState; final EditorState editorState;
@ -48,6 +50,8 @@ class AppFlowyEditor extends StatefulWidget {
final List<SelectionMenuItem> selectionMenuItems; final List<SelectionMenuItem> selectionMenuItems;
final EditorStyle editorStyle;
@override @override
State<AppFlowyEditor> createState() => _AppFlowyEditorState(); State<AppFlowyEditor> createState() => _AppFlowyEditorState();
} }
@ -60,6 +64,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
super.initState(); super.initState();
editorState.selectionMenuItems = widget.selectionMenuItems; editorState.selectionMenuItems = widget.selectionMenuItems;
editorState.editorStyle = widget.editorStyle;
editorState.service.renderPluginService = _createRenderPlugin(); editorState.service.renderPluginService = _createRenderPlugin();
} }
@ -68,6 +73,8 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (editorState.service != oldWidget.editorState.service) { if (editorState.service != oldWidget.editorState.service) {
editorState.selectionMenuItems = widget.selectionMenuItems;
editorState.editorStyle = widget.editorStyle;
editorState.service.renderPluginService = _createRenderPlugin(); editorState.service.renderPluginService = _createRenderPlugin();
} }
} }
@ -76,27 +83,31 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppFlowyScroll( return AppFlowyScroll(
key: editorState.service.scrollServiceKey, key: editorState.service.scrollServiceKey,
child: AppFlowySelection( child: Padding(
key: editorState.service.selectionServiceKey, padding: widget.editorStyle.padding,
editorState: editorState, child: AppFlowySelection(
child: AppFlowyInput( key: editorState.service.selectionServiceKey,
key: editorState.service.inputServiceKey,
editorState: editorState, editorState: editorState,
child: AppFlowyKeyboard( child: AppFlowyInput(
key: editorState.service.keyboardServiceKey, key: editorState.service.inputServiceKey,
handlers: [
...defaultKeyEventHandlers,
...widget.keyEventHandlers,
],
editorState: editorState, editorState: editorState,
child: FlowyToolbar( child: AppFlowyKeyboard(
key: editorState.service.toolbarServiceKey, key: editorState.service.keyboardServiceKey,
handlers: [
...defaultKeyEventHandlers,
...widget.keyEventHandlers,
],
editorState: editorState, editorState: editorState,
child: editorState.service.renderPluginService.buildPluginWidget( child: FlowyToolbar(
NodeWidgetContext( key: editorState.service.toolbarServiceKey,
context: context, editorState: editorState,
node: editorState.document.root, child:
editorState: editorState, editorState.service.renderPluginService.buildPluginWidget(
NodeWidgetContext(
context: context,
node: editorState.document.root,
editorState: editorState,
),
), ),
), ),
), ),

View file

@ -49,9 +49,10 @@ void main() async {
final editorRect = tester.getRect(editorFinder); final editorRect = tester.getRect(editorFinder);
final leftImageRect = tester.getRect(imageFinder.at(0)); final leftImageRect = tester.getRect(imageFinder.at(0));
expect(leftImageRect.left, editorRect.left); expect(leftImageRect.left, editor.editorState.editorStyle.padding.left);
final rightImageRect = tester.getRect(imageFinder.at(2)); final rightImageRect = tester.getRect(imageFinder.at(2));
expect(rightImageRect.right, editorRect.right); expect(rightImageRect.right,
editorRect.right - editor.editorState.editorStyle.padding.right);
final centerImageRect = tester.getRect(imageFinder.at(1)); final centerImageRect = tester.getRect(imageFinder.at(1));
expect(centerImageRect.left, expect(centerImageRect.left,
(leftImageRect.left + rightImageRect.left) / 2.0); (leftImageRect.left + rightImageRect.left) / 2.0);
@ -73,8 +74,8 @@ void main() async {
leftImage.onAlign(Alignment.centerRight); leftImage.onAlign(Alignment.centerRight);
await tester.pump(const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 100));
expect( expect(
tester.getRect(imageFinder.at(0)).left, tester.getRect(imageFinder.at(0)).right,
rightImageRect.left, rightImageRect.right,
); );
}); });
}); });