mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #1364 from LucasXu0/adapt_dart_mode
feat: adapt dark mode
This commit is contained in:
commit
c7c9048fe3
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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'],
|
||||
|
@ -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,
|
||||
);
|
||||
};
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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),
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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) =>
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
@ -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;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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);
|
||||
},
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,7 @@ ShortcutEventHandler formatHighlightEventHandler = (editorState, event) {
|
||||
}
|
||||
formatHighlight(
|
||||
editorState,
|
||||
editorState.editorStyle.textStyle.highlightColorHex,
|
||||
editorState.editorStyle.highlightColorHex!,
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
@ -39,7 +39,6 @@ class EditorWidgetTester {
|
||||
home: Scaffold(
|
||||
body: AppFlowyEditor(
|
||||
editorState: _editorState,
|
||||
editorStyle: EditorStyle.defaultStyle(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
Loading…
Reference in New Issue
Block a user