Merge pull request #1364 from LucasXu0/adapt_dart_mode

feat: adapt dark mode
This commit is contained in:
Lucas.Xu 2022-10-26 14:55:37 +08:00 committed by GitHub
commit c7c9048fe3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 921 additions and 534 deletions

View File

@ -93,15 +93,20 @@ class _DocumentPageState extends State<DocumentPage> {
}
Widget _renderAppFlowyEditor(EditorState editorState) {
final theme = Theme.of(context);
final editor = AppFlowyEditor(
editorState: editorState,
editorStyle: customEditorStyle(context),
customBuilders: {
'horizontal_rule': HorizontalRuleWidgetBuilder(),
},
shortcutEvents: [
insertHorizontalRule,
],
themeData: theme.copyWith(extensions: [
...theme.extensions.values,
customEditorTheme(context),
...customPluginTheme(context),
]),
);
return Expanded(
child: SizedBox.expand(

View File

@ -3,59 +3,63 @@ import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
EditorStyle customEditorStyle(BuildContext context) {
const _baseFontSize = 14.0;
EditorStyle customEditorTheme(BuildContext context) {
final theme = context.watch<AppTheme>();
const baseFontSize = 14.0;
const basePadding = 12.0;
var textStyle = theme.isDark
? BuiltInTextStyle.builtInDarkMode()
: BuiltInTextStyle.builtIn();
textStyle = textStyle.copyWith(
defaultTextStyle: textStyle.defaultTextStyle.copyWith(
var editorStyle = theme.isDark ? EditorStyle.dark : EditorStyle.light;
editorStyle = editorStyle.copyWith(
textStyle: editorStyle.textStyle?.copyWith(
fontFamily: 'poppins',
fontSize: baseFontSize,
fontSize: _baseFontSize,
),
bold: textStyle.bold.copyWith(
placeholderTextStyle: editorStyle.placeholderTextStyle?.copyWith(
fontFamily: 'poppins',
fontSize: _baseFontSize,
),
bold: editorStyle.bold?.copyWith(
fontWeight: FontWeight.w500,
),
);
return EditorStyle.defaultStyle().copyWith(
padding: const EdgeInsets.symmetric(horizontal: 80),
textStyle: textStyle,
pluginStyles: {
'text/heading': builtInPluginStyle
..update(
'textStyle',
(_) => (EditorState editorState, Node node) {
final headingToFontSize = {
'h1': baseFontSize + 12,
'h2': baseFontSize + 8,
'h3': baseFontSize + 4,
'h4': baseFontSize,
'h5': baseFontSize,
'h6': baseFontSize,
};
final fontSize =
headingToFontSize[node.attributes.heading] ?? baseFontSize;
return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
},
)
..update(
'padding',
(_) => (EditorState editorState, Node node) {
final headingToPadding = {
'h1': basePadding + 6,
'h2': basePadding + 4,
'h3': basePadding + 2,
'h4': basePadding,
'h5': basePadding,
'h6': basePadding,
};
final padding =
headingToPadding[node.attributes.heading] ?? basePadding;
return EdgeInsets.only(bottom: padding);
},
)
return editorStyle;
}
Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
final theme = context.watch<AppTheme>();
const basePadding = 12.0;
var headingPluginStyle =
theme.isDark ? HeadingPluginStyle.dark : HeadingPluginStyle.light;
headingPluginStyle = headingPluginStyle.copyWith(
textStyle: (EditorState editorState, Node node) {
final headingToFontSize = {
'h1': _baseFontSize + 12,
'h2': _baseFontSize + 8,
'h3': _baseFontSize + 4,
'h4': _baseFontSize,
'h5': _baseFontSize,
'h6': _baseFontSize,
};
final fontSize =
headingToFontSize[node.attributes.heading] ?? _baseFontSize;
return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
},
padding: (EditorState editorState, Node node) {
final headingToPadding = {
'h1': basePadding + 6,
'h2': basePadding + 4,
'h3': basePadding + 2,
'h4': basePadding,
'h5': basePadding,
'h6': basePadding,
};
final padding = headingToPadding[node.attributes.heading] ?? basePadding;
return EdgeInsets.only(bottom: padding);
},
);
final pluginTheme =
theme.isDark ? darkPlguinStyleExtension : lightPlguinStyleExtension;
return pluginTheme.toList()
..removeWhere((element) => element is HeadingPluginStyle)
..add(headingPluginStyle);
}

View File

@ -38,9 +38,11 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
name: () => 'Horizontal rule',
icon: const Icon(
icon: (editorState, onSelected) => Icon(
Icons.horizontal_rule,
color: Colors.black,
color: onSelected
? editorState.editorStyle.selectionMenuItemSelectedIconColor
: editorState.editorStyle.selectionMenuItemIconColor,
size: 18.0,
),
keywords: ['horizontal rule'],

View File

@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:path_provider/path_provider.dart';
import 'package:universal_html/html.dart' as html;
@ -38,7 +37,10 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
// extensions: [HeadingPluginStyle.light],
),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.dark,
home: const MyHomePage(title: 'AppFlowyEditor Example'),
);
}
@ -56,7 +58,6 @@ class _MyHomePageState extends State<MyHomePage> {
int _pageIndex = 0;
EditorState? _editorState;
bool darkMode = false;
EditorStyle _editorStyle = EditorStyle.defaultStyle();
Future<String>? _jsonString;
@override
@ -125,12 +126,31 @@ class _MyHomePageState extends State<MyHomePage> {
_editorState!.transactionStream.listen((event) {
debugPrint('Transaction: ${event.toJson()}');
});
final themeData = darkMode
? ThemeData.dark().copyWith(extensions: [
HeadingPluginStyle.dark,
CheckboxPluginStyle.dark,
NumberListPluginStyle.dark,
QuotedTextPluginStyle.dark,
BulletedListPluginStyle.dark,
EditorStyle.dark,
])
: ThemeData.light().copyWith(
extensions: [
HeadingPluginStyle.light,
CheckboxPluginStyle.light,
NumberListPluginStyle.light,
QuotedTextPluginStyle.light,
BulletedListPluginStyle.light,
EditorStyle.light,
],
);
return Container(
color: darkMode ? Colors.black : Colors.white,
width: MediaQuery.of(context).size.width,
child: AppFlowyEditor(
editorState: _editorState!,
editorStyle: _editorStyle,
themeData: themeData,
editable: true,
customBuilders: {
'text/code_block': CodeBlockNodeWidgetBuilder(),
@ -186,8 +206,6 @@ class _MyHomePageState extends State<MyHomePage> {
icon: const Icon(Icons.color_lens),
onPressed: () {
setState(() {
_editorStyle =
darkMode ? EditorStyle.defaultStyle() : _customizedStyle();
darkMode = !darkMode;
});
},
@ -256,44 +274,4 @@ class _MyHomePageState extends State<MyHomePage> {
});
}
}
EditorStyle _customizedStyle() {
final editorStyle = EditorStyle.defaultStyle();
return editorStyle.copyWith(
cursorColor: Colors.white,
selectionColor: Colors.blue.withOpacity(0.3),
textStyle: editorStyle.textStyle.copyWith(
defaultTextStyle: GoogleFonts.poppins().copyWith(
color: Colors.white,
fontSize: 14.0,
),
defaultPlaceholderTextStyle: GoogleFonts.poppins().copyWith(
color: Colors.white.withOpacity(0.5),
fontSize: 14.0,
),
bold: const TextStyle(fontWeight: FontWeight.w900),
code: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.red[300],
backgroundColor: Colors.grey.withOpacity(0.3),
),
highlightColorHex: '0x6FFFEB3B',
),
pluginStyles: {
'text/quote': builtInPluginStyle
..update(
'textStyle',
(_) {
return (EditorState editorState, Node node) {
return TextStyle(
color: Colors.blue[200],
fontStyle: FontStyle.italic,
fontSize: 12.0,
);
};
},
),
},
);
}
}

View File

@ -46,7 +46,7 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) {
SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
name: () => 'Code Block',
icon: const Icon(
icon: (_, __) => const Icon(
Icons.abc,
color: Colors.black,
size: 18.0,
@ -167,7 +167,7 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
textNode: widget.textNode,
editorState: widget.editorState,
textSpanDecorator: (textSpan) => TextSpan(
style: widget.editorState.editorStyle.textStyle.defaultTextStyle,
style: widget.editorState.editorStyle.textStyle,
children: codeTextSpan,
),
),

View File

@ -38,7 +38,7 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
name: () => 'Horizontal rule',
icon: const Icon(
icon: (_, __) => const Icon(
Icons.horizontal_rule,
color: Colors.black,
size: 18.0,

View File

@ -6,7 +6,7 @@ import 'package:flutter_math_fork/flutter_math.dart';
SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
name: () => 'Tex',
icon: const Icon(
icon: (_, __) => const Icon(
Icons.text_fields_rounded,
color: Colors.black,
size: 18.0,

View File

@ -31,3 +31,5 @@ export 'src/render/rich_text/default_selectable.dart';
export 'src/render/rich_text/flowy_rich_text.dart';
export 'src/render/selection_menu/selection_menu_widget.dart';
export 'src/l10n/l10n.dart';
export 'src/render/style/plugin_styles.dart';
export 'src/render/style/editor_style.dart';

View File

@ -59,13 +59,14 @@ class EditorState {
/// Stores the selection menu items.
List<SelectionMenuItem> selectionMenuItems = [];
/// Stores the editor style.
EditorStyle editorStyle = EditorStyle.defaultStyle();
/// Operation stream.
Stream<Transaction> get transactionStream => _observer.stream;
final StreamController<Transaction> _observer = StreamController.broadcast();
late ThemeData themeData;
EditorStyle get editorStyle =>
themeData.extension<EditorStyle>() ?? EditorStyle.light;
final UndoManager undoManager = UndoManager();
Selection? _cursorSelection;

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
extension ThemeExtension on ThemeData {
T? extensionOrNull<T>() {
if (extensions.containsKey(T)) {
return extensions[T] as T;
}
return null;
}
}

View File

@ -2,6 +2,7 @@ import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
import 'package:appflowy_editor/src/render/style/editor_style.dart';
import 'package:flutter/material.dart';
OverlayEntry? _imageUploadMenu;
@ -20,6 +21,7 @@ void showImageUploadMenu(
left: menuService.topLeft.dx,
child: Material(
child: ImageUploadMenu(
editorState: editorState,
onSubmitted: (text) {
// _dismissImageUploadMenu();
editorState.insertImageNode(text);
@ -53,10 +55,12 @@ class ImageUploadMenu extends StatefulWidget {
Key? key,
required this.onSubmitted,
required this.onUpload,
this.editorState,
}) : super(key: key);
final void Function(String text) onSubmitted;
final void Function(String text) onUpload;
final EditorState? editorState;
@override
State<ImageUploadMenu> createState() => _ImageUploadMenuState();
@ -66,6 +70,8 @@ class _ImageUploadMenuState extends State<ImageUploadMenu> {
final _textEditingController = TextEditingController();
final _focusNode = FocusNode();
EditorStyle? get style => widget.editorState?.editorStyle;
@override
void initState() {
super.initState();
@ -84,7 +90,7 @@ class _ImageUploadMenuState extends State<ImageUploadMenu> {
width: 300,
padding: const EdgeInsets.all(24.0),
decoration: BoxDecoration(
color: Colors.white,
color: style?.selectionMenuBackgroundColor ?? Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 5,
@ -108,12 +114,12 @@ class _ImageUploadMenuState extends State<ImageUploadMenu> {
}
Widget _buildHeader(BuildContext context) {
return const Text(
return Text(
'URL Image',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 14.0,
color: Colors.black,
color: style?.selectionMenuItemTextColor ?? Colors.black,
fontWeight: FontWeight.w500,
),
);

View File

@ -1,10 +1,13 @@
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/style/editor_style.dart';
import 'package:flutter/material.dart';
class LinkMenu extends StatefulWidget {
const LinkMenu({
Key? key,
this.linkText,
this.editorState,
required this.onSubmitted,
required this.onOpenLink,
required this.onCopyLink,
@ -13,6 +16,7 @@ class LinkMenu extends StatefulWidget {
}) : super(key: key);
final String? linkText;
final EditorState? editorState;
final void Function(String text) onSubmitted;
final VoidCallback onOpenLink;
final VoidCallback onCopyLink;
@ -27,6 +31,8 @@ class _LinkMenuState extends State<LinkMenu> {
final _textEditingController = TextEditingController();
final _focusNode = FocusNode();
EditorStyle? get style => widget.editorState?.editorStyle;
@override
void initState() {
super.initState();
@ -48,7 +54,7 @@ class _LinkMenuState extends State<LinkMenu> {
width: 350,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
color: style?.selectionMenuBackgroundColor ?? Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 5,
@ -71,17 +77,19 @@ class _LinkMenuState extends State<LinkMenu> {
if (widget.linkText != null) ...[
_buildIconButton(
iconName: 'link',
color: style?.selectionMenuItemIconColor,
text: 'Open link',
onPressed: widget.onOpenLink,
),
_buildIconButton(
iconName: 'copy',
color: Colors.black,
color: style?.selectionMenuItemIconColor,
text: 'Copy link',
onPressed: widget.onCopyLink,
),
_buildIconButton(
iconName: 'delete',
color: style?.selectionMenuItemIconColor,
text: 'Remove link',
onPressed: widget.onRemoveLink,
),
@ -154,8 +162,8 @@ class _LinkMenuState extends State<LinkMenu> {
label: Text(
text,
textAlign: TextAlign.left,
style: const TextStyle(
color: Colors.black,
style: TextStyle(
color: style?.selectionMenuItemTextColor ?? Colors.black,
fontSize: 14.0,
),
),

View File

@ -10,56 +10,6 @@ abstract class BuiltInTextWidget extends StatefulWidget {
TextNode get textNode;
}
mixin BuiltInStyleMixin<T extends BuiltInTextWidget> on State<T> {
EdgeInsets get padding {
final padding = widget.editorState.editorStyle.style(
widget.editorState,
widget.textNode,
'padding',
);
if (padding is EdgeInsets) {
return padding;
}
return const EdgeInsets.all(0);
}
TextStyle get textStyle {
final textStyle = widget.editorState.editorStyle.style(
widget.editorState,
widget.textNode,
'textStyle',
);
if (textStyle is TextStyle) {
return textStyle;
}
return const TextStyle();
}
Size? get iconSize {
final iconSize = widget.editorState.editorStyle.style(
widget.editorState,
widget.textNode,
'iconSize',
);
if (iconSize is Size) {
return iconSize;
}
return const Size.square(18.0);
}
EdgeInsets? get iconPadding {
final iconPadding = widget.editorState.editorStyle.style(
widget.editorState,
widget.textNode,
'iconPadding',
);
if (iconPadding is EdgeInsets) {
return iconPadding;
}
return const EdgeInsets.all(0);
}
}
mixin BuiltInTextWidgetMixin<T extends BuiltInTextWidget> on State<T>
implements DefaultSelectable {
@override

View File

@ -1,13 +1,14 @@
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/render/style/plugin_styles.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
import 'package:appflowy_editor/src/extensions/theme_extension.dart';
class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
@override
@ -45,11 +46,7 @@ class BulletedListTextNodeWidget extends BuiltInTextWidget {
// customize
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
with
SelectableMixin,
DefaultSelectable,
BuiltInStyleMixin,
BuiltInTextWidgetMixin {
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
@override
final iconKey = GlobalKey();
@ -64,6 +61,25 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
return super.baseOffset.translate(0, padding.top);
}
BulletedListPluginStyle get style =>
Theme.of(context).extensionOrNull<BulletedListPluginStyle>() ??
BulletedListPluginStyle.light;
EdgeInsets get padding => style.padding(
widget.editorState,
widget.textNode,
);
TextStyle get textStyle => style.textStyle(
widget.editorState,
widget.textNode,
);
Widget get icon => style.icon(
widget.editorState,
widget.textNode,
);
@override
Widget buildWithSingle(BuildContext context) {
return Padding(
@ -71,12 +87,9 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowySvg(
Container(
key: iconKey,
width: iconSize?.width,
height: iconSize?.height,
padding: iconPadding,
name: 'point',
child: icon,
),
Flexible(
child: FlowyRichText(
@ -86,7 +99,7 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
textSpan.updateTextStyle(textStyle),
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
lineHeight: widget.editorState.editorStyle.lineHeight,
textNode: widget.textNode,
editorState: widget.editorState,
),

View File

@ -1,10 +1,10 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/commands/text/text_commands.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/theme_extension.dart';
class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
@override
@ -39,11 +39,7 @@ class CheckboxNodeWidget extends BuiltInTextWidget {
}
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
with
SelectableMixin,
DefaultSelectable,
BuiltInStyleMixin,
BuiltInTextWidgetMixin {
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
@override
final iconKey = GlobalKey();
@ -58,6 +54,25 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
return super.baseOffset.translate(0, padding.top);
}
CheckboxPluginStyle get style =>
Theme.of(context).extensionOrNull<CheckboxPluginStyle>() ??
CheckboxPluginStyle.light;
EdgeInsets get padding => style.padding(
widget.editorState,
widget.textNode,
);
TextStyle get textStyle => style.textStyle(
widget.editorState,
widget.textNode,
);
Widget get icon => style.icon(
widget.editorState,
widget.textNode,
);
@override
Widget buildWithSingle(BuildContext context) {
final check = widget.textNode.attributes.check;
@ -68,12 +83,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
children: [
GestureDetector(
key: iconKey,
child: FlowySvg(
width: iconSize?.width,
height: iconSize?.height,
padding: iconPadding,
name: check ? 'check' : 'uncheck',
),
child: icon,
onTap: () async {
await widget.editorState.formatTextToCheckbox(
widget.editorState,
@ -86,7 +96,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
child: FlowyRichText(
key: _richTextKey,
placeholderText: 'To-do',
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
lineHeight: widget.editorState.editorStyle.lineHeight,
textNode: widget.textNode,
textSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),

View File

@ -202,12 +202,13 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
}
TextSpan get _placeholderTextSpan {
final style = widget.editorState.editorStyle.textStyle;
final placeholderTextStyle =
widget.editorState.editorStyle.placeholderTextStyle;
return TextSpan(
children: [
TextSpan(
text: widget.placeholderText,
style: style.defaultPlaceholderTextStyle,
style: placeholderTextStyle,
),
],
);
@ -216,10 +217,10 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
TextSpan get _textSpan {
var offset = 0;
List<TextSpan> textSpans = [];
final style = widget.editorState.editorStyle.textStyle;
final style = widget.editorState.editorStyle;
final textInserts = widget.textNode.delta.whereType<TextInsert>();
for (final textInsert in textInserts) {
var textStyle = style.defaultTextStyle;
var textStyle = style.textStyle!;
GestureRecognizer? recognizer;
final attributes = textInsert.attributes;
if (attributes != null) {

View File

@ -4,10 +4,12 @@ import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/render/style/plugin_styles.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
import 'package:appflowy_editor/src/extensions/theme_extension.dart';
class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
@override
@ -43,7 +45,7 @@ class HeadingTextNodeWidget extends BuiltInTextWidget {
// customize
class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
with SelectableMixin, DefaultSelectable {
@override
GlobalKey? get iconKey => null;
@ -58,6 +60,20 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
return padding.topLeft;
}
HeadingPluginStyle get style =>
Theme.of(context).extensionOrNull<HeadingPluginStyle>() ??
HeadingPluginStyle.light;
EdgeInsets get padding => style.padding(
widget.editorState,
widget.textNode,
);
TextStyle get textStyle => style.textStyle(
widget.editorState,
widget.textNode,
);
@override
Widget build(BuildContext context) {
return Padding(
@ -68,7 +84,7 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
lineHeight: widget.editorState.editorStyle.lineHeight,
textNode: widget.textNode,
editorState: widget.editorState,
),

View File

@ -4,10 +4,12 @@ import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/render/style/plugin_styles.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
import 'package:appflowy_editor/src/extensions/theme_extension.dart';
class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
@override
@ -43,7 +45,7 @@ class NumberListTextNodeWidget extends BuiltInTextWidget {
}
class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
with SelectableMixin, DefaultSelectable {
@override
final iconKey = GlobalKey();
@ -58,6 +60,25 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
return super.baseOffset.translate(0, padding.top);
}
NumberListPluginStyle get style =>
Theme.of(context).extensionOrNull<NumberListPluginStyle>() ??
NumberListPluginStyle.light;
EdgeInsets get padding => style.padding(
widget.editorState,
widget.textNode,
);
TextStyle get textStyle => style.textStyle(
widget.editorState,
widget.textNode,
);
Widget get icon => style.icon(
widget.editorState,
widget.textNode,
);
@override
Widget build(BuildContext context) {
return Padding(
@ -67,12 +88,7 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
children: [
Container(
key: iconKey,
padding: iconPadding,
child: Text(
'${widget.textNode.attributes.number.toString()}.',
// FIXME: customize
style: const TextStyle(fontSize: 16.0, color: Colors.black),
),
child: icon,
),
Flexible(
child: FlowyRichText(
@ -80,7 +96,7 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
placeholderText: 'List',
textNode: widget.textNode,
editorState: widget.editorState,
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
lineHeight: widget.editorState.editorStyle.lineHeight,
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
textSpanDecorator: (textSpan) =>

View File

@ -1,13 +1,14 @@
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/render/style/plugin_styles.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
import 'package:appflowy_editor/src/extensions/theme_extension.dart';
class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
@override
@ -44,7 +45,7 @@ class QuotedTextNodeWidget extends BuiltInTextWidget {
// customize
class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
with SelectableMixin, DefaultSelectable {
@override
final iconKey = GlobalKey();
@ -59,6 +60,25 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
return super.baseOffset.translate(0, padding.top);
}
QuotedTextPluginStyle get style =>
Theme.of(context).extensionOrNull<QuotedTextPluginStyle>() ??
QuotedTextPluginStyle.light;
EdgeInsets get padding => style.padding(
widget.editorState,
widget.textNode,
);
TextStyle get textStyle => style.textStyle(
widget.editorState,
widget.textNode,
);
Widget get icon => style.icon(
widget.editorState,
widget.textNode,
);
@override
Widget build(BuildContext context) {
return Padding(
@ -67,11 +87,9 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FlowySvg(
Container(
key: iconKey,
width: iconSize?.width,
padding: iconPadding,
name: 'quote',
child: icon,
),
Flexible(
child: FlowyRichText(
@ -82,7 +100,7 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
textSpan.updateTextStyle(textStyle),
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
lineHeight: widget.editorState.editorStyle.lineHeight,
editorState: widget.editorState,
),
),

View File

@ -4,6 +4,7 @@ import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/render/style/editor_style.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
@ -43,11 +44,7 @@ class RichTextNodeWidget extends BuiltInTextWidget {
// customize
class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
with
SelectableMixin,
DefaultSelectable,
BuiltInStyleMixin,
BuiltInTextWidgetMixin {
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
@override
GlobalKey? get iconKey => null;
@ -59,20 +56,26 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
@override
Offset get baseOffset {
return padding.topLeft;
return textPadding.topLeft;
}
EditorStyle get style => widget.editorState.editorStyle;
EdgeInsets get textPadding => style.textPadding!;
TextStyle get textStyle => style.textStyle!;
@override
Widget buildWithSingle(BuildContext context) {
return Padding(
padding: padding,
padding: textPadding,
child: FlowyRichText(
key: _richTextKey,
textNode: widget.textNode,
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
lineHeight: widget.editorState.editorStyle.lineHeight,
editorState: widget.editorState,
),
);

View File

@ -3,7 +3,7 @@ import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
import 'package:flutter/material.dart';
class SelectionMenuItemWidget extends StatelessWidget {
class SelectionMenuItemWidget extends StatefulWidget {
const SelectionMenuItemWidget({
Key? key,
required this.editorState,
@ -11,7 +11,6 @@ class SelectionMenuItemWidget extends StatelessWidget {
required this.item,
required this.isSelected,
this.width = 140.0,
this.selectedColor = const Color(0xFFE0F8FF),
}) : super(key: key);
final EditorState editorState;
@ -19,33 +18,52 @@ class SelectionMenuItemWidget extends StatelessWidget {
final SelectionMenuItem item;
final double width;
final bool isSelected;
final Color selectedColor;
@override
State<SelectionMenuItemWidget> createState() =>
_SelectionMenuItemWidgetState();
}
class _SelectionMenuItemWidgetState extends State<SelectionMenuItemWidget> {
var _onHover = false;
@override
Widget build(BuildContext context) {
final editorStyle = widget.editorState.editorStyle;
return Container(
padding: const EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0),
child: SizedBox(
width: width,
width: widget.width,
child: TextButton.icon(
icon: item.icon,
icon: widget.item
.icon(widget.editorState, widget.isSelected || _onHover),
style: ButtonStyle(
alignment: Alignment.centerLeft,
overlayColor: MaterialStateProperty.all(selectedColor),
backgroundColor: isSelected
? MaterialStateProperty.all(selectedColor)
overlayColor: MaterialStateProperty.all(
editorStyle.selectionMenuItemSelectedColor),
backgroundColor: widget.isSelected
? MaterialStateProperty.all(
editorStyle.selectionMenuItemSelectedColor)
: MaterialStateProperty.all(Colors.transparent),
),
label: Text(
item.name(),
widget.item.name(),
textAlign: TextAlign.left,
style: const TextStyle(
color: Colors.black,
fontSize: 14.0,
style: TextStyle(
color: (widget.isSelected || _onHover)
? editorStyle.selectionMenuItemSelectedTextColor
: editorStyle.selectionMenuItemTextColor,
fontSize: 12.0,
),
),
onPressed: () {
item.handler(editorState, menuService, context);
widget.item
.handler(widget.editorState, widget.menuService, context);
},
onHover: (value) {
setState(() {
_onHover = value;
});
},
),
),

View File

@ -61,19 +61,27 @@ class SelectionMenu implements SelectionMenuService {
// Just subtract the padding here as a result.
const menuHeight = 200.0;
const menuOffset = Offset(10, 10);
final baseOffset =
final editorOffset =
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
var offset = selectionRects.first.bottomRight + menuOffset;
if (offset.dy >=
baseOffset.dy + editorState.renderBox!.size.height - menuHeight) {
offset = selectionRects.first.topRight - menuOffset;
offset = offset.translate(0, -menuHeight);
final editorHeight = editorState.renderBox!.size.height;
// show below defualt
var showBelow = true;
final bottomRight = selectionRects.first.bottomRight;
final topRight = selectionRects.first.topRight;
var offset = bottomRight + menuOffset;
// overflow
if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) {
// show above
offset = topRight - menuOffset;
showBelow = false;
}
_topLeft = offset;
_selectionMenuEntry = OverlayEntry(builder: (context) {
return Positioned(
top: offset.dy,
top: showBelow ? offset.dy : null,
bottom: showBelow ? null : editorHeight - offset.dy,
left: offset.dx,
child: SelectionMenuWidget(
items: [
@ -131,7 +139,8 @@ List<SelectionMenuItem> get defaultSelectionMenuItems =>
final List<SelectionMenuItem> _defaultSelectionMenuItems = [
SelectionMenuItem(
name: () => AppFlowyEditorLocalizations.current.text,
icon: _selectionMenuIcon('text'),
icon: (editorState, onSelected) =>
_selectionMenuIcon('text', editorState, onSelected),
keywords: ['text'],
handler: (editorState, _, __) {
insertTextNodeAfterSelection(editorState, {});
@ -139,7 +148,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
),
SelectionMenuItem(
name: () => AppFlowyEditorLocalizations.current.heading1,
icon: _selectionMenuIcon('h1'),
icon: (editorState, onSelected) =>
_selectionMenuIcon('h1', editorState, onSelected),
keywords: ['heading 1, h1'],
handler: (editorState, _, __) {
insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h1);
@ -147,7 +157,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
),
SelectionMenuItem(
name: () => AppFlowyEditorLocalizations.current.heading2,
icon: _selectionMenuIcon('h2'),
icon: (editorState, onSelected) =>
_selectionMenuIcon('h2', editorState, onSelected),
keywords: ['heading 2, h2'],
handler: (editorState, _, __) {
insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h2);
@ -155,7 +166,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
),
SelectionMenuItem(
name: () => AppFlowyEditorLocalizations.current.heading3,
icon: _selectionMenuIcon('h3'),
icon: (editorState, onSelected) =>
_selectionMenuIcon('h3', editorState, onSelected),
keywords: ['heading 3, h3'],
handler: (editorState, _, __) {
insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h3);
@ -163,13 +175,15 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
),
SelectionMenuItem(
name: () => AppFlowyEditorLocalizations.current.image,
icon: _selectionMenuIcon('image'),
icon: (editorState, onSelected) =>
_selectionMenuIcon('image', editorState, onSelected),
keywords: ['image'],
handler: showImageUploadMenu,
),
SelectionMenuItem(
name: () => AppFlowyEditorLocalizations.current.bulletedList,
icon: _selectionMenuIcon('bulleted_list'),
icon: (editorState, onSelected) =>
_selectionMenuIcon('bulleted_list', editorState, onSelected),
keywords: ['bulleted list', 'list', 'unordered list'],
handler: (editorState, _, __) {
insertBulletedListAfterSelection(editorState);
@ -177,7 +191,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
),
SelectionMenuItem(
name: () => AppFlowyEditorLocalizations.current.numberedList,
icon: _selectionMenuIcon('number'),
icon: (editorState, onSelected) =>
_selectionMenuIcon('number', editorState, onSelected),
keywords: ['numbered list', 'list', 'ordered list'],
handler: (editorState, _, __) {
insertNumberedListAfterSelection(editorState);
@ -185,7 +200,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
),
SelectionMenuItem(
name: () => AppFlowyEditorLocalizations.current.checkbox,
icon: _selectionMenuIcon('checkbox'),
icon: (editorState, onSelected) =>
_selectionMenuIcon('checkbox', editorState, onSelected),
keywords: ['todo list', 'list', 'checkbox list'],
handler: (editorState, _, __) {
insertCheckboxAfterSelection(editorState);
@ -193,7 +209,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
),
SelectionMenuItem(
name: () => AppFlowyEditorLocalizations.current.quote,
icon: _selectionMenuIcon('quote'),
icon: (editorState, onSelected) =>
_selectionMenuIcon('quote', editorState, onSelected),
keywords: ['quote', 'refer'],
handler: (editorState, _, __) {
insertQuoteAfterSelection(editorState);
@ -201,10 +218,13 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
),
];
Widget _selectionMenuIcon(String name) {
Widget _selectionMenuIcon(
String name, EditorState editorState, bool onSelected) {
return FlowySvg(
name: 'selection_menu/$name',
color: Colors.black,
color: onSelected
? editorState.editorStyle.selectionMenuItemSelectedIconColor
: editorState.editorStyle.selectionMenuItemIconColor,
width: 18.0,
height: 18.0,
);

View File

@ -29,7 +29,7 @@ class SelectionMenuItem {
}
final String Function() name;
final Widget icon;
final Widget Function(EditorState editorState, bool onSelected) icon;
/// Customizes keywords for item.
///
@ -142,7 +142,7 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
onKey: _onKey,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
color: widget.editorState.editorStyle.selectionMenuBackgroundColor,
boxShadow: [
BoxShadow(
blurRadius: 5,

View File

@ -1,202 +1,78 @@
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
Iterable<ThemeExtension<dynamic>> get lightEditorStyleExtension => [
EditorStyle.light,
];
typedef PluginStyler = Object Function(EditorState editorState, Node node);
typedef PluginStyle = Map<String, PluginStyler>;
Iterable<ThemeExtension<dynamic>> get darkEditorStyleExtension => [
EditorStyle.dark,
];
class EditorStyle extends ThemeExtension<EditorStyle> {
// Editor styles
final EdgeInsets? padding;
final Color? cursorColor;
final Color? selectionColor;
// Selection menu styles
final Color? selectionMenuBackgroundColor;
final Color? selectionMenuItemTextColor;
final Color? selectionMenuItemIconColor;
final Color? selectionMenuItemSelectedTextColor;
final Color? selectionMenuItemSelectedIconColor;
final Color? selectionMenuItemSelectedColor;
// Text styles
final EdgeInsets? textPadding;
final TextStyle? textStyle;
final TextStyle? placeholderTextStyle;
final double lineHeight;
// Rich text styles
final TextStyle? bold;
final TextStyle? italic;
final TextStyle? underline;
final TextStyle? strikethrough;
final TextStyle? href;
final TextStyle? code;
final String? highlightColorHex;
/// Editor style configuration
class EditorStyle {
EditorStyle({
required this.padding,
required this.textStyle,
required this.cursorColor,
required this.selectionColor,
Map<String, PluginStyle> pluginStyles = const {},
}) {
_pluginStyles.addAll(pluginStyles);
}
EditorStyle.defaultStyle()
: padding = const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0),
textStyle = BuiltInTextStyle.builtIn(),
cursorColor = const Color(0xFF00BCF0),
selectionColor = const Color.fromARGB(53, 111, 201, 231);
/// The margin of the document context from the editor.
final EdgeInsets padding;
final BuiltInTextStyle textStyle;
final Color cursorColor;
final Color selectionColor;
final Map<String, PluginStyle> _pluginStyles = Map.from(builtInTextStylers);
Object? style(EditorState editorState, Node node, String key) {
final styler = _pluginStyles[node.id]?[key];
if (styler != null) {
return styler(editorState, node);
}
return null;
}
EditorStyle copyWith({
EdgeInsets? padding,
BuiltInTextStyle? textStyle,
Color? cursorColor,
Color? selectionColor,
Map<String, PluginStyle>? pluginStyles,
}) {
return EditorStyle(
padding: padding ?? this.padding,
textStyle: textStyle ?? this.textStyle,
cursorColor: cursorColor ?? this.cursorColor,
selectionColor: selectionColor ?? this.selectionColor,
pluginStyles: pluginStyles ?? {},
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is EditorStyle &&
other.padding == padding &&
other.textStyle == textStyle &&
other.cursorColor == cursorColor &&
other.selectionColor == selectionColor;
}
@override
int get hashCode {
return padding.hashCode ^
textStyle.hashCode ^
cursorColor.hashCode ^
selectionColor.hashCode;
}
}
PluginStyle get builtInPluginStyle => Map.from({
'padding': (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
'textStyle': (_, __) => const TextStyle(),
'iconSize': (_, __) => const Size.square(20.0),
'iconPadding': (_, __) => const EdgeInsets.only(right: 5.0),
});
Map<String, PluginStyle> builtInTextStylers = {
'text': builtInPluginStyle,
'text/checkbox': builtInPluginStyle
..update(
'textStyle',
(_) => (EditorState editorState, Node node) {
if (node is TextNode && node.attributes.check == true) {
return const TextStyle(
color: Colors.grey,
decoration: TextDecoration.lineThrough,
);
}
return const TextStyle();
},
),
'text/heading': builtInPluginStyle
..update(
'textStyle',
(_) => (EditorState editorState, Node node) {
final headingToFontSize = {
'h1': 32.0,
'h2': 28.0,
'h3': 24.0,
'h4': 18.0,
'h5': 18.0,
'h6': 18.0,
};
final fontSize = headingToFontSize[node.attributes.heading] ?? 18.0;
return TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold);
},
),
'text/bulleted-list': builtInPluginStyle,
'text/number-list': builtInPluginStyle
..update(
'iconPadding',
(_) => (EditorState editorState, Node node) {
return const EdgeInsets.only(left: 5.0, right: 5.0);
},
),
'text/quote': builtInPluginStyle,
'image': builtInPluginStyle,
};
class BuiltInTextStyle {
const BuiltInTextStyle({
required this.defaultTextStyle,
required this.defaultPlaceholderTextStyle,
required this.selectionMenuBackgroundColor,
required this.selectionMenuItemTextColor,
required this.selectionMenuItemIconColor,
required this.selectionMenuItemSelectedTextColor,
required this.selectionMenuItemSelectedIconColor,
required this.selectionMenuItemSelectedColor,
required this.textPadding,
required this.textStyle,
required this.placeholderTextStyle,
required this.bold,
required this.italic,
required this.underline,
required this.strikethrough,
required this.href,
required this.code,
this.highlightColorHex = '0x6000BCF0',
this.lineHeight = 1.5,
required this.highlightColorHex,
required this.lineHeight,
});
final TextStyle defaultTextStyle;
final TextStyle defaultPlaceholderTextStyle;
final TextStyle bold;
final TextStyle italic;
final TextStyle underline;
final TextStyle strikethrough;
final TextStyle href;
final TextStyle code;
final String highlightColorHex;
final double lineHeight;
BuiltInTextStyle.builtIn()
: defaultTextStyle = const TextStyle(fontSize: 16.0, color: Colors.black),
defaultPlaceholderTextStyle =
const TextStyle(fontSize: 16.0, color: Colors.grey),
bold = const TextStyle(fontWeight: FontWeight.bold),
italic = const TextStyle(fontStyle: FontStyle.italic),
underline = const TextStyle(decoration: TextDecoration.underline),
strikethrough = const TextStyle(decoration: TextDecoration.lineThrough),
href = const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
code = const TextStyle(
fontFamily: 'monospace',
color: Color(0xFF00BCF0),
backgroundColor: Color(0xFFE0F8FF),
),
highlightColorHex = '0x6000BCF0',
lineHeight = 1.5;
BuiltInTextStyle.builtInDarkMode()
: defaultTextStyle = const TextStyle(fontSize: 16.0, color: Colors.white),
defaultPlaceholderTextStyle = TextStyle(
fontSize: 16.0,
color: Colors.white.withOpacity(0.3),
),
bold = const TextStyle(fontWeight: FontWeight.bold),
italic = const TextStyle(fontStyle: FontStyle.italic),
underline = const TextStyle(decoration: TextDecoration.underline),
strikethrough = const TextStyle(decoration: TextDecoration.lineThrough),
href = const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
code = const TextStyle(
fontFamily: 'monospace',
color: Color(0xFF00BCF0),
backgroundColor: Color(0xFFE0F8FF),
),
highlightColorHex = '0x6000BCF0',
lineHeight = 1.5;
BuiltInTextStyle copyWith({
TextStyle? defaultTextStyle,
TextStyle? defaultPlaceholderTextStyle,
@override
EditorStyle copyWith({
EdgeInsets? padding,
Color? cursorColor,
Color? selectionColor,
Color? selectionMenuBackgroundColor,
Color? selectionMenuItemTextColor,
Color? selectionMenuItemIconColor,
Color? selectionMenuItemSelectedTextColor,
Color? selectionMenuItemSelectedIconColor,
Color? selectionMenuItemSelectedColor,
TextStyle? textStyle,
TextStyle? placeholderTextStyle,
TextStyle? bold,
TextStyle? italic,
TextStyle? underline,
@ -206,10 +82,25 @@ class BuiltInTextStyle {
String? highlightColorHex,
double? lineHeight,
}) {
return BuiltInTextStyle(
defaultTextStyle: defaultTextStyle ?? this.defaultTextStyle,
defaultPlaceholderTextStyle:
defaultPlaceholderTextStyle ?? this.defaultPlaceholderTextStyle,
return EditorStyle(
padding: padding ?? this.padding,
cursorColor: cursorColor ?? this.cursorColor,
selectionColor: selectionColor ?? this.selectionColor,
selectionMenuBackgroundColor:
selectionMenuBackgroundColor ?? this.selectionMenuBackgroundColor,
selectionMenuItemTextColor:
selectionMenuItemTextColor ?? this.selectionMenuItemTextColor,
selectionMenuItemIconColor:
selectionMenuItemIconColor ?? this.selectionMenuItemIconColor,
selectionMenuItemSelectedTextColor: selectionMenuItemSelectedTextColor ??
this.selectionMenuItemSelectedTextColor,
selectionMenuItemSelectedIconColor: selectionMenuItemSelectedIconColor ??
this.selectionMenuItemSelectedIconColor,
selectionMenuItemSelectedColor:
selectionMenuItemSelectedColor ?? this.selectionMenuItemSelectedColor,
textPadding: textPadding ?? textPadding,
textStyle: textStyle ?? this.textStyle,
placeholderTextStyle: placeholderTextStyle ?? this.placeholderTextStyle,
bold: bold ?? this.bold,
italic: italic ?? this.italic,
underline: underline ?? this.underline,
@ -222,33 +113,87 @@ class BuiltInTextStyle {
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is BuiltInTextStyle &&
other.defaultTextStyle == defaultTextStyle &&
other.defaultPlaceholderTextStyle == defaultPlaceholderTextStyle &&
other.bold == bold &&
other.italic == italic &&
other.underline == underline &&
other.strikethrough == strikethrough &&
other.href == href &&
other.code == code &&
other.highlightColorHex == highlightColorHex &&
other.lineHeight == lineHeight;
ThemeExtension<EditorStyle> lerp(
ThemeExtension<EditorStyle>? other, double t) {
if (other == null || other is! EditorStyle) {
return this;
}
return EditorStyle(
padding: EdgeInsets.lerp(padding, other.padding, t),
cursorColor: Color.lerp(cursorColor, other.cursorColor, t),
textPadding: EdgeInsets.lerp(textPadding, other.textPadding, t),
selectionColor: Color.lerp(selectionColor, other.selectionColor, t),
selectionMenuBackgroundColor: Color.lerp(
selectionMenuBackgroundColor, other.selectionMenuBackgroundColor, t),
selectionMenuItemTextColor: Color.lerp(
selectionMenuItemTextColor, other.selectionMenuItemTextColor, t),
selectionMenuItemIconColor: Color.lerp(
selectionMenuItemIconColor, other.selectionMenuItemIconColor, t),
selectionMenuItemSelectedTextColor: Color.lerp(
selectionMenuItemSelectedTextColor,
other.selectionMenuItemSelectedTextColor,
t),
selectionMenuItemSelectedIconColor: Color.lerp(
selectionMenuItemSelectedIconColor,
other.selectionMenuItemSelectedIconColor,
t),
selectionMenuItemSelectedColor: Color.lerp(selectionMenuItemSelectedColor,
other.selectionMenuItemSelectedColor, t),
textStyle: TextStyle.lerp(textStyle, other.textStyle, t),
placeholderTextStyle:
TextStyle.lerp(placeholderTextStyle, other.placeholderTextStyle, t),
bold: TextStyle.lerp(bold, other.bold, t),
italic: TextStyle.lerp(italic, other.italic, t),
underline: TextStyle.lerp(underline, other.underline, t),
strikethrough: TextStyle.lerp(strikethrough, other.strikethrough, t),
href: TextStyle.lerp(href, other.href, t),
code: TextStyle.lerp(code, other.code, t),
highlightColorHex: highlightColorHex,
lineHeight: lineHeight,
);
}
@override
int get hashCode {
return defaultTextStyle.hashCode ^
defaultPlaceholderTextStyle.hashCode ^
bold.hashCode ^
italic.hashCode ^
underline.hashCode ^
strikethrough.hashCode ^
href.hashCode ^
code.hashCode ^
highlightColorHex.hashCode ^
lineHeight.hashCode;
}
static final light = EditorStyle(
padding: const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0),
cursorColor: const Color(0xFF00BCF0),
selectionColor: const Color.fromARGB(53, 111, 201, 231),
selectionMenuBackgroundColor: const Color(0xFFFFFFFF),
selectionMenuItemTextColor: const Color(0xFF333333),
selectionMenuItemIconColor: const Color(0xFF333333),
selectionMenuItemSelectedTextColor: const Color(0xFF333333),
selectionMenuItemSelectedIconColor: const Color(0xFF333333),
selectionMenuItemSelectedColor: const Color(0xFFE0F8FF),
textPadding: const EdgeInsets.symmetric(vertical: 8.0),
textStyle: const TextStyle(fontSize: 16.0, color: Colors.black),
placeholderTextStyle: const TextStyle(fontSize: 16.0, color: Colors.grey),
bold: const TextStyle(fontWeight: FontWeight.bold),
italic: const TextStyle(fontStyle: FontStyle.italic),
underline: const TextStyle(decoration: TextDecoration.underline),
strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
href: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
code: const TextStyle(
fontFamily: 'monospace',
color: Color(0xFF00BCF0),
backgroundColor: Color(0xFFE0F8FF),
),
highlightColorHex: '0x6000BCF0',
lineHeight: 1.5,
);
static final dark = light.copyWith(
textStyle: const TextStyle(fontSize: 16.0, color: Colors.white),
placeholderTextStyle: TextStyle(
fontSize: 16.0,
color: Colors.white.withOpacity(0.3),
),
selectionMenuBackgroundColor: const Color(0xFF282E3A),
selectionMenuItemTextColor: const Color(0xFFBBC3CD),
selectionMenuItemIconColor: const Color(0xFFBBC3CD),
selectionMenuItemSelectedTextColor: const Color(0xFF131720),
selectionMenuItemSelectedIconColor: const Color(0xFF131720),
selectionMenuItemSelectedColor: const Color(0xFF00BCF0),
);
}

View File

@ -0,0 +1,327 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:flutter/material.dart';
Iterable<ThemeExtension<dynamic>> get lightPlguinStyleExtension => [
HeadingPluginStyle.light,
CheckboxPluginStyle.light,
NumberListPluginStyle.light,
QuotedTextPluginStyle.light,
];
Iterable<ThemeExtension<dynamic>> get darkPlguinStyleExtension => [
HeadingPluginStyle.dark,
CheckboxPluginStyle.dark,
NumberListPluginStyle.dark,
QuotedTextPluginStyle.dark,
BulletedListPluginStyle.dark,
];
typedef TextStyleCustomizer = TextStyle Function(
EditorState editorState, TextNode textNode);
typedef PaddingCustomizer = EdgeInsets Function(
EditorState editorState, TextNode textNode);
typedef IconCustomizer = Widget Function(
EditorState editorState, TextNode textNode);
class HeadingPluginStyle extends ThemeExtension<HeadingPluginStyle> {
const HeadingPluginStyle({
required this.textStyle,
required this.padding,
});
final TextStyleCustomizer textStyle;
final PaddingCustomizer padding;
@override
HeadingPluginStyle copyWith({
TextStyleCustomizer? textStyle,
PaddingCustomizer? padding,
}) {
return HeadingPluginStyle(
textStyle: textStyle ?? this.textStyle,
padding: padding ?? this.padding,
);
}
@override
ThemeExtension<HeadingPluginStyle> lerp(
ThemeExtension<HeadingPluginStyle>? other, double t) {
if (other is! HeadingPluginStyle) {
return this;
}
return HeadingPluginStyle(
textStyle: other.textStyle,
padding: other.padding,
);
}
static final light = HeadingPluginStyle(
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
textStyle: (editorState, textNode) {
final headingToFontSize = {
'h1': 32.0,
'h2': 28.0,
'h3': 24.0,
'h4': 18.0,
'h5': 18.0,
'h6': 18.0,
};
final fontSize = headingToFontSize[textNode.attributes.heading] ?? 18.0;
return TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
);
},
);
static final dark = light;
}
class CheckboxPluginStyle extends ThemeExtension<CheckboxPluginStyle> {
const CheckboxPluginStyle({
required this.textStyle,
required this.padding,
required this.icon,
});
final TextStyleCustomizer textStyle;
final PaddingCustomizer padding;
final IconCustomizer icon;
@override
CheckboxPluginStyle copyWith({
TextStyleCustomizer? textStyle,
PaddingCustomizer? padding,
IconCustomizer? icon,
}) {
return CheckboxPluginStyle(
textStyle: textStyle ?? this.textStyle,
padding: padding ?? this.padding,
icon: icon ?? this.icon,
);
}
@override
ThemeExtension<CheckboxPluginStyle> lerp(
ThemeExtension<CheckboxPluginStyle>? other, double t) {
if (other is! CheckboxPluginStyle) {
return this;
}
return CheckboxPluginStyle(
textStyle: other.textStyle,
padding: other.padding,
icon: other.icon,
);
}
static final light = CheckboxPluginStyle(
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
textStyle: (editorState, textNode) => const TextStyle(),
icon: (editorState, textNode) {
final isCheck = textNode.attributes.check;
const iconSize = Size.square(20.0);
const iconPadding = EdgeInsets.only(right: 5.0);
return FlowySvg(
width: iconSize.width,
height: iconSize.height,
padding: iconPadding,
name: isCheck ? 'check' : 'uncheck',
);
},
);
static final dark = light;
}
class BulletedListPluginStyle extends ThemeExtension<BulletedListPluginStyle> {
const BulletedListPluginStyle({
required this.textStyle,
required this.padding,
required this.icon,
});
final TextStyleCustomizer textStyle;
final PaddingCustomizer padding;
final IconCustomizer icon;
@override
BulletedListPluginStyle copyWith({
TextStyleCustomizer? textStyle,
PaddingCustomizer? padding,
IconCustomizer? icon,
}) {
return BulletedListPluginStyle(
textStyle: textStyle ?? this.textStyle,
padding: padding ?? this.padding,
icon: icon ?? this.icon,
);
}
@override
ThemeExtension<BulletedListPluginStyle> lerp(
ThemeExtension<BulletedListPluginStyle>? other, double t) {
if (other is! BulletedListPluginStyle) {
return this;
}
return BulletedListPluginStyle(
textStyle: other.textStyle,
padding: other.padding,
icon: other.icon,
);
}
static final light = BulletedListPluginStyle(
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
textStyle: (_, __) => const TextStyle(),
icon: (_, __) {
const iconSize = Size.square(20.0);
const iconPadding = EdgeInsets.only(right: 5.0);
return FlowySvg(
width: iconSize.width,
height: iconSize.height,
padding: iconPadding,
color: Colors.black,
name: 'point',
);
},
);
static final dark = light.copyWith(icon: (_, __) {
const iconSize = Size.square(20.0);
const iconPadding = EdgeInsets.only(right: 5.0);
return FlowySvg(
width: iconSize.width,
height: iconSize.height,
padding: iconPadding,
color: Colors.white,
name: 'point',
);
});
}
class NumberListPluginStyle extends ThemeExtension<NumberListPluginStyle> {
const NumberListPluginStyle({
required this.textStyle,
required this.padding,
required this.icon,
});
final TextStyleCustomizer textStyle;
final PaddingCustomizer padding;
final IconCustomizer icon;
@override
NumberListPluginStyle copyWith({
TextStyleCustomizer? textStyle,
PaddingCustomizer? padding,
IconCustomizer? icon,
}) {
return NumberListPluginStyle(
textStyle: textStyle ?? this.textStyle,
padding: padding ?? this.padding,
icon: icon ?? this.icon,
);
}
@override
ThemeExtension<NumberListPluginStyle> lerp(
ThemeExtension<NumberListPluginStyle>? other,
double t,
) {
if (other is! NumberListPluginStyle) {
return this;
}
return NumberListPluginStyle(
textStyle: other.textStyle,
padding: other.padding,
icon: other.icon,
);
}
static final light = NumberListPluginStyle(
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
textStyle: (_, __) => const TextStyle(),
icon: (_, textNode) {
const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
return Container(
padding: iconPadding,
child: Text(
'${textNode.attributes.number.toString()}.',
style: const TextStyle(
fontSize: 16,
color: Colors.black,
),
),
);
},
);
static final dark = light.copyWith(icon: (editorState, textNode) {
const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
return Container(
padding: iconPadding,
child: Text(
'${textNode.attributes.number.toString()}.',
style: const TextStyle(
fontSize: 16,
color: Colors.white,
),
),
);
});
}
class QuotedTextPluginStyle extends ThemeExtension<QuotedTextPluginStyle> {
const QuotedTextPluginStyle({
required this.textStyle,
required this.padding,
required this.icon,
});
final TextStyleCustomizer textStyle;
final PaddingCustomizer padding;
final IconCustomizer icon;
@override
QuotedTextPluginStyle copyWith({
TextStyleCustomizer? textStyle,
PaddingCustomizer? padding,
IconCustomizer? icon,
}) {
return QuotedTextPluginStyle(
textStyle: textStyle ?? this.textStyle,
padding: padding ?? this.padding,
icon: icon ?? this.icon,
);
}
@override
ThemeExtension<QuotedTextPluginStyle> lerp(
ThemeExtension<QuotedTextPluginStyle>? other, double t) {
if (other is! QuotedTextPluginStyle) {
return this;
}
return QuotedTextPluginStyle(
textStyle: other.textStyle,
padding: other.padding,
icon: other.icon,
);
}
static final light = QuotedTextPluginStyle(
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
textStyle: (_, __) => const TextStyle(),
icon: (_, __) {
const iconSize = Size.square(20.0);
const iconPadding = EdgeInsets.only(right: 5.0);
return FlowySvg(
width: iconSize.width,
padding: iconPadding,
name: 'quote',
);
},
);
static final dark = light;
}

View File

@ -259,7 +259,7 @@ List<ToolbarItem> defaultToolbarItems = [
),
handler: (editorState, context) => formatHighlight(
editorState,
editorState.editorStyle.textStyle.highlightColorHex,
editorState.editorStyle.highlightColorHex!,
),
),
];
@ -348,6 +348,7 @@ void showLinkMenu(
child: Material(
child: LinkMenu(
linkText: linkText,
editorState: editorState,
onOpenLink: () async {
await safeLaunchUrl(linkText);
},

View File

@ -25,6 +25,7 @@ class ToolbarItemWidget extends StatelessWidget {
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: IconButton(
hoverColor: Colors.transparent,
highlightColor: Colors.transparent,
padding: EdgeInsets.zero,
icon: item.iconBuilder(isHighlight),

View File

@ -30,26 +30,43 @@ class ContextMenu extends StatelessWidget {
final children = <Widget>[];
for (var i = 0; i < items.length; i++) {
for (var j = 0; j < items[i].length; j++) {
var onHover = false;
children.add(
Material(
child: InkWell(
hoverColor: const Color(0xFFE0F8FF),
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
onTap: () {
items[i][j].onPressed(editorState);
onPressed();
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
items[i][j].name,
textAlign: TextAlign.start,
style: const TextStyle(fontSize: 14),
StatefulBuilder(
builder: (BuildContext context, setState) {
return Material(
color: editorState.editorStyle.selectionMenuBackgroundColor,
child: InkWell(
hoverColor:
editorState.editorStyle.selectionMenuItemSelectedColor,
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
onTap: () {
items[i][j].onPressed(editorState);
onPressed();
},
onHover: (value) => setState(() {
onHover = value;
}),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
items[i][j].name,
textAlign: TextAlign.start,
style: TextStyle(
fontSize: 14,
color: onHover
? editorState
.editorStyle.selectionMenuItemSelectedTextColor
: editorState
.editorStyle.selectionMenuItemTextColor,
),
),
),
),
),
),
);
},
),
);
}
@ -67,7 +84,7 @@ class ContextMenu extends StatelessWidget {
minWidth: 140,
),
decoration: BoxDecoration(
color: Colors.white,
color: editorState.editorStyle.selectionMenuBackgroundColor,
boxShadow: [
BoxShadow(
blurRadius: 5,

View File

@ -1,12 +1,9 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/flutter/overlay.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/style/editor_style.dart';
import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/render/editor/editor_entry.dart';
import 'package:appflowy_editor/src/render/rich_text/bulleted_list_text.dart';
import 'package:appflowy_editor/src/render/rich_text/checkbox_text.dart';
@ -14,12 +11,6 @@ import 'package:appflowy_editor/src/render/rich_text/heading_text.dart';
import 'package:appflowy_editor/src/render/rich_text/number_list_text.dart';
import 'package:appflowy_editor/src/render/rich_text/quoted_text.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text.dart';
import 'package:appflowy_editor/src/service/input_service.dart';
import 'package:appflowy_editor/src/service/keyboard_service.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:appflowy_editor/src/service/scroll_service.dart';
import 'package:appflowy_editor/src/service/selection_service.dart';
import 'package:appflowy_editor/src/service/toolbar_service.dart';
NodeWidgetBuilders defaultBuilders = {
'editor': EditorEntryWidgetBuilder(),
@ -33,15 +24,21 @@ NodeWidgetBuilders defaultBuilders = {
};
class AppFlowyEditor extends StatefulWidget {
const AppFlowyEditor({
AppFlowyEditor({
Key? key,
required this.editorState,
this.customBuilders = const {},
this.shortcutEvents = const [],
this.selectionMenuItems = const [],
this.editable = true,
required this.editorStyle,
}) : super(key: key);
ThemeData? themeData,
}) : super(key: key) {
this.themeData = themeData ??
ThemeData.light().copyWith(extensions: [
...lightEditorStyleExtension,
...lightPlguinStyleExtension,
]);
}
final EditorState editorState;
@ -53,7 +50,7 @@ class AppFlowyEditor extends StatefulWidget {
final List<SelectionMenuItem> selectionMenuItems;
final EditorStyle editorStyle;
late final ThemeData themeData;
final bool editable;
@ -65,13 +62,15 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
Widget? services;
EditorState get editorState => widget.editorState;
EditorStyle get editorStyle =>
editorState.themeData.extension<EditorStyle>() ?? EditorStyle.light;
@override
void initState() {
super.initState();
editorState.selectionMenuItems = widget.selectionMenuItems;
editorState.editorStyle = widget.editorStyle;
editorState.themeData = widget.themeData;
editorState.service.renderPluginService = _createRenderPlugin();
editorState.editable = widget.editable;
}
@ -85,7 +84,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
editorState.service.renderPluginService = _createRenderPlugin();
}
editorState.editorStyle = widget.editorStyle;
editorState.themeData = widget.themeData;
editorState.editable = widget.editable;
services = null;
}
@ -102,38 +101,41 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
);
}
AppFlowyScroll _buildServices(BuildContext context) {
return AppFlowyScroll(
key: editorState.service.scrollServiceKey,
child: Padding(
padding: widget.editorStyle.padding,
child: AppFlowySelection(
key: editorState.service.selectionServiceKey,
cursorColor: widget.editorStyle.cursorColor,
selectionColor: widget.editorStyle.selectionColor,
editorState: editorState,
editable: widget.editable,
child: AppFlowyInput(
key: editorState.service.inputServiceKey,
Widget _buildServices(BuildContext context) {
return Theme(
data: widget.themeData,
child: AppFlowyScroll(
key: editorState.service.scrollServiceKey,
child: Padding(
padding: editorStyle.padding!,
child: AppFlowySelection(
key: editorState.service.selectionServiceKey,
cursorColor: editorStyle.cursorColor!,
selectionColor: editorStyle.selectionColor!,
editorState: editorState,
editable: widget.editable,
child: AppFlowyKeyboard(
key: editorState.service.keyboardServiceKey,
editable: widget.editable,
shortcutEvents: [
...widget.shortcutEvents,
...builtInShortcutEvents,
],
child: AppFlowyInput(
key: editorState.service.inputServiceKey,
editorState: editorState,
child: FlowyToolbar(
key: editorState.service.toolbarServiceKey,
editable: widget.editable,
child: AppFlowyKeyboard(
key: editorState.service.keyboardServiceKey,
editable: widget.editable,
shortcutEvents: [
...widget.shortcutEvents,
...builtInShortcutEvents,
],
editorState: editorState,
child:
editorState.service.renderPluginService.buildPluginWidget(
NodeWidgetContext(
context: context,
node: editorState.document.root,
editorState: editorState,
child: FlowyToolbar(
key: editorState.service.toolbarServiceKey,
editorState: editorState,
child:
editorState.service.renderPluginService.buildPluginWidget(
NodeWidgetContext(
context: context,
node: editorState.document.root,
editorState: editorState,
),
),
),
),

View File

@ -41,7 +41,10 @@ void _handleCopy(EditorState editorState) async {
Log.keyboard.debug('copy html: $htmlString');
RichClipboard.setData(RichClipboardData(
html: htmlString,
text: textNode.toPlainText(),
text: textNode.toPlainText().substring(
selection.startIndex,
selection.endIndex,
),
));
} else {
Log.keyboard.debug('unimplemented: copy non-text');
@ -63,9 +66,19 @@ void _handleCopy(EditorState editorState) async {
startOffset: selection.start.offset,
endOffset: selection.end.offset,
).toHTMLString();
final text = nodes
.map((node) => node is TextNode ? node.toPlainText() : '\n')
.join('\n');
var text = '';
for (final node in nodes) {
if (node is TextNode) {
if (node.path == selection.start.path) {
text += node.toPlainText().substring(selection.start.offset);
} else if (node.path == selection.end.path) {
text += node.toPlainText().substring(0, selection.end.offset);
} else {
text += node.toPlainText();
}
}
text += '\n';
}
RichClipboard.setData(RichClipboardData(html: html, text: text));
}

View File

@ -57,7 +57,7 @@ ShortcutEventHandler formatHighlightEventHandler = (editorState, event) {
}
formatHighlight(
editorState,
editorState.editorStyle.textStyle.highlightColorHex,
editorState.editorStyle.highlightColorHex!,
);
return KeyEventResult.handled;
};

View File

@ -39,7 +39,6 @@ class EditorWidgetTester {
home: Scaffold(
body: AppFlowyEditor(
editorState: _editorState,
editorStyle: EditorStyle.defaultStyle(),
),
),
);

View File

@ -49,10 +49,11 @@ void main() async {
final editorRect = tester.getRect(editorFinder);
final leftImageRect = tester.getRect(imageFinder.at(0));
expect(leftImageRect.left, editor.editorState.editorStyle.padding.left);
expect(
leftImageRect.left, editor.editorState.editorStyle.padding!.left);
final rightImageRect = tester.getRect(imageFinder.at(2));
expect(rightImageRect.right,
editorRect.right - editor.editorState.editorStyle.padding.right);
editorRect.right - editor.editorState.editorStyle.padding!.right);
final centerImageRect = tester.getRect(imageFinder.at(1));
expect(centerImageRect.left,
(leftImageRect.left + rightImageRect.left) / 2.0);

View File

@ -137,7 +137,7 @@ Future<EditorWidgetTester> _prepare(WidgetTester tester) async {
);
for (final item in defaultSelectionMenuItems) {
expect(find.byWidget(item.icon), findsOneWidget);
expect(find.text(item.name()), findsOneWidget);
}
return Future.value(editor);

View File

@ -30,7 +30,7 @@ void main() async {
);
for (final item in defaultSelectionMenuItems) {
expect(find.byWidget(item.icon), findsOneWidget);
expect(find.text(item.name()), findsOneWidget);
}
await editor.updateSelection(Selection.single(path: [1], startOffset: 0));