Compare commits

...
Author SHA1 Message Date
Lucas.Xu d24625c053 Merge pull request #1375 from LucasXu0/release/0.0.6.1
Release 0.0.6.1
2022-10-27 14:12:44 +08:00
nathan b293150b19 test: fix test 2022-10-27 07:11:41 +08:00
Lucas.Xu 3bec4caa53 chore: enable CI check for release/* branch 2022-10-26 21:40:26 +08:00
Lucas.Xu d0d7529d28 chore: update changlog and version 2022-10-26 21:36:44 +08:00
appflowy 6884e97146 fix: group by field 2022-10-26 21:28:48 +08:00
Lucas.Xu 9477f00831 fix: selection menu show position 2022-10-26 21:26:52 +08:00
Lucas.Xu e426aa5195 fix: copy link and code style 2022-10-26 20:24:55 +08:00
Lucas.Xu ceb309ac50 feat: adapt dark mode 2022-10-26 20:24:30 +08:00
50 changed files with 1072 additions and 580 deletions

No files matched your search

@@ -4,10 +4,12 @@ on:
push:
branches:
- "main"
- "release/*"
pull_request:
branches:
- "main"
- "release/*"
paths:
- "frontend/app_flowy/packages/appflowy_editor/**"
+2
View File
@@ -4,10 +4,12 @@ on:
push:
branches:
- "main"
- "release/*"
pull_request:
branches:
- "main"
- "release/*"
jobs:
build:
+2
View File
@@ -9,12 +9,14 @@ on:
push:
branches:
- "main"
- "release/*"
paths:
- "frontend/app_flowy/**"
pull_request:
branches:
- "main"
- "release/*"
paths:
- "frontend/app_flowy/**"
+2
View File
@@ -4,12 +4,14 @@ on:
push:
branches:
- "main"
- "release/*"
paths:
- "frontend/app_flowy/**"
pull_request:
branches:
- "main"
- "release/*"
paths:
- "frontend/app_flowy/**"
+2
View File
@@ -4,6 +4,7 @@ on:
push:
branches:
- "main"
- "release/*"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
@@ -11,6 +12,7 @@ on:
pull_request:
branches:
- "main"
- "release/*"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
+2
View File
@@ -4,6 +4,7 @@ on:
push:
branches:
- "main"
- "release/*"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
@@ -11,6 +12,7 @@ on:
pull_request:
branches:
- "main"
- "release/*"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
+2
View File
@@ -4,6 +4,7 @@ on:
push:
branches:
- "main"
- "release/*"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
@@ -11,6 +12,7 @@ on:
pull_request:
branches:
- "main"
- "release/*"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
+8
View File
@@ -1,5 +1,13 @@
# Release Notes
## Version 0.0.6.1 - 10/26/2022
New features
- Optimzie appflowy_editor dark mode style
### Bug Fixes
- Unable to copy the text with checkbox or link style
## Version 0.0.6 - 10/23/2022
New features
+1 -1
View File
@@ -22,7 +22,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
CURRENT_APP_VERSION = "0.0.6"
CURRENT_APP_VERSION = "0.0.6.1"
FEATURES = "flutter"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
@@ -34,7 +34,8 @@ class BoardDataController {
// key: the block id
final LinkedHashMap<String, GridBlockCache> _blocks;
LinkedHashMap<String, GridBlockCache> get blocks => _blocks;
UnmodifiableMapView<String, GridBlockCache> get blocks =>
UnmodifiableMapView(_blocks);
OnFieldsChanged? _onFieldsChanged;
OnGridChanged? _onGridChanged;
@@ -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;
}
}
@@ -241,6 +241,8 @@ class HTMLToNodesConverter {
} else if (element.localName == HTMLTag.del) {
delta.insert(element.text,
attributes: {BuiltInAttributeKey.strikethrough: true});
} else if (element.localName == HTMLTag.code) {
delta.insert(element.text, attributes: {BuiltInAttributeKey.code: true});
} else {
delta.insert(element.text);
}
@@ -276,11 +278,13 @@ class HTMLToNodesConverter {
}
}
final textNode = TextNode(delta: delta, attributes: attributes);
if (isCheckbox) {
textNode.attributes["subtype"] = BuiltInAttributeKey.checkbox;
textNode.attributes["checkbox"] = checked;
}
final textNode = TextNode(delta: delta, attributes: {
if (attributes != null) ...attributes,
if (isCheckbox) ...{
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
BuiltInAttributeKey.checkbox: checked,
}
});
return textNode;
}
@@ -557,6 +561,17 @@ class NodesToHTMLConverter {
final strong = html.Element.tag(HTMLTag.del);
strong.append(html.Text(op.text));
childNodes.add(strong);
} else if (attributes.length == 1 &&
attributes[BuiltInAttributeKey.code] == true) {
final code = html.Element.tag(HTMLTag.code);
code.append(html.Text(op.text));
childNodes.add(code);
} else if (attributes.length == 1 &&
attributes[BuiltInAttributeKey.href] != null) {
final anchor = html.Element.tag(HTMLTag.anchor);
anchor.attributes["href"] = attributes[BuiltInAttributeKey.href];
anchor.append(html.Text(op.text));
childNodes.add(anchor);
} else {
final span = html.Element.tag(HTMLTag.span);
final cssString = _attributesToCssStyle(attributes);
@@ -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,
@@ -92,7 +98,7 @@ class _ImageUploadMenuState extends State<ImageUploadMenu> {
color: Colors.black.withOpacity(0.1),
),
],
borderRadius: BorderRadius.circular(6.0),
// borderRadius: BorderRadius.circular(6.0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -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;
});
},
),
),
@@ -60,20 +60,29 @@ class SelectionMenu implements SelectionMenuService {
// but the coordinates of overlay are not properly converted currently.
// Just subtract the padding here as a result.
const menuHeight = 200.0;
const menuOffset = Offset(10, 10);
final baseOffset =
const menuOffset = Offset(0, 10);
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 : MediaQuery.of(context).size.height - offset.dy,
left: offset.dx,
child: SelectionMenuWidget(
items: [
@@ -131,7 +140,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 +149,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 +158,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 +167,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 +176,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 +192,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 +201,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 +210,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 +219,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));
@@ -1,8 +1,12 @@
import 'dart:collection';
import 'package:app_flowy/plugins/board/application/board_data_controller.dart';
import 'package:app_flowy/plugins/board/board.dart';
import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart';
import 'package:app_flowy/plugins/grid/application/row/row_bloc.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
@@ -16,29 +20,90 @@ import '../../util.dart';
/// Create a empty Grid for test
class AppFlowyGridTest {
final AppFlowyUnitTest _inner;
final AppFlowyUnitTest unitTest;
late ViewPB gridView;
late GridDataController _dataController;
GridDataController? _gridDataController;
BoardDataController? _boardDataController;
AppFlowyGridTest(AppFlowyUnitTest unitTest) : _inner = unitTest;
AppFlowyGridTest({required this.unitTest});
static Future<AppFlowyGridTest> ensureInitialized() async {
final inner = await AppFlowyUnitTest.ensureInitialized();
return AppFlowyGridTest(inner);
return AppFlowyGridTest(unitTest: inner);
}
List<RowInfo> get rowInfos => _dataController.rowInfos;
List<RowInfo> get rowInfos {
if (_gridDataController != null) {
return _gridDataController!.rowInfos;
}
UnmodifiableMapView<String, GridBlockCache> get blocks =>
_dataController.blocks;
if (_boardDataController != null) {
return _boardDataController!.rowInfos;
}
List<GridFieldContext> get fieldContexts =>
_dataController.fieldController.fieldContexts;
throw Exception();
}
GridFieldController get fieldController => _dataController.fieldController;
UnmodifiableMapView<String, GridBlockCache> get blocks {
if (_gridDataController != null) {
return _gridDataController!.blocks;
}
if (_boardDataController != null) {
return _boardDataController!.blocks;
}
throw Exception();
}
List<GridFieldContext> get fieldContexts => fieldController.fieldContexts;
GridFieldController get fieldController {
if (_gridDataController != null) {
return _gridDataController!.fieldController;
}
if (_boardDataController != null) {
return _boardDataController!.fieldController;
}
throw Exception();
}
Future<void> createRow() async {
await _dataController.createRow();
if (_gridDataController != null) {
return _gridDataController!.createRow();
}
throw Exception();
}
FieldEditorBloc createFieldEditor({
GridFieldContext? fieldContext,
}) {
IFieldTypeOptionLoader loader;
if (fieldContext == null) {
loader = NewFieldTypeOptionLoader(gridId: gridView.id);
} else {
loader =
FieldTypeOptionLoader(gridId: gridView.id, field: fieldContext.field);
}
final editorBloc = FieldEditorBloc(
fieldName: fieldContext?.name ?? '',
isGroupField: fieldContext?.isGroupField ?? false,
loader: loader,
gridId: gridView.id,
);
return editorBloc;
}
Future<FieldEditorBloc> createFieldFromType(FieldType fieldType) async {
final editor = createFieldEditor()..add(const FieldEditorEvent.initial());
await gridResponseFuture();
editor.dataController.switchToField(fieldType);
await gridResponseFuture();
return Future(() => editor);
}
GridFieldContext singleSelectFieldContext() {
@@ -53,7 +118,7 @@ class AppFlowyGridTest {
}
Future<void> createTestGrid() async {
final app = await _inner.createTestApp();
final app = await unitTest.createTestApp();
final builder = GridPluginBuilder();
final result = await AppService().createView(
appId: app.id,
@@ -65,13 +130,34 @@ class AppFlowyGridTest {
await result.fold(
(view) async {
gridView = view;
_dataController = GridDataController(view: view);
final result = await _dataController.loadData();
_gridDataController = GridDataController(view: view);
final result = await _gridDataController!.loadData();
result.fold((l) => null, (r) => throw Exception(r));
},
(error) {},
);
}
Future<void> createTestBoard() async {
final app = await unitTest.createTestApp();
final builder = BoardPluginBuilder();
final result = await AppService().createView(
appId: app.id,
name: "Test Board",
dataFormatType: builder.dataFormatType,
pluginType: builder.pluginType,
layoutType: builder.layoutType!,
);
await result.fold(
(view) async {
_boardDataController = BoardDataController(view: view);
final result = await _boardDataController!.loadData();
result.fold((l) => null, (r) => throw Exception(r));
gridView = view;
},
(error) {},
);
}
}
/// Create a new Grid for cell test
@@ -101,7 +187,7 @@ class AppFlowyGridCellTest {
final rowDataController = GridRowDataController(
rowInfo: rowInfo,
fieldController: _gridTest._dataController.fieldController,
fieldController: _gridTest._gridDataController!.fieldController,
rowCache: rowCache!,
);
@@ -90,6 +90,7 @@ void main() {
"delete the document",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) => bloc.add(AppEvent.deleteView(view.id)),
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.views.isEmpty);
},
@@ -465,19 +465,6 @@ pub struct FieldChangesetParams {
pub type_option_data: Option<Vec<u8>>,
}
impl FieldChangesetParams {
pub fn has_changes(&self) -> bool {
self.name.is_some()
|| self.desc.is_some()
|| self.field_type.is_some()
|| self.frozen.is_some()
|| self.type_option_data.is_some()
|| self.frozen.is_some()
|| self.visibility.is_some()
|| self.width.is_some()
}
}
/// Certain field types have user-defined options such as color, date format, number format,
/// or a list of values for a multi-select list. These options are defined within a specialization
/// of the FieldTypeOption class.
@@ -109,10 +109,6 @@ impl GridRevisionEditor {
field_id: &str,
type_option_data: Vec<u8>,
) -> FlowyResult<()> {
if type_option_data.is_empty() {
return Ok(());
}
let result = self.get_field_rev(field_id).await;
if result.is_none() {
tracing::warn!("Can't find the field with id: {}", field_id);
@@ -307,10 +303,6 @@ impl GridRevisionEditor {
#[tracing::instrument(level = "debug", skip_all, err)]
async fn update_field_rev(&self, params: FieldChangesetParams, field_type: FieldType) -> FlowyResult<()> {
let mut is_type_option_changed = false;
if !params.has_changes() {
return Ok(());
}
let _ = self
.modify(|grid| {
let changeset = grid.modify_field(&params.field_id, |field| {
@@ -334,11 +326,11 @@ impl GridRevisionEditor {
}
if let Some(type_option_data) = params.type_option_data {
let deserializer = TypeOptionJsonDeserializer(field_type);
is_type_option_changed = true;
match deserializer.deserialize(type_option_data) {
Ok(json_str) => {
let field_type = field.ty;
field.insert_type_option_str(&field_type, json_str);
is_type_option_changed = true;
}
Err(err) => {
tracing::error!("Deserialize data to type option json failed: {}", err);