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) {
|
Widget _renderAppFlowyEditor(EditorState editorState) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
final editor = AppFlowyEditor(
|
final editor = AppFlowyEditor(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
editorStyle: customEditorStyle(context),
|
|
||||||
customBuilders: {
|
customBuilders: {
|
||||||
'horizontal_rule': HorizontalRuleWidgetBuilder(),
|
'horizontal_rule': HorizontalRuleWidgetBuilder(),
|
||||||
},
|
},
|
||||||
shortcutEvents: [
|
shortcutEvents: [
|
||||||
insertHorizontalRule,
|
insertHorizontalRule,
|
||||||
],
|
],
|
||||||
|
themeData: theme.copyWith(extensions: [
|
||||||
|
...theme.extensions.values,
|
||||||
|
customEditorTheme(context),
|
||||||
|
...customPluginTheme(context),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: SizedBox.expand(
|
child: SizedBox.expand(
|
||||||
|
@ -3,59 +3,63 @@ import 'package:flowy_infra/theme.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
EditorStyle customEditorStyle(BuildContext context) {
|
const _baseFontSize = 14.0;
|
||||||
|
|
||||||
|
EditorStyle customEditorTheme(BuildContext context) {
|
||||||
final theme = context.watch<AppTheme>();
|
final theme = context.watch<AppTheme>();
|
||||||
const baseFontSize = 14.0;
|
|
||||||
const basePadding = 12.0;
|
var editorStyle = theme.isDark ? EditorStyle.dark : EditorStyle.light;
|
||||||
var textStyle = theme.isDark
|
editorStyle = editorStyle.copyWith(
|
||||||
? BuiltInTextStyle.builtInDarkMode()
|
textStyle: editorStyle.textStyle?.copyWith(
|
||||||
: BuiltInTextStyle.builtIn();
|
|
||||||
textStyle = textStyle.copyWith(
|
|
||||||
defaultTextStyle: textStyle.defaultTextStyle.copyWith(
|
|
||||||
fontFamily: 'poppins',
|
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,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return EditorStyle.defaultStyle().copyWith(
|
return editorStyle;
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 80),
|
}
|
||||||
textStyle: textStyle,
|
|
||||||
pluginStyles: {
|
Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
|
||||||
'text/heading': builtInPluginStyle
|
final theme = context.watch<AppTheme>();
|
||||||
..update(
|
const basePadding = 12.0;
|
||||||
'textStyle',
|
var headingPluginStyle =
|
||||||
(_) => (EditorState editorState, Node node) {
|
theme.isDark ? HeadingPluginStyle.dark : HeadingPluginStyle.light;
|
||||||
final headingToFontSize = {
|
headingPluginStyle = headingPluginStyle.copyWith(
|
||||||
'h1': baseFontSize + 12,
|
textStyle: (EditorState editorState, Node node) {
|
||||||
'h2': baseFontSize + 8,
|
final headingToFontSize = {
|
||||||
'h3': baseFontSize + 4,
|
'h1': _baseFontSize + 12,
|
||||||
'h4': baseFontSize,
|
'h2': _baseFontSize + 8,
|
||||||
'h5': baseFontSize,
|
'h3': _baseFontSize + 4,
|
||||||
'h6': baseFontSize,
|
'h4': _baseFontSize,
|
||||||
};
|
'h5': _baseFontSize,
|
||||||
final fontSize =
|
'h6': _baseFontSize,
|
||||||
headingToFontSize[node.attributes.heading] ?? baseFontSize;
|
};
|
||||||
return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
|
final fontSize =
|
||||||
},
|
headingToFontSize[node.attributes.heading] ?? _baseFontSize;
|
||||||
)
|
return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
|
||||||
..update(
|
},
|
||||||
'padding',
|
padding: (EditorState editorState, Node node) {
|
||||||
(_) => (EditorState editorState, Node node) {
|
final headingToPadding = {
|
||||||
final headingToPadding = {
|
'h1': basePadding + 6,
|
||||||
'h1': basePadding + 6,
|
'h2': basePadding + 4,
|
||||||
'h2': basePadding + 4,
|
'h3': basePadding + 2,
|
||||||
'h3': basePadding + 2,
|
'h4': basePadding,
|
||||||
'h4': basePadding,
|
'h5': basePadding,
|
||||||
'h5': basePadding,
|
'h6': basePadding,
|
||||||
'h6': basePadding,
|
};
|
||||||
};
|
final padding = headingToPadding[node.attributes.heading] ?? basePadding;
|
||||||
final padding =
|
return EdgeInsets.only(bottom: 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(
|
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
|
||||||
name: () => 'Horizontal rule',
|
name: () => 'Horizontal rule',
|
||||||
icon: const Icon(
|
icon: (editorState, onSelected) => Icon(
|
||||||
Icons.horizontal_rule,
|
Icons.horizontal_rule,
|
||||||
color: Colors.black,
|
color: onSelected
|
||||||
|
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||||
|
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||||
size: 18.0,
|
size: 18.0,
|
||||||
),
|
),
|
||||||
keywords: ['horizontal rule'],
|
keywords: ['horizontal rule'],
|
||||||
|
@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
|
|||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:universal_html/html.dart' as html;
|
import 'package:universal_html/html.dart' as html;
|
||||||
|
|
||||||
@ -38,7 +37,10 @@ class MyApp extends StatelessWidget {
|
|||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
primarySwatch: Colors.blue,
|
primarySwatch: Colors.blue,
|
||||||
|
// extensions: [HeadingPluginStyle.light],
|
||||||
),
|
),
|
||||||
|
darkTheme: ThemeData.dark(),
|
||||||
|
themeMode: ThemeMode.dark,
|
||||||
home: const MyHomePage(title: 'AppFlowyEditor Example'),
|
home: const MyHomePage(title: 'AppFlowyEditor Example'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -56,7 +58,6 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
int _pageIndex = 0;
|
int _pageIndex = 0;
|
||||||
EditorState? _editorState;
|
EditorState? _editorState;
|
||||||
bool darkMode = false;
|
bool darkMode = false;
|
||||||
EditorStyle _editorStyle = EditorStyle.defaultStyle();
|
|
||||||
Future<String>? _jsonString;
|
Future<String>? _jsonString;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -125,12 +126,31 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
_editorState!.transactionStream.listen((event) {
|
_editorState!.transactionStream.listen((event) {
|
||||||
debugPrint('Transaction: ${event.toJson()}');
|
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(
|
return Container(
|
||||||
color: darkMode ? Colors.black : Colors.white,
|
color: darkMode ? Colors.black : Colors.white,
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
child: AppFlowyEditor(
|
child: AppFlowyEditor(
|
||||||
editorState: _editorState!,
|
editorState: _editorState!,
|
||||||
editorStyle: _editorStyle,
|
themeData: themeData,
|
||||||
editable: true,
|
editable: true,
|
||||||
customBuilders: {
|
customBuilders: {
|
||||||
'text/code_block': CodeBlockNodeWidgetBuilder(),
|
'text/code_block': CodeBlockNodeWidgetBuilder(),
|
||||||
@ -186,8 +206,6 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
icon: const Icon(Icons.color_lens),
|
icon: const Icon(Icons.color_lens),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_editorStyle =
|
|
||||||
darkMode ? EditorStyle.defaultStyle() : _customizedStyle();
|
|
||||||
darkMode = !darkMode;
|
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(
|
SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
|
||||||
name: () => 'Code Block',
|
name: () => 'Code Block',
|
||||||
icon: const Icon(
|
icon: (_, __) => const Icon(
|
||||||
Icons.abc,
|
Icons.abc,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
size: 18.0,
|
size: 18.0,
|
||||||
@ -167,7 +167,7 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
|
|||||||
textNode: widget.textNode,
|
textNode: widget.textNode,
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
textSpanDecorator: (textSpan) => TextSpan(
|
textSpanDecorator: (textSpan) => TextSpan(
|
||||||
style: widget.editorState.editorStyle.textStyle.defaultTextStyle,
|
style: widget.editorState.editorStyle.textStyle,
|
||||||
children: codeTextSpan,
|
children: codeTextSpan,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -38,7 +38,7 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
|
|||||||
|
|
||||||
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
|
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
|
||||||
name: () => 'Horizontal rule',
|
name: () => 'Horizontal rule',
|
||||||
icon: const Icon(
|
icon: (_, __) => const Icon(
|
||||||
Icons.horizontal_rule,
|
Icons.horizontal_rule,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
size: 18.0,
|
size: 18.0,
|
||||||
|
@ -6,7 +6,7 @@ import 'package:flutter_math_fork/flutter_math.dart';
|
|||||||
|
|
||||||
SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
|
SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
|
||||||
name: () => 'Tex',
|
name: () => 'Tex',
|
||||||
icon: const Icon(
|
icon: (_, __) => const Icon(
|
||||||
Icons.text_fields_rounded,
|
Icons.text_fields_rounded,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
size: 18.0,
|
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/rich_text/flowy_rich_text.dart';
|
||||||
export 'src/render/selection_menu/selection_menu_widget.dart';
|
export 'src/render/selection_menu/selection_menu_widget.dart';
|
||||||
export 'src/l10n/l10n.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.
|
/// Stores the selection menu items.
|
||||||
List<SelectionMenuItem> selectionMenuItems = [];
|
List<SelectionMenuItem> selectionMenuItems = [];
|
||||||
|
|
||||||
/// Stores the editor style.
|
|
||||||
EditorStyle editorStyle = EditorStyle.defaultStyle();
|
|
||||||
|
|
||||||
/// Operation stream.
|
/// Operation stream.
|
||||||
Stream<Transaction> get transactionStream => _observer.stream;
|
Stream<Transaction> get transactionStream => _observer.stream;
|
||||||
final StreamController<Transaction> _observer = StreamController.broadcast();
|
final StreamController<Transaction> _observer = StreamController.broadcast();
|
||||||
|
|
||||||
|
late ThemeData themeData;
|
||||||
|
EditorStyle get editorStyle =>
|
||||||
|
themeData.extension<EditorStyle>() ?? EditorStyle.light;
|
||||||
|
|
||||||
final UndoManager undoManager = UndoManager();
|
final UndoManager undoManager = UndoManager();
|
||||||
Selection? _cursorSelection;
|
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/editor_state.dart';
|
||||||
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
||||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.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';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
OverlayEntry? _imageUploadMenu;
|
OverlayEntry? _imageUploadMenu;
|
||||||
@ -20,6 +21,7 @@ void showImageUploadMenu(
|
|||||||
left: menuService.topLeft.dx,
|
left: menuService.topLeft.dx,
|
||||||
child: Material(
|
child: Material(
|
||||||
child: ImageUploadMenu(
|
child: ImageUploadMenu(
|
||||||
|
editorState: editorState,
|
||||||
onSubmitted: (text) {
|
onSubmitted: (text) {
|
||||||
// _dismissImageUploadMenu();
|
// _dismissImageUploadMenu();
|
||||||
editorState.insertImageNode(text);
|
editorState.insertImageNode(text);
|
||||||
@ -53,10 +55,12 @@ class ImageUploadMenu extends StatefulWidget {
|
|||||||
Key? key,
|
Key? key,
|
||||||
required this.onSubmitted,
|
required this.onSubmitted,
|
||||||
required this.onUpload,
|
required this.onUpload,
|
||||||
|
this.editorState,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final void Function(String text) onSubmitted;
|
final void Function(String text) onSubmitted;
|
||||||
final void Function(String text) onUpload;
|
final void Function(String text) onUpload;
|
||||||
|
final EditorState? editorState;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ImageUploadMenu> createState() => _ImageUploadMenuState();
|
State<ImageUploadMenu> createState() => _ImageUploadMenuState();
|
||||||
@ -66,6 +70,8 @@ class _ImageUploadMenuState extends State<ImageUploadMenu> {
|
|||||||
final _textEditingController = TextEditingController();
|
final _textEditingController = TextEditingController();
|
||||||
final _focusNode = FocusNode();
|
final _focusNode = FocusNode();
|
||||||
|
|
||||||
|
EditorStyle? get style => widget.editorState?.editorStyle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -84,7 +90,7 @@ class _ImageUploadMenuState extends State<ImageUploadMenu> {
|
|||||||
width: 300,
|
width: 300,
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: style?.selectionMenuBackgroundColor ?? Colors.white,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
blurRadius: 5,
|
blurRadius: 5,
|
||||||
@ -108,12 +114,12 @@ class _ImageUploadMenuState extends State<ImageUploadMenu> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeader(BuildContext context) {
|
Widget _buildHeader(BuildContext context) {
|
||||||
return const Text(
|
return Text(
|
||||||
'URL Image',
|
'URL Image',
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
color: Colors.black,
|
color: style?.selectionMenuItemTextColor ?? Colors.black,
|
||||||
fontWeight: FontWeight.w500,
|
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/infra/flowy_svg.dart';
|
||||||
|
import 'package:appflowy_editor/src/render/style/editor_style.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class LinkMenu extends StatefulWidget {
|
class LinkMenu extends StatefulWidget {
|
||||||
const LinkMenu({
|
const LinkMenu({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.linkText,
|
this.linkText,
|
||||||
|
this.editorState,
|
||||||
required this.onSubmitted,
|
required this.onSubmitted,
|
||||||
required this.onOpenLink,
|
required this.onOpenLink,
|
||||||
required this.onCopyLink,
|
required this.onCopyLink,
|
||||||
@ -13,6 +16,7 @@ class LinkMenu extends StatefulWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String? linkText;
|
final String? linkText;
|
||||||
|
final EditorState? editorState;
|
||||||
final void Function(String text) onSubmitted;
|
final void Function(String text) onSubmitted;
|
||||||
final VoidCallback onOpenLink;
|
final VoidCallback onOpenLink;
|
||||||
final VoidCallback onCopyLink;
|
final VoidCallback onCopyLink;
|
||||||
@ -27,6 +31,8 @@ class _LinkMenuState extends State<LinkMenu> {
|
|||||||
final _textEditingController = TextEditingController();
|
final _textEditingController = TextEditingController();
|
||||||
final _focusNode = FocusNode();
|
final _focusNode = FocusNode();
|
||||||
|
|
||||||
|
EditorStyle? get style => widget.editorState?.editorStyle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -48,7 +54,7 @@ class _LinkMenuState extends State<LinkMenu> {
|
|||||||
width: 350,
|
width: 350,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: style?.selectionMenuBackgroundColor ?? Colors.white,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
blurRadius: 5,
|
blurRadius: 5,
|
||||||
@ -71,17 +77,19 @@ class _LinkMenuState extends State<LinkMenu> {
|
|||||||
if (widget.linkText != null) ...[
|
if (widget.linkText != null) ...[
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
iconName: 'link',
|
iconName: 'link',
|
||||||
|
color: style?.selectionMenuItemIconColor,
|
||||||
text: 'Open link',
|
text: 'Open link',
|
||||||
onPressed: widget.onOpenLink,
|
onPressed: widget.onOpenLink,
|
||||||
),
|
),
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
iconName: 'copy',
|
iconName: 'copy',
|
||||||
color: Colors.black,
|
color: style?.selectionMenuItemIconColor,
|
||||||
text: 'Copy link',
|
text: 'Copy link',
|
||||||
onPressed: widget.onCopyLink,
|
onPressed: widget.onCopyLink,
|
||||||
),
|
),
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
iconName: 'delete',
|
iconName: 'delete',
|
||||||
|
color: style?.selectionMenuItemIconColor,
|
||||||
text: 'Remove link',
|
text: 'Remove link',
|
||||||
onPressed: widget.onRemoveLink,
|
onPressed: widget.onRemoveLink,
|
||||||
),
|
),
|
||||||
@ -154,8 +162,8 @@ class _LinkMenuState extends State<LinkMenu> {
|
|||||||
label: Text(
|
label: Text(
|
||||||
text,
|
text,
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: style?.selectionMenuItemTextColor ?? Colors.black,
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -10,56 +10,6 @@ abstract class BuiltInTextWidget extends StatefulWidget {
|
|||||||
TextNode get textNode;
|
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>
|
mixin BuiltInTextWidgetMixin<T extends BuiltInTextWidget> on State<T>
|
||||||
implements DefaultSelectable {
|
implements DefaultSelectable {
|
||||||
@override
|
@override
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||||
import 'package:appflowy_editor/src/editor_state.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/built_in_text_widget.dart';
|
||||||
import 'package:appflowy_editor/src/render/rich_text/default_selectable.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/rich_text/flowy_rich_text.dart';
|
||||||
import 'package:appflowy_editor/src/render/selection/selectable.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:appflowy_editor/src/service/render_plugin_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/text_style_extension.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> {
|
class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
||||||
@override
|
@override
|
||||||
@ -45,11 +46,7 @@ class BulletedListTextNodeWidget extends BuiltInTextWidget {
|
|||||||
// customize
|
// customize
|
||||||
|
|
||||||
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
||||||
with
|
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
|
||||||
SelectableMixin,
|
|
||||||
DefaultSelectable,
|
|
||||||
BuiltInStyleMixin,
|
|
||||||
BuiltInTextWidgetMixin {
|
|
||||||
@override
|
@override
|
||||||
final iconKey = GlobalKey();
|
final iconKey = GlobalKey();
|
||||||
|
|
||||||
@ -64,6 +61,25 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
|||||||
return super.baseOffset.translate(0, padding.top);
|
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
|
@override
|
||||||
Widget buildWithSingle(BuildContext context) {
|
Widget buildWithSingle(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -71,12 +87,9 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
FlowySvg(
|
Container(
|
||||||
key: iconKey,
|
key: iconKey,
|
||||||
width: iconSize?.width,
|
child: icon,
|
||||||
height: iconSize?.height,
|
|
||||||
padding: iconPadding,
|
|
||||||
name: 'point',
|
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: FlowyRichText(
|
child: FlowyRichText(
|
||||||
@ -86,7 +99,7 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
|||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
placeholderTextSpanDecorator: (textSpan) =>
|
placeholderTextSpanDecorator: (textSpan) =>
|
||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
|
lineHeight: widget.editorState.editorStyle.lineHeight,
|
||||||
textNode: widget.textNode,
|
textNode: widget.textNode,
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
),
|
),
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/commands/text/text_commands.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/render/rich_text/built_in_text_widget.dart';
|
||||||
|
|
||||||
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
|
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:appflowy_editor/src/extensions/theme_extension.dart';
|
||||||
|
|
||||||
class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
||||||
@override
|
@override
|
||||||
@ -39,11 +39,7 @@ class CheckboxNodeWidget extends BuiltInTextWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
||||||
with
|
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
|
||||||
SelectableMixin,
|
|
||||||
DefaultSelectable,
|
|
||||||
BuiltInStyleMixin,
|
|
||||||
BuiltInTextWidgetMixin {
|
|
||||||
@override
|
@override
|
||||||
final iconKey = GlobalKey();
|
final iconKey = GlobalKey();
|
||||||
|
|
||||||
@ -58,6 +54,25 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
|||||||
return super.baseOffset.translate(0, padding.top);
|
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
|
@override
|
||||||
Widget buildWithSingle(BuildContext context) {
|
Widget buildWithSingle(BuildContext context) {
|
||||||
final check = widget.textNode.attributes.check;
|
final check = widget.textNode.attributes.check;
|
||||||
@ -68,12 +83,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
|||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
key: iconKey,
|
key: iconKey,
|
||||||
child: FlowySvg(
|
child: icon,
|
||||||
width: iconSize?.width,
|
|
||||||
height: iconSize?.height,
|
|
||||||
padding: iconPadding,
|
|
||||||
name: check ? 'check' : 'uncheck',
|
|
||||||
),
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await widget.editorState.formatTextToCheckbox(
|
await widget.editorState.formatTextToCheckbox(
|
||||||
widget.editorState,
|
widget.editorState,
|
||||||
@ -86,7 +96,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
|||||||
child: FlowyRichText(
|
child: FlowyRichText(
|
||||||
key: _richTextKey,
|
key: _richTextKey,
|
||||||
placeholderText: 'To-do',
|
placeholderText: 'To-do',
|
||||||
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
|
lineHeight: widget.editorState.editorStyle.lineHeight,
|
||||||
textNode: widget.textNode,
|
textNode: widget.textNode,
|
||||||
textSpanDecorator: (textSpan) =>
|
textSpanDecorator: (textSpan) =>
|
||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
|
@ -202,12 +202,13 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TextSpan get _placeholderTextSpan {
|
TextSpan get _placeholderTextSpan {
|
||||||
final style = widget.editorState.editorStyle.textStyle;
|
final placeholderTextStyle =
|
||||||
|
widget.editorState.editorStyle.placeholderTextStyle;
|
||||||
return TextSpan(
|
return TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: widget.placeholderText,
|
text: widget.placeholderText,
|
||||||
style: style.defaultPlaceholderTextStyle,
|
style: placeholderTextStyle,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -216,10 +217,10 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
|||||||
TextSpan get _textSpan {
|
TextSpan get _textSpan {
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
List<TextSpan> textSpans = [];
|
List<TextSpan> textSpans = [];
|
||||||
final style = widget.editorState.editorStyle.textStyle;
|
final style = widget.editorState.editorStyle;
|
||||||
final textInserts = widget.textNode.delta.whereType<TextInsert>();
|
final textInserts = widget.textNode.delta.whereType<TextInsert>();
|
||||||
for (final textInsert in textInserts) {
|
for (final textInsert in textInserts) {
|
||||||
var textStyle = style.defaultTextStyle;
|
var textStyle = style.textStyle!;
|
||||||
GestureRecognizer? recognizer;
|
GestureRecognizer? recognizer;
|
||||||
final attributes = textInsert.attributes;
|
final attributes = textInsert.attributes;
|
||||||
if (attributes != null) {
|
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/default_selectable.dart';
|
||||||
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.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/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:appflowy_editor/src/service/render_plugin_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/attributes_extension.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/text_style_extension.dart';
|
||||||
|
import 'package:appflowy_editor/src/extensions/theme_extension.dart';
|
||||||
|
|
||||||
class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
||||||
@override
|
@override
|
||||||
@ -43,7 +45,7 @@ class HeadingTextNodeWidget extends BuiltInTextWidget {
|
|||||||
|
|
||||||
// customize
|
// customize
|
||||||
class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
|
class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
|
||||||
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
|
with SelectableMixin, DefaultSelectable {
|
||||||
@override
|
@override
|
||||||
GlobalKey? get iconKey => null;
|
GlobalKey? get iconKey => null;
|
||||||
|
|
||||||
@ -58,6 +60,20 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
|
|||||||
return padding.topLeft;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -68,7 +84,7 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
|
|||||||
placeholderTextSpanDecorator: (textSpan) =>
|
placeholderTextSpanDecorator: (textSpan) =>
|
||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
|
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
|
||||||
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
|
lineHeight: widget.editorState.editorStyle.lineHeight,
|
||||||
textNode: widget.textNode,
|
textNode: widget.textNode,
|
||||||
editorState: widget.editorState,
|
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/default_selectable.dart';
|
||||||
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.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/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:appflowy_editor/src/service/render_plugin_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/attributes_extension.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/text_style_extension.dart';
|
||||||
|
import 'package:appflowy_editor/src/extensions/theme_extension.dart';
|
||||||
|
|
||||||
class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
||||||
@override
|
@override
|
||||||
@ -43,7 +45,7 @@ class NumberListTextNodeWidget extends BuiltInTextWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
|
class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
|
||||||
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
|
with SelectableMixin, DefaultSelectable {
|
||||||
@override
|
@override
|
||||||
final iconKey = GlobalKey();
|
final iconKey = GlobalKey();
|
||||||
|
|
||||||
@ -58,6 +60,25 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
|
|||||||
return super.baseOffset.translate(0, padding.top);
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -67,12 +88,7 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
key: iconKey,
|
key: iconKey,
|
||||||
padding: iconPadding,
|
child: icon,
|
||||||
child: Text(
|
|
||||||
'${widget.textNode.attributes.number.toString()}.',
|
|
||||||
// FIXME: customize
|
|
||||||
style: const TextStyle(fontSize: 16.0, color: Colors.black),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: FlowyRichText(
|
child: FlowyRichText(
|
||||||
@ -80,7 +96,7 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
|
|||||||
placeholderText: 'List',
|
placeholderText: 'List',
|
||||||
textNode: widget.textNode,
|
textNode: widget.textNode,
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
|
lineHeight: widget.editorState.editorStyle.lineHeight,
|
||||||
placeholderTextSpanDecorator: (textSpan) =>
|
placeholderTextSpanDecorator: (textSpan) =>
|
||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
textSpanDecorator: (textSpan) =>
|
textSpanDecorator: (textSpan) =>
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||||
import 'package:appflowy_editor/src/editor_state.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/built_in_text_widget.dart';
|
||||||
import 'package:appflowy_editor/src/render/rich_text/default_selectable.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/rich_text/flowy_rich_text.dart';
|
||||||
import 'package:appflowy_editor/src/render/selection/selectable.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:appflowy_editor/src/service/render_plugin_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/text_style_extension.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> {
|
class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
||||||
@override
|
@override
|
||||||
@ -44,7 +45,7 @@ class QuotedTextNodeWidget extends BuiltInTextWidget {
|
|||||||
// customize
|
// customize
|
||||||
|
|
||||||
class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
|
class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
|
||||||
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
|
with SelectableMixin, DefaultSelectable {
|
||||||
@override
|
@override
|
||||||
final iconKey = GlobalKey();
|
final iconKey = GlobalKey();
|
||||||
|
|
||||||
@ -59,6 +60,25 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
|
|||||||
return super.baseOffset.translate(0, padding.top);
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -67,11 +87,9 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
FlowySvg(
|
Container(
|
||||||
key: iconKey,
|
key: iconKey,
|
||||||
width: iconSize?.width,
|
child: icon,
|
||||||
padding: iconPadding,
|
|
||||||
name: 'quote',
|
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: FlowyRichText(
|
child: FlowyRichText(
|
||||||
@ -82,7 +100,7 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
|
|||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
placeholderTextSpanDecorator: (textSpan) =>
|
placeholderTextSpanDecorator: (textSpan) =>
|
||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
|
lineHeight: widget.editorState.editorStyle.lineHeight,
|
||||||
editorState: widget.editorState,
|
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/default_selectable.dart';
|
||||||
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.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/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:appflowy_editor/src/service/render_plugin_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
|
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
|
||||||
@ -43,11 +44,7 @@ class RichTextNodeWidget extends BuiltInTextWidget {
|
|||||||
// customize
|
// customize
|
||||||
|
|
||||||
class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
|
class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
|
||||||
with
|
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
|
||||||
SelectableMixin,
|
|
||||||
DefaultSelectable,
|
|
||||||
BuiltInStyleMixin,
|
|
||||||
BuiltInTextWidgetMixin {
|
|
||||||
@override
|
@override
|
||||||
GlobalKey? get iconKey => null;
|
GlobalKey? get iconKey => null;
|
||||||
|
|
||||||
@ -59,20 +56,26 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Offset get baseOffset {
|
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
|
@override
|
||||||
Widget buildWithSingle(BuildContext context) {
|
Widget buildWithSingle(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: textPadding,
|
||||||
child: FlowyRichText(
|
child: FlowyRichText(
|
||||||
key: _richTextKey,
|
key: _richTextKey,
|
||||||
textNode: widget.textNode,
|
textNode: widget.textNode,
|
||||||
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
|
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
|
||||||
placeholderTextSpanDecorator: (textSpan) =>
|
placeholderTextSpanDecorator: (textSpan) =>
|
||||||
textSpan.updateTextStyle(textStyle),
|
textSpan.updateTextStyle(textStyle),
|
||||||
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
|
lineHeight: widget.editorState.editorStyle.lineHeight,
|
||||||
editorState: widget.editorState,
|
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:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SelectionMenuItemWidget extends StatelessWidget {
|
class SelectionMenuItemWidget extends StatefulWidget {
|
||||||
const SelectionMenuItemWidget({
|
const SelectionMenuItemWidget({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
@ -11,7 +11,6 @@ class SelectionMenuItemWidget extends StatelessWidget {
|
|||||||
required this.item,
|
required this.item,
|
||||||
required this.isSelected,
|
required this.isSelected,
|
||||||
this.width = 140.0,
|
this.width = 140.0,
|
||||||
this.selectedColor = const Color(0xFFE0F8FF),
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
@ -19,33 +18,52 @@ class SelectionMenuItemWidget extends StatelessWidget {
|
|||||||
final SelectionMenuItem item;
|
final SelectionMenuItem item;
|
||||||
final double width;
|
final double width;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final Color selectedColor;
|
|
||||||
|
@override
|
||||||
|
State<SelectionMenuItemWidget> createState() =>
|
||||||
|
_SelectionMenuItemWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectionMenuItemWidgetState extends State<SelectionMenuItemWidget> {
|
||||||
|
var _onHover = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final editorStyle = widget.editorState.editorStyle;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0),
|
padding: const EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: width,
|
width: widget.width,
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
icon: item.icon,
|
icon: widget.item
|
||||||
|
.icon(widget.editorState, widget.isSelected || _onHover),
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
overlayColor: MaterialStateProperty.all(selectedColor),
|
overlayColor: MaterialStateProperty.all(
|
||||||
backgroundColor: isSelected
|
editorStyle.selectionMenuItemSelectedColor),
|
||||||
? MaterialStateProperty.all(selectedColor)
|
backgroundColor: widget.isSelected
|
||||||
|
? MaterialStateProperty.all(
|
||||||
|
editorStyle.selectionMenuItemSelectedColor)
|
||||||
: MaterialStateProperty.all(Colors.transparent),
|
: MaterialStateProperty.all(Colors.transparent),
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
item.name(),
|
widget.item.name(),
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: (widget.isSelected || _onHover)
|
||||||
fontSize: 14.0,
|
? editorStyle.selectionMenuItemSelectedTextColor
|
||||||
|
: editorStyle.selectionMenuItemTextColor,
|
||||||
|
fontSize: 12.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
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.
|
// Just subtract the padding here as a result.
|
||||||
const menuHeight = 200.0;
|
const menuHeight = 200.0;
|
||||||
const menuOffset = Offset(10, 10);
|
const menuOffset = Offset(10, 10);
|
||||||
final baseOffset =
|
final editorOffset =
|
||||||
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
||||||
var offset = selectionRects.first.bottomRight + menuOffset;
|
final editorHeight = editorState.renderBox!.size.height;
|
||||||
if (offset.dy >=
|
|
||||||
baseOffset.dy + editorState.renderBox!.size.height - menuHeight) {
|
// show below defualt
|
||||||
offset = selectionRects.first.topRight - menuOffset;
|
var showBelow = true;
|
||||||
offset = offset.translate(0, -menuHeight);
|
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;
|
_topLeft = offset;
|
||||||
|
|
||||||
_selectionMenuEntry = OverlayEntry(builder: (context) {
|
_selectionMenuEntry = OverlayEntry(builder: (context) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: offset.dy,
|
top: showBelow ? offset.dy : null,
|
||||||
|
bottom: showBelow ? null : editorHeight - offset.dy,
|
||||||
left: offset.dx,
|
left: offset.dx,
|
||||||
child: SelectionMenuWidget(
|
child: SelectionMenuWidget(
|
||||||
items: [
|
items: [
|
||||||
@ -131,7 +139,8 @@ List<SelectionMenuItem> get defaultSelectionMenuItems =>
|
|||||||
final List<SelectionMenuItem> _defaultSelectionMenuItems = [
|
final List<SelectionMenuItem> _defaultSelectionMenuItems = [
|
||||||
SelectionMenuItem(
|
SelectionMenuItem(
|
||||||
name: () => AppFlowyEditorLocalizations.current.text,
|
name: () => AppFlowyEditorLocalizations.current.text,
|
||||||
icon: _selectionMenuIcon('text'),
|
icon: (editorState, onSelected) =>
|
||||||
|
_selectionMenuIcon('text', editorState, onSelected),
|
||||||
keywords: ['text'],
|
keywords: ['text'],
|
||||||
handler: (editorState, _, __) {
|
handler: (editorState, _, __) {
|
||||||
insertTextNodeAfterSelection(editorState, {});
|
insertTextNodeAfterSelection(editorState, {});
|
||||||
@ -139,7 +148,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
|
|||||||
),
|
),
|
||||||
SelectionMenuItem(
|
SelectionMenuItem(
|
||||||
name: () => AppFlowyEditorLocalizations.current.heading1,
|
name: () => AppFlowyEditorLocalizations.current.heading1,
|
||||||
icon: _selectionMenuIcon('h1'),
|
icon: (editorState, onSelected) =>
|
||||||
|
_selectionMenuIcon('h1', editorState, onSelected),
|
||||||
keywords: ['heading 1, h1'],
|
keywords: ['heading 1, h1'],
|
||||||
handler: (editorState, _, __) {
|
handler: (editorState, _, __) {
|
||||||
insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h1);
|
insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h1);
|
||||||
@ -147,7 +157,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
|
|||||||
),
|
),
|
||||||
SelectionMenuItem(
|
SelectionMenuItem(
|
||||||
name: () => AppFlowyEditorLocalizations.current.heading2,
|
name: () => AppFlowyEditorLocalizations.current.heading2,
|
||||||
icon: _selectionMenuIcon('h2'),
|
icon: (editorState, onSelected) =>
|
||||||
|
_selectionMenuIcon('h2', editorState, onSelected),
|
||||||
keywords: ['heading 2, h2'],
|
keywords: ['heading 2, h2'],
|
||||||
handler: (editorState, _, __) {
|
handler: (editorState, _, __) {
|
||||||
insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h2);
|
insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h2);
|
||||||
@ -155,7 +166,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
|
|||||||
),
|
),
|
||||||
SelectionMenuItem(
|
SelectionMenuItem(
|
||||||
name: () => AppFlowyEditorLocalizations.current.heading3,
|
name: () => AppFlowyEditorLocalizations.current.heading3,
|
||||||
icon: _selectionMenuIcon('h3'),
|
icon: (editorState, onSelected) =>
|
||||||
|
_selectionMenuIcon('h3', editorState, onSelected),
|
||||||
keywords: ['heading 3, h3'],
|
keywords: ['heading 3, h3'],
|
||||||
handler: (editorState, _, __) {
|
handler: (editorState, _, __) {
|
||||||
insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h3);
|
insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h3);
|
||||||
@ -163,13 +175,15 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
|
|||||||
),
|
),
|
||||||
SelectionMenuItem(
|
SelectionMenuItem(
|
||||||
name: () => AppFlowyEditorLocalizations.current.image,
|
name: () => AppFlowyEditorLocalizations.current.image,
|
||||||
icon: _selectionMenuIcon('image'),
|
icon: (editorState, onSelected) =>
|
||||||
|
_selectionMenuIcon('image', editorState, onSelected),
|
||||||
keywords: ['image'],
|
keywords: ['image'],
|
||||||
handler: showImageUploadMenu,
|
handler: showImageUploadMenu,
|
||||||
),
|
),
|
||||||
SelectionMenuItem(
|
SelectionMenuItem(
|
||||||
name: () => AppFlowyEditorLocalizations.current.bulletedList,
|
name: () => AppFlowyEditorLocalizations.current.bulletedList,
|
||||||
icon: _selectionMenuIcon('bulleted_list'),
|
icon: (editorState, onSelected) =>
|
||||||
|
_selectionMenuIcon('bulleted_list', editorState, onSelected),
|
||||||
keywords: ['bulleted list', 'list', 'unordered list'],
|
keywords: ['bulleted list', 'list', 'unordered list'],
|
||||||
handler: (editorState, _, __) {
|
handler: (editorState, _, __) {
|
||||||
insertBulletedListAfterSelection(editorState);
|
insertBulletedListAfterSelection(editorState);
|
||||||
@ -177,7 +191,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
|
|||||||
),
|
),
|
||||||
SelectionMenuItem(
|
SelectionMenuItem(
|
||||||
name: () => AppFlowyEditorLocalizations.current.numberedList,
|
name: () => AppFlowyEditorLocalizations.current.numberedList,
|
||||||
icon: _selectionMenuIcon('number'),
|
icon: (editorState, onSelected) =>
|
||||||
|
_selectionMenuIcon('number', editorState, onSelected),
|
||||||
keywords: ['numbered list', 'list', 'ordered list'],
|
keywords: ['numbered list', 'list', 'ordered list'],
|
||||||
handler: (editorState, _, __) {
|
handler: (editorState, _, __) {
|
||||||
insertNumberedListAfterSelection(editorState);
|
insertNumberedListAfterSelection(editorState);
|
||||||
@ -185,7 +200,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
|
|||||||
),
|
),
|
||||||
SelectionMenuItem(
|
SelectionMenuItem(
|
||||||
name: () => AppFlowyEditorLocalizations.current.checkbox,
|
name: () => AppFlowyEditorLocalizations.current.checkbox,
|
||||||
icon: _selectionMenuIcon('checkbox'),
|
icon: (editorState, onSelected) =>
|
||||||
|
_selectionMenuIcon('checkbox', editorState, onSelected),
|
||||||
keywords: ['todo list', 'list', 'checkbox list'],
|
keywords: ['todo list', 'list', 'checkbox list'],
|
||||||
handler: (editorState, _, __) {
|
handler: (editorState, _, __) {
|
||||||
insertCheckboxAfterSelection(editorState);
|
insertCheckboxAfterSelection(editorState);
|
||||||
@ -193,7 +209,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
|
|||||||
),
|
),
|
||||||
SelectionMenuItem(
|
SelectionMenuItem(
|
||||||
name: () => AppFlowyEditorLocalizations.current.quote,
|
name: () => AppFlowyEditorLocalizations.current.quote,
|
||||||
icon: _selectionMenuIcon('quote'),
|
icon: (editorState, onSelected) =>
|
||||||
|
_selectionMenuIcon('quote', editorState, onSelected),
|
||||||
keywords: ['quote', 'refer'],
|
keywords: ['quote', 'refer'],
|
||||||
handler: (editorState, _, __) {
|
handler: (editorState, _, __) {
|
||||||
insertQuoteAfterSelection(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(
|
return FlowySvg(
|
||||||
name: 'selection_menu/$name',
|
name: 'selection_menu/$name',
|
||||||
color: Colors.black,
|
color: onSelected
|
||||||
|
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||||
|
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||||
width: 18.0,
|
width: 18.0,
|
||||||
height: 18.0,
|
height: 18.0,
|
||||||
);
|
);
|
||||||
|
@ -29,7 +29,7 @@ class SelectionMenuItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final String Function() name;
|
final String Function() name;
|
||||||
final Widget icon;
|
final Widget Function(EditorState editorState, bool onSelected) icon;
|
||||||
|
|
||||||
/// Customizes keywords for item.
|
/// Customizes keywords for item.
|
||||||
///
|
///
|
||||||
@ -142,7 +142,7 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
|
|||||||
onKey: _onKey,
|
onKey: _onKey,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: widget.editorState.editorStyle.selectionMenuBackgroundColor,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
blurRadius: 5,
|
blurRadius: 5,
|
||||||
|
@ -1,202 +1,78 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
Iterable<ThemeExtension<dynamic>> get lightEditorStyleExtension => [
|
||||||
import 'package:appflowy_editor/src/editor_state.dart';
|
EditorStyle.light,
|
||||||
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
|
];
|
||||||
|
|
||||||
typedef PluginStyler = Object Function(EditorState editorState, Node node);
|
Iterable<ThemeExtension<dynamic>> get darkEditorStyleExtension => [
|
||||||
typedef PluginStyle = Map<String, PluginStyler>;
|
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({
|
EditorStyle({
|
||||||
required this.padding,
|
required this.padding,
|
||||||
required this.textStyle,
|
|
||||||
required this.cursorColor,
|
required this.cursorColor,
|
||||||
required this.selectionColor,
|
required this.selectionColor,
|
||||||
Map<String, PluginStyle> pluginStyles = const {},
|
required this.selectionMenuBackgroundColor,
|
||||||
}) {
|
required this.selectionMenuItemTextColor,
|
||||||
_pluginStyles.addAll(pluginStyles);
|
required this.selectionMenuItemIconColor,
|
||||||
}
|
required this.selectionMenuItemSelectedTextColor,
|
||||||
|
required this.selectionMenuItemSelectedIconColor,
|
||||||
EditorStyle.defaultStyle()
|
required this.selectionMenuItemSelectedColor,
|
||||||
: padding = const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0),
|
required this.textPadding,
|
||||||
textStyle = BuiltInTextStyle.builtIn(),
|
required this.textStyle,
|
||||||
cursorColor = const Color(0xFF00BCF0),
|
required this.placeholderTextStyle,
|
||||||
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.bold,
|
required this.bold,
|
||||||
required this.italic,
|
required this.italic,
|
||||||
required this.underline,
|
required this.underline,
|
||||||
required this.strikethrough,
|
required this.strikethrough,
|
||||||
required this.href,
|
required this.href,
|
||||||
required this.code,
|
required this.code,
|
||||||
this.highlightColorHex = '0x6000BCF0',
|
required this.highlightColorHex,
|
||||||
this.lineHeight = 1.5,
|
required this.lineHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextStyle defaultTextStyle;
|
@override
|
||||||
final TextStyle defaultPlaceholderTextStyle;
|
EditorStyle copyWith({
|
||||||
final TextStyle bold;
|
EdgeInsets? padding,
|
||||||
final TextStyle italic;
|
Color? cursorColor,
|
||||||
final TextStyle underline;
|
Color? selectionColor,
|
||||||
final TextStyle strikethrough;
|
Color? selectionMenuBackgroundColor,
|
||||||
final TextStyle href;
|
Color? selectionMenuItemTextColor,
|
||||||
final TextStyle code;
|
Color? selectionMenuItemIconColor,
|
||||||
final String highlightColorHex;
|
Color? selectionMenuItemSelectedTextColor,
|
||||||
final double lineHeight;
|
Color? selectionMenuItemSelectedIconColor,
|
||||||
|
Color? selectionMenuItemSelectedColor,
|
||||||
BuiltInTextStyle.builtIn()
|
TextStyle? textStyle,
|
||||||
: defaultTextStyle = const TextStyle(fontSize: 16.0, color: Colors.black),
|
TextStyle? placeholderTextStyle,
|
||||||
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,
|
|
||||||
TextStyle? bold,
|
TextStyle? bold,
|
||||||
TextStyle? italic,
|
TextStyle? italic,
|
||||||
TextStyle? underline,
|
TextStyle? underline,
|
||||||
@ -206,10 +82,25 @@ class BuiltInTextStyle {
|
|||||||
String? highlightColorHex,
|
String? highlightColorHex,
|
||||||
double? lineHeight,
|
double? lineHeight,
|
||||||
}) {
|
}) {
|
||||||
return BuiltInTextStyle(
|
return EditorStyle(
|
||||||
defaultTextStyle: defaultTextStyle ?? this.defaultTextStyle,
|
padding: padding ?? this.padding,
|
||||||
defaultPlaceholderTextStyle:
|
cursorColor: cursorColor ?? this.cursorColor,
|
||||||
defaultPlaceholderTextStyle ?? this.defaultPlaceholderTextStyle,
|
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,
|
bold: bold ?? this.bold,
|
||||||
italic: italic ?? this.italic,
|
italic: italic ?? this.italic,
|
||||||
underline: underline ?? this.underline,
|
underline: underline ?? this.underline,
|
||||||
@ -222,33 +113,87 @@ class BuiltInTextStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
ThemeExtension<EditorStyle> lerp(
|
||||||
if (identical(this, other)) return true;
|
ThemeExtension<EditorStyle>? other, double t) {
|
||||||
|
if (other == null || other is! EditorStyle) {
|
||||||
return other is BuiltInTextStyle &&
|
return this;
|
||||||
other.defaultTextStyle == defaultTextStyle &&
|
}
|
||||||
other.defaultPlaceholderTextStyle == defaultPlaceholderTextStyle &&
|
return EditorStyle(
|
||||||
other.bold == bold &&
|
padding: EdgeInsets.lerp(padding, other.padding, t),
|
||||||
other.italic == italic &&
|
cursorColor: Color.lerp(cursorColor, other.cursorColor, t),
|
||||||
other.underline == underline &&
|
textPadding: EdgeInsets.lerp(textPadding, other.textPadding, t),
|
||||||
other.strikethrough == strikethrough &&
|
selectionColor: Color.lerp(selectionColor, other.selectionColor, t),
|
||||||
other.href == href &&
|
selectionMenuBackgroundColor: Color.lerp(
|
||||||
other.code == code &&
|
selectionMenuBackgroundColor, other.selectionMenuBackgroundColor, t),
|
||||||
other.highlightColorHex == highlightColorHex &&
|
selectionMenuItemTextColor: Color.lerp(
|
||||||
other.lineHeight == lineHeight;
|
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
|
static final light = EditorStyle(
|
||||||
int get hashCode {
|
padding: const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0),
|
||||||
return defaultTextStyle.hashCode ^
|
cursorColor: const Color(0xFF00BCF0),
|
||||||
defaultPlaceholderTextStyle.hashCode ^
|
selectionColor: const Color.fromARGB(53, 111, 201, 231),
|
||||||
bold.hashCode ^
|
selectionMenuBackgroundColor: const Color(0xFFFFFFFF),
|
||||||
italic.hashCode ^
|
selectionMenuItemTextColor: const Color(0xFF333333),
|
||||||
underline.hashCode ^
|
selectionMenuItemIconColor: const Color(0xFF333333),
|
||||||
strikethrough.hashCode ^
|
selectionMenuItemSelectedTextColor: const Color(0xFF333333),
|
||||||
href.hashCode ^
|
selectionMenuItemSelectedIconColor: const Color(0xFF333333),
|
||||||
code.hashCode ^
|
selectionMenuItemSelectedColor: const Color(0xFFE0F8FF),
|
||||||
highlightColorHex.hashCode ^
|
textPadding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
lineHeight.hashCode;
|
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(
|
handler: (editorState, context) => formatHighlight(
|
||||||
editorState,
|
editorState,
|
||||||
editorState.editorStyle.textStyle.highlightColorHex,
|
editorState.editorStyle.highlightColorHex!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@ -348,6 +348,7 @@ void showLinkMenu(
|
|||||||
child: Material(
|
child: Material(
|
||||||
child: LinkMenu(
|
child: LinkMenu(
|
||||||
linkText: linkText,
|
linkText: linkText,
|
||||||
|
editorState: editorState,
|
||||||
onOpenLink: () async {
|
onOpenLink: () async {
|
||||||
await safeLaunchUrl(linkText);
|
await safeLaunchUrl(linkText);
|
||||||
},
|
},
|
||||||
|
@ -25,6 +25,7 @@ class ToolbarItemWidget extends StatelessWidget {
|
|||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
|
hoverColor: Colors.transparent,
|
||||||
highlightColor: Colors.transparent,
|
highlightColor: Colors.transparent,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
icon: item.iconBuilder(isHighlight),
|
icon: item.iconBuilder(isHighlight),
|
||||||
|
@ -30,26 +30,43 @@ class ContextMenu extends StatelessWidget {
|
|||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
for (var i = 0; i < items.length; i++) {
|
for (var i = 0; i < items.length; i++) {
|
||||||
for (var j = 0; j < items[i].length; j++) {
|
for (var j = 0; j < items[i].length; j++) {
|
||||||
|
var onHover = false;
|
||||||
children.add(
|
children.add(
|
||||||
Material(
|
StatefulBuilder(
|
||||||
child: InkWell(
|
builder: (BuildContext context, setState) {
|
||||||
hoverColor: const Color(0xFFE0F8FF),
|
return Material(
|
||||||
customBorder: RoundedRectangleBorder(
|
color: editorState.editorStyle.selectionMenuBackgroundColor,
|
||||||
borderRadius: BorderRadius.circular(6),
|
child: InkWell(
|
||||||
),
|
hoverColor:
|
||||||
onTap: () {
|
editorState.editorStyle.selectionMenuItemSelectedColor,
|
||||||
items[i][j].onPressed(editorState);
|
customBorder: RoundedRectangleBorder(
|
||||||
onPressed();
|
borderRadius: BorderRadius.circular(6),
|
||||||
},
|
),
|
||||||
child: Padding(
|
onTap: () {
|
||||||
padding: const EdgeInsets.all(8.0),
|
items[i][j].onPressed(editorState);
|
||||||
child: Text(
|
onPressed();
|
||||||
items[i][j].name,
|
},
|
||||||
textAlign: TextAlign.start,
|
onHover: (value) => setState(() {
|
||||||
style: const TextStyle(fontSize: 14),
|
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,
|
minWidth: 140,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: editorState.editorStyle.selectionMenuBackgroundColor,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
blurRadius: 5,
|
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/flutter/overlay.dart';
|
||||||
import 'package:appflowy_editor/src/render/image/image_node_builder.dart';
|
import 'package:appflowy_editor/src/render/image/image_node_builder.dart';
|
||||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
|
|
||||||
import 'package:appflowy_editor/src/render/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/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: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/editor/editor_entry.dart';
|
||||||
import 'package:appflowy_editor/src/render/rich_text/bulleted_list_text.dart';
|
import 'package:appflowy_editor/src/render/rich_text/bulleted_list_text.dart';
|
||||||
import 'package:appflowy_editor/src/render/rich_text/checkbox_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/number_list_text.dart';
|
||||||
import 'package:appflowy_editor/src/render/rich_text/quoted_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/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 = {
|
NodeWidgetBuilders defaultBuilders = {
|
||||||
'editor': EditorEntryWidgetBuilder(),
|
'editor': EditorEntryWidgetBuilder(),
|
||||||
@ -33,15 +24,21 @@ NodeWidgetBuilders defaultBuilders = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class AppFlowyEditor extends StatefulWidget {
|
class AppFlowyEditor extends StatefulWidget {
|
||||||
const AppFlowyEditor({
|
AppFlowyEditor({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
this.customBuilders = const {},
|
this.customBuilders = const {},
|
||||||
this.shortcutEvents = const [],
|
this.shortcutEvents = const [],
|
||||||
this.selectionMenuItems = const [],
|
this.selectionMenuItems = const [],
|
||||||
this.editable = true,
|
this.editable = true,
|
||||||
required this.editorStyle,
|
ThemeData? themeData,
|
||||||
}) : super(key: key);
|
}) : super(key: key) {
|
||||||
|
this.themeData = themeData ??
|
||||||
|
ThemeData.light().copyWith(extensions: [
|
||||||
|
...lightEditorStyleExtension,
|
||||||
|
...lightPlguinStyleExtension,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
|
|
||||||
@ -53,7 +50,7 @@ class AppFlowyEditor extends StatefulWidget {
|
|||||||
|
|
||||||
final List<SelectionMenuItem> selectionMenuItems;
|
final List<SelectionMenuItem> selectionMenuItems;
|
||||||
|
|
||||||
final EditorStyle editorStyle;
|
late final ThemeData themeData;
|
||||||
|
|
||||||
final bool editable;
|
final bool editable;
|
||||||
|
|
||||||
@ -65,13 +62,15 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
|||||||
Widget? services;
|
Widget? services;
|
||||||
|
|
||||||
EditorState get editorState => widget.editorState;
|
EditorState get editorState => widget.editorState;
|
||||||
|
EditorStyle get editorStyle =>
|
||||||
|
editorState.themeData.extension<EditorStyle>() ?? EditorStyle.light;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
editorState.selectionMenuItems = widget.selectionMenuItems;
|
editorState.selectionMenuItems = widget.selectionMenuItems;
|
||||||
editorState.editorStyle = widget.editorStyle;
|
editorState.themeData = widget.themeData;
|
||||||
editorState.service.renderPluginService = _createRenderPlugin();
|
editorState.service.renderPluginService = _createRenderPlugin();
|
||||||
editorState.editable = widget.editable;
|
editorState.editable = widget.editable;
|
||||||
}
|
}
|
||||||
@ -85,7 +84,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
|||||||
editorState.service.renderPluginService = _createRenderPlugin();
|
editorState.service.renderPluginService = _createRenderPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
editorState.editorStyle = widget.editorStyle;
|
editorState.themeData = widget.themeData;
|
||||||
editorState.editable = widget.editable;
|
editorState.editable = widget.editable;
|
||||||
services = null;
|
services = null;
|
||||||
}
|
}
|
||||||
@ -102,38 +101,41 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AppFlowyScroll _buildServices(BuildContext context) {
|
Widget _buildServices(BuildContext context) {
|
||||||
return AppFlowyScroll(
|
return Theme(
|
||||||
key: editorState.service.scrollServiceKey,
|
data: widget.themeData,
|
||||||
child: Padding(
|
child: AppFlowyScroll(
|
||||||
padding: widget.editorStyle.padding,
|
key: editorState.service.scrollServiceKey,
|
||||||
child: AppFlowySelection(
|
child: Padding(
|
||||||
key: editorState.service.selectionServiceKey,
|
padding: editorStyle.padding!,
|
||||||
cursorColor: widget.editorStyle.cursorColor,
|
child: AppFlowySelection(
|
||||||
selectionColor: widget.editorStyle.selectionColor,
|
key: editorState.service.selectionServiceKey,
|
||||||
editorState: editorState,
|
cursorColor: editorStyle.cursorColor!,
|
||||||
editable: widget.editable,
|
selectionColor: editorStyle.selectionColor!,
|
||||||
child: AppFlowyInput(
|
|
||||||
key: editorState.service.inputServiceKey,
|
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
editable: widget.editable,
|
editable: widget.editable,
|
||||||
child: AppFlowyKeyboard(
|
child: AppFlowyInput(
|
||||||
key: editorState.service.keyboardServiceKey,
|
key: editorState.service.inputServiceKey,
|
||||||
editable: widget.editable,
|
|
||||||
shortcutEvents: [
|
|
||||||
...widget.shortcutEvents,
|
|
||||||
...builtInShortcutEvents,
|
|
||||||
],
|
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child: FlowyToolbar(
|
editable: widget.editable,
|
||||||
key: editorState.service.toolbarServiceKey,
|
child: AppFlowyKeyboard(
|
||||||
|
key: editorState.service.keyboardServiceKey,
|
||||||
|
editable: widget.editable,
|
||||||
|
shortcutEvents: [
|
||||||
|
...widget.shortcutEvents,
|
||||||
|
...builtInShortcutEvents,
|
||||||
|
],
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child:
|
child: FlowyToolbar(
|
||||||
editorState.service.renderPluginService.buildPluginWidget(
|
key: editorState.service.toolbarServiceKey,
|
||||||
NodeWidgetContext(
|
editorState: editorState,
|
||||||
context: context,
|
child:
|
||||||
node: editorState.document.root,
|
editorState.service.renderPluginService.buildPluginWidget(
|
||||||
editorState: editorState,
|
NodeWidgetContext(
|
||||||
|
context: context,
|
||||||
|
node: editorState.document.root,
|
||||||
|
editorState: editorState,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -41,7 +41,10 @@ void _handleCopy(EditorState editorState) async {
|
|||||||
Log.keyboard.debug('copy html: $htmlString');
|
Log.keyboard.debug('copy html: $htmlString');
|
||||||
RichClipboard.setData(RichClipboardData(
|
RichClipboard.setData(RichClipboardData(
|
||||||
html: htmlString,
|
html: htmlString,
|
||||||
text: textNode.toPlainText(),
|
text: textNode.toPlainText().substring(
|
||||||
|
selection.startIndex,
|
||||||
|
selection.endIndex,
|
||||||
|
),
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
Log.keyboard.debug('unimplemented: copy non-text');
|
Log.keyboard.debug('unimplemented: copy non-text');
|
||||||
@ -63,9 +66,19 @@ void _handleCopy(EditorState editorState) async {
|
|||||||
startOffset: selection.start.offset,
|
startOffset: selection.start.offset,
|
||||||
endOffset: selection.end.offset,
|
endOffset: selection.end.offset,
|
||||||
).toHTMLString();
|
).toHTMLString();
|
||||||
final text = nodes
|
var text = '';
|
||||||
.map((node) => node is TextNode ? node.toPlainText() : '\n')
|
for (final node in nodes) {
|
||||||
.join('\n');
|
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));
|
RichClipboard.setData(RichClipboardData(html: html, text: text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ ShortcutEventHandler formatHighlightEventHandler = (editorState, event) {
|
|||||||
}
|
}
|
||||||
formatHighlight(
|
formatHighlight(
|
||||||
editorState,
|
editorState,
|
||||||
editorState.editorStyle.textStyle.highlightColorHex,
|
editorState.editorStyle.highlightColorHex!,
|
||||||
);
|
);
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
};
|
};
|
||||||
|
@ -39,7 +39,6 @@ class EditorWidgetTester {
|
|||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: AppFlowyEditor(
|
body: AppFlowyEditor(
|
||||||
editorState: _editorState,
|
editorState: _editorState,
|
||||||
editorStyle: EditorStyle.defaultStyle(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -49,10 +49,11 @@ void main() async {
|
|||||||
final editorRect = tester.getRect(editorFinder);
|
final editorRect = tester.getRect(editorFinder);
|
||||||
|
|
||||||
final leftImageRect = tester.getRect(imageFinder.at(0));
|
final leftImageRect = tester.getRect(imageFinder.at(0));
|
||||||
expect(leftImageRect.left, editor.editorState.editorStyle.padding.left);
|
expect(
|
||||||
|
leftImageRect.left, editor.editorState.editorStyle.padding!.left);
|
||||||
final rightImageRect = tester.getRect(imageFinder.at(2));
|
final rightImageRect = tester.getRect(imageFinder.at(2));
|
||||||
expect(rightImageRect.right,
|
expect(rightImageRect.right,
|
||||||
editorRect.right - editor.editorState.editorStyle.padding.right);
|
editorRect.right - editor.editorState.editorStyle.padding!.right);
|
||||||
final centerImageRect = tester.getRect(imageFinder.at(1));
|
final centerImageRect = tester.getRect(imageFinder.at(1));
|
||||||
expect(centerImageRect.left,
|
expect(centerImageRect.left,
|
||||||
(leftImageRect.left + rightImageRect.left) / 2.0);
|
(leftImageRect.left + rightImageRect.left) / 2.0);
|
||||||
|
@ -137,7 +137,7 @@ Future<EditorWidgetTester> _prepare(WidgetTester tester) async {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (final item in defaultSelectionMenuItems) {
|
for (final item in defaultSelectionMenuItems) {
|
||||||
expect(find.byWidget(item.icon), findsOneWidget);
|
expect(find.text(item.name()), findsOneWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Future.value(editor);
|
return Future.value(editor);
|
||||||
|
@ -30,7 +30,7 @@ void main() async {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (final item in defaultSelectionMenuItems) {
|
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));
|
await editor.updateSelection(Selection.single(path: [1], startOffset: 0));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user