fix: analysis issues (#283)

This commit is contained in:
Lucas.Xu 2023-07-04 11:22:35 +08:00 • committed by GitHub
commit 2b2bce4f3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
82 changed files with 306 additions and 4766 deletions

View file

@ -4,12 +4,14 @@ on:
push: push:
branches: branches:
- "main" - "main"
- "release/*"
pull_request: pull_request:
branches: branches:
- "main" - "main"
- "release/*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env: env:
FLUTTER_VERSION: "3.10.1" FLUTTER_VERSION: "3.10.1"
@ -18,7 +20,7 @@ jobs:
tests: tests:
strategy: strategy:
matrix: matrix:
os: [macos-latest, ubuntu-latest, windows-latest] os: [macos-13, macos-12, macos-11, ubuntu-22.04, ubuntu-20.04, windows-2022, windows-2019]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -38,8 +40,13 @@ jobs:
dart format --set-exit-if-changed . dart format --set-exit-if-changed .
flutter test --coverage flutter test --coverage
- uses: codecov/codecov-action@v3 - name: Upload coverage to Codecov
uses: Wandalen/wretry.action@v1.0.36
with: with:
env_vars: ${{ matrix.os }} action: codecov/codecov-action@v3
with: |
fail_ci_if_error: true fail_ci_if_error: true
verbose: true verbose: true
os: ${{ matrix.os }}
attempt_limit: 10
attempt_delay: 10000

View file

@ -14,6 +14,3 @@ analyzer:
deprecated_member_use_from_same_package: ignore deprecated_member_use_from_same_package: ignore
exclude: exclude:
- lib/src/l10n/** - lib/src/l10n/**
# Remove the below directory until migration is complete.
- test/**
- lib/src/service/**

View file

@ -1,68 +1,27 @@
/// AppFlowyEditor library /// AppFlowyEditor library
library appflowy_editor; library appflowy_editor;
export 'src/core/document/attributes.dart'; // core part, including document, node, selection, etc.
export 'src/core/document/deprecated/document.dart'; export 'src/core/core.dart';
export 'src/core/document/deprecated/node.dart'; // editor part, including editor component, block component, etc.
export 'src/core/document/document.dart'; export 'src/editor/editor.dart';
export 'src/core/document/node.dart'; export 'src/editor/selection_menu/selection_menu.dart';
export 'src/core/document/node_iterator.dart'; // editor state
export 'src/core/document/path.dart';
export 'src/core/document/text_delta.dart';
export 'src/core/legacy/built_in_attribute_keys.dart';
export 'src/core/location/position.dart';
export 'src/core/location/selection.dart';
export 'src/core/transform/operation.dart';
export 'src/core/transform/transaction.dart';
export 'src/editor/block_component/block_component.dart';
export 'src/editor/command/transform.dart';
export 'src/editor/editor_component/editor_component.dart';
export 'src/editor/toolbar/toolbar.dart';
export 'src/editor/util/util.dart';
export 'src/editor_state.dart'; export 'src/editor_state.dart';
// extension
export 'src/extensions/extensions.dart'; export 'src/extensions/extensions.dart';
export 'src/extensions/node_extensions.dart';
export 'src/infra/flowy_svg.dart'; export 'src/infra/flowy_svg.dart';
export 'src/infra/html_converter.dart'; export 'src/infra/html_converter.dart';
export 'src/infra/log.dart'; export 'src/infra/log.dart';
export 'src/infra/mobile/mobile.dart'; export 'src/infra/mobile/mobile.dart';
export 'src/l10n/l10n.dart'; export 'src/l10n/l10n.dart';
export 'src/plugins/html/html_document.dart'; // plugins part, including decoder and encoder.
export 'src/plugins/html/html_document_decoder.dart'; export 'src/plugins/plugins.dart';
export 'src/plugins/html/html_document_encoder.dart'; // legacy
export 'src/plugins/markdown/decoder/delta_markdown_decoder.dart';
export 'src/plugins/markdown/document_markdown.dart';
export 'src/plugins/markdown/encoder/delta_markdown_encoder.dart';
export 'src/plugins/markdown/encoder/document_markdown_encoder.dart';
export 'src/plugins/markdown/encoder/parser/image_node_parser.dart';
export 'src/plugins/markdown/encoder/parser/node_parser.dart';
export 'src/plugins/markdown/encoder/parser/text_node_parser.dart';
export 'src/plugins/quill_delta/quill_delta_encoder.dart';
export 'src/render/action_menu/action_menu.dart';
export 'src/render/action_menu/action_menu_item.dart';
export 'src/render/rich_text/default_selectable.dart'; 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/rich_text/flowy_rich_text_keys.dart'; export 'src/render/rich_text/flowy_rich_text_keys.dart';
export 'src/render/selection/selectable.dart'; export 'src/render/selection/selectable.dart';
export 'src/render/selection_menu/selection_menu_icon.dart';
export 'src/render/selection_menu/selection_menu_item_widget.dart';
export 'src/render/selection_menu/selection_menu_service.dart';
export 'src/render/selection_menu/selection_menu_widget.dart';
export 'src/render/style/editor_style.dart';
export 'src/render/style/plugin_styles.dart';
export 'src/render/toolbar/toolbar_item.dart'; export 'src/render/toolbar/toolbar_item.dart';
export 'src/service/default_text_operations/format_rich_text_style.dart';
export 'src/service/editor_service.dart';
export 'src/service/input_service.dart';
export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
export 'src/service/keyboard_service.dart';
export 'src/service/render_plugin_service.dart';
export 'src/service/scroll_service.dart';
export 'src/service/selection_service.dart';
export 'src/service/service.dart';
export 'src/service/shortcut_event/key_mapping.dart'; export 'src/service/shortcut_event/key_mapping.dart';
export 'src/service/shortcut_event/keybinding.dart'; export 'src/service/shortcut_event/keybinding.dart';
export 'src/service/shortcut_event/shortcut_event.dart';
export 'src/service/shortcut_event/shortcut_event_handler.dart';
export 'src/service/standard_block_components.dart';
export 'src/service/toolbar_service.dart';

13
lib/src/core/core.dart Normal file
View file

@ -0,0 +1,13 @@
export 'document/attributes.dart';
export 'document/deprecated/document.dart';
export 'document/deprecated/node.dart';
export 'document/document.dart';
export 'document/node.dart';
export 'document/node_iterator.dart';
export 'document/path.dart';
export 'document/text_delta.dart';
export 'legacy/built_in_attribute_keys.dart';
export 'location/position.dart';
export 'location/selection.dart';
export 'transform/operation.dart';
export 'transform/transaction.dart';

View file

@ -46,3 +46,5 @@ export 'base_component/background_color_mixin.dart';
export 'base_component/markdown_format_helper.dart'; export 'base_component/markdown_format_helper.dart';
export 'base_component/widget/nested_list_widget.dart'; export 'base_component/widget/nested_list_widget.dart';
export 'base_component/block_component_action_wrapper.dart'; export 'base_component/block_component_action_wrapper.dart';
export 'standard_block_components.dart';

View file

@ -168,7 +168,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
isDense: true, isDense: true,
suffixIcon: IconButton( suffixIcon: IconButton(
padding: const EdgeInsets.all(4.0), padding: const EdgeInsets.all(4.0),
icon: const FlowySvg( icon: const EditorSvg(
name: 'clear', name: 'clear',
width: 24, width: 24,
height: 24, height: 24,
@ -291,7 +291,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
FlowySvg( EditorSvg(
name: 'upload_image', name: 'upload_image',
width: 32, width: 32,
height: 32, height: 32,

View file

@ -142,7 +142,7 @@ class _QuoteIcon extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const FlowySvg( return const EditorSvg(
width: 20, width: 20,
height: 20, height: 20,
padding: EdgeInsets.only(right: 5.0), padding: EdgeInsets.only(right: 5.0),

View file

@ -0,0 +1,128 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
const standardBlockComponentConfiguration = BlockComponentConfiguration();
final Map<String, BlockComponentBuilder> standardBlockComponentBuilderMap = {
PageBlockKeys.type: PageBlockComponentBuilder(),
ParagraphBlockKeys.type: TextBlockComponentBuilder(
configuration: standardBlockComponentConfiguration,
),
TodoListBlockKeys.type: TodoListBlockComponentBuilder(
configuration: standardBlockComponentConfiguration.copyWith(
placeholderText: (_) => 'To-do',
),
),
BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
configuration: standardBlockComponentConfiguration.copyWith(
placeholderText: (_) => 'List',
),
),
NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
configuration: standardBlockComponentConfiguration.copyWith(
placeholderText: (_) => 'List',
),
),
QuoteBlockKeys.type: QuoteBlockComponentBuilder(
configuration: standardBlockComponentConfiguration.copyWith(
placeholderText: (_) => 'Quote',
),
),
HeadingBlockKeys.type: HeadingBlockComponentBuilder(
configuration: standardBlockComponentConfiguration.copyWith(
placeholderText: (node) =>
'Heading ${node.attributes[HeadingBlockKeys.level]}',
),
),
ImageBlockKeys.type: ImageBlockComponentBuilder(),
DividerBlockKeys.type: DividerBlockComponentBuilder(
configuration: standardBlockComponentConfiguration.copyWith(
padding: (node) => const EdgeInsets.symmetric(vertical: 8.0),
),
),
};
final List<CharacterShortcutEvent> standardCharacterShortcutEvents = [
// '\n'
insertNewLineAfterBulletedList,
insertNewLineAfterTodoList,
insertNewLineAfterNumberedList,
insertNewLine,
// bulleted list
formatAsteriskToBulletedList,
formatMinusToBulletedList,
// numbered list
formatNumberToNumberedList,
// quote
formatDoubleQuoteToQuote,
// heading
formatSignToHeading,
// checkbox
// format unchecked box, [] or -[]
formatEmptyBracketsToUncheckedBox,
formatHyphenEmptyBracketsToUncheckedBox,
// format checked box, [x] or -[x]
formatFilledBracketsToCheckedBox,
formatHyphenFilledBracketsToCheckedBox,
// slash
slashCommand,
// divider
convertMinusesToDivider,
convertStarsToDivider,
convertUnderscoreToDivider,
// markdown syntax
...markdownSyntaxShortcutEvents,
];
final List<CommandShortcutEvent> standardCommandShortcutEvents = [
// undo, redo
undoCommand,
redoCommand,
// backspace
convertToParagraphCommand,
backspaceCommand,
deleteLeftWordCommand,
deleteLeftSentenceCommand,
// arrow keys
...arrowLeftKeys,
...arrowRightKeys,
...arrowUpKeys,
...arrowDownKeys,
//
homeCommand,
endCommand,
//
toggleTodoListCommand,
...toggleMarkdownCommands,
showLinkMenuCommand,
//
indentCommand,
outdentCommand,
exitEditingCommand,
//
pageUpCommand,
pageDownCommand,
//
selectAllCommand,
// copy and paste
copyCommand,
pasteCommand,
];

View file

@ -192,7 +192,7 @@ class _TodoListIcon extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: onTap, onTap: onTap,
child: FlowySvg( child: EditorSvg(
width: 22, width: 22,
height: 22, height: 22,
padding: const EdgeInsets.only(right: 5.0), padding: const EdgeInsets.only(right: 5.0),

View file

@ -0,0 +1,5 @@
export 'block_component/block_component.dart';
export 'command/transform.dart';
export 'editor_component/editor_component.dart';
export 'toolbar/toolbar.dart';
export 'util/util.dart';

View file

@ -1,3 +1,7 @@
// editor
export 'service/editor.dart';
export 'service/editor_service.dart';
// entry // entry
export 'entry/page_block_component.dart'; export 'entry/page_block_component.dart';
@ -11,6 +15,9 @@ export 'service/shortcut_events.dart';
export 'service/keyboard_service_widget.dart'; export 'service/keyboard_service_widget.dart';
export 'service/scroll_service_widget.dart'; export 'service/scroll_service_widget.dart';
export 'service/selection_service_widget.dart'; export 'service/selection_service_widget.dart';
export 'service/selection_service.dart';
export 'service/keyboard_service.dart';
export 'service/scroll_service.dart';
// toolbar // toolbar
export '../toolbar/mobile/mobile.dart'; export '../toolbar/mobile/mobile.dart';
@ -21,3 +28,6 @@ export 'service/renderer/block_component_container.dart';
export 'service/renderer/block_component_widget.dart'; export 'service/renderer/block_component_widget.dart';
export 'service/renderer/block_component_service.dart'; export 'service/renderer/block_component_service.dart';
export 'service/renderer/block_component_context.dart'; export 'service/renderer/block_component_context.dart';
// style
export 'style/editor_style.dart';

View file

@ -0,0 +1,261 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/flutter/overlay.dart';
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
import 'package:provider/provider.dart';
// workaround for the issue:
// the popover will grab the focus even if it's inside the editor
// setup a global value to indicate whether the focus should be grabbed
// increase the value when the popover is opened
// decrease the value when the popover is closed
// only grab the focus when the value is 0
// the operation must be paired
ValueNotifier<int> keepEditorFocusNotifier = ValueNotifier(0);
class AppFlowyEditor extends StatefulWidget {
@Deprecated('Use AppFlowyEditor.custom or AppFlowyEditor.standard instead')
const AppFlowyEditor({
super.key,
required this.editorState,
this.blockComponentBuilders = const {},
this.characterShortcutEvents = const [],
this.commandShortcutEvents = const [],
this.selectionMenuItems = const [],
this.toolbarItems = const [],
this.editable = true,
this.autoFocus = false,
this.focusedSelection,
this.shrinkWrap = false,
this.scrollController,
this.themeData,
this.editorStyle = const EditorStyle.desktop(),
this.header,
this.footer,
this.focusNode,
});
const AppFlowyEditor.custom({
Key? key,
required EditorState editorState,
ScrollController? scrollController,
bool editable = true,
bool autoFocus = false,
Selection? focusedSelection,
EditorStyle? editorStyle,
Map<String, BlockComponentBuilder> blockComponentBuilders = const {},
List<CharacterShortcutEvent> characterShortcutEvents = const [],
List<CommandShortcutEvent> commandShortcutEvents = const [],
List<SelectionMenuItem> selectionMenuItems = const [],
Widget? header,
Widget? footer,
FocusNode? focusNode,
bool shrinkWrap = false,
}) : this(
key: key,
editorState: editorState,
scrollController: scrollController,
editable: editable,
autoFocus: autoFocus,
focusedSelection: focusedSelection,
blockComponentBuilders: blockComponentBuilders,
characterShortcutEvents: characterShortcutEvents,
commandShortcutEvents: commandShortcutEvents,
selectionMenuItems: selectionMenuItems,
editorStyle: editorStyle ?? const EditorStyle.desktop(),
header: header,
footer: footer,
focusNode: focusNode,
shrinkWrap: shrinkWrap,
);
AppFlowyEditor.standard({
Key? key,
required EditorState editorState,
ScrollController? scrollController,
bool editable = true,
bool autoFocus = false,
Selection? focusedSelection,
EditorStyle? editorStyle,
Widget? header,
Widget? footer,
FocusNode? focusNode,
bool shrinkWrap = false,
}) : this(
key: key,
editorState: editorState,
scrollController: scrollController,
editable: editable,
autoFocus: autoFocus,
focusedSelection: focusedSelection,
blockComponentBuilders: standardBlockComponentBuilderMap,
characterShortcutEvents: standardCharacterShortcutEvents,
commandShortcutEvents: standardCommandShortcutEvents,
editorStyle: editorStyle ?? const EditorStyle.desktop(),
header: header,
footer: footer,
focusNode: focusNode,
shrinkWrap: shrinkWrap,
);
final EditorState editorState;
final EditorStyle editorStyle;
final Map<String, BlockComponentBuilder> blockComponentBuilders;
/// Character event handlers
final List<CharacterShortcutEvent> characterShortcutEvents;
// Command event handlers
final List<CommandShortcutEvent> commandShortcutEvents;
final ScrollController? scrollController;
/// Set the value to false to disable editing.
final bool editable;
/// Set the value to true to focus the editor on the start of the document.
final bool autoFocus;
final Selection? focusedSelection;
final Widget? header;
final Widget? footer;
final FocusNode? focusNode;
/// if true, the editor will be sized to its contents.
final bool shrinkWrap;
@Deprecated('Use FloatingToolbar or MobileToolbar instead.')
final List<ToolbarItem> toolbarItems;
@Deprecated('Customize the style that block component provides instead.')
final ThemeData? themeData;
@Deprecated('Use customSlashCommand instead')
final List<SelectionMenuItem> selectionMenuItems;
@override
State<AppFlowyEditor> createState() => _AppFlowyEditorState();
}
class _AppFlowyEditorState extends State<AppFlowyEditor> {
Widget? services;
EditorState get editorState => widget.editorState;
@override
void initState() {
super.initState();
if (widget.shrinkWrap && widget.scrollController == null) {
throw ArgumentError(
'scrollController must be provided when shrinkWrap is true.',
);
}
editorState.editorStyle = widget.editorStyle;
editorState.selectionMenuItems = widget.selectionMenuItems;
editorState.renderer = _renderer;
editorState.editable = widget.editable;
// auto focus
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_autoFocusIfNeeded();
});
}
@override
void didUpdateWidget(covariant AppFlowyEditor oldWidget) {
super.didUpdateWidget(oldWidget);
editorState.editorStyle = widget.editorStyle;
editorState.editable = widget.editable;
if (editorState.service != oldWidget.editorState.service) {
editorState.selectionMenuItems = widget.selectionMenuItems;
editorState.renderer = _renderer;
}
services = null;
}
@override
Widget build(BuildContext context) {
services ??= _buildServices(context);
return Provider.value(
value: editorState,
child: FocusScope(
child: Overlay(
initialEntries: [
OverlayEntry(
builder: (context) => services!,
),
],
),
),
);
}
Widget _buildServices(BuildContext context) {
Widget child = Container(
padding: widget.editorStyle.padding,
child: editorState.renderer.build(
context,
editorState.document.root,
),
);
if (widget.header != null || widget.footer != null) {
child = Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.header != null) widget.header!,
child,
if (widget.footer != null) widget.footer!,
],
);
}
if (widget.editable) {
child = SelectionServiceWidget(
key: editorState.service.selectionServiceKey,
cursorColor: widget.editorStyle.cursorColor,
selectionColor: widget.editorStyle.selectionColor,
child: KeyboardServiceWidget(
key: editorState.service.keyboardServiceKey,
characterShortcutEvents: widget.characterShortcutEvents,
commandShortcutEvents: widget.commandShortcutEvents,
focusNode: widget.focusNode,
child: child,
),
);
}
return ScrollServiceWidget(
key: editorState.service.scrollServiceKey,
shrinkWrap: widget.shrinkWrap,
scrollController: widget.scrollController,
child: child,
);
}
void _autoFocusIfNeeded() {
if (widget.editable && widget.autoFocus) {
editorState.updateSelectionWithReason(
widget.focusedSelection ??
Selection.single(
path: [0],
startOffset: 0,
),
reason: SelectionUpdateReason.uiEvent,
);
}
}
BlockComponentRendererService get _renderer => BlockComponentRenderer(
builders: {...widget.blockComponentBuilders},
);
}

View file

@ -0,0 +1,50 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/toolbar_service.dart';
import 'package:flutter/material.dart';
class EditorService {
// selection service
final selectionServiceKey = GlobalKey(debugLabel: 'flowy_selection_service');
AppFlowySelectionService get selectionService {
assert(
selectionServiceKey.currentState != null &&
selectionServiceKey.currentState is AppFlowySelectionService,
);
return selectionServiceKey.currentState! as AppFlowySelectionService;
}
// keyboard service
final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service');
AppFlowyKeyboardService? get keyboardService {
if (keyboardServiceKey.currentState != null &&
keyboardServiceKey.currentState is AppFlowyKeyboardService) {
return keyboardServiceKey.currentState! as AppFlowyKeyboardService;
}
return null;
}
// render plugin service
// late AppFlowyRenderPlugin renderPluginService;
late BlockComponentRendererService rendererService;
// toolbar service
final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service');
AppFlowyToolbarService? get toolbarService {
if (toolbarServiceKey.currentState != null &&
toolbarServiceKey.currentState is AppFlowyToolbarService) {
return toolbarServiceKey.currentState! as AppFlowyToolbarService;
}
return null;
}
// scroll service
final scrollServiceKey = GlobalKey(debugLabel: 'flowy_scroll_service');
AppFlowyScrollService? get scrollService {
if (scrollServiceKey.currentState != null &&
scrollServiceKey.currentState is AppFlowyScrollService) {
return scrollServiceKey.currentState! as AppFlowyScrollService;
}
return null;
}
}

View file

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
/// [AppFlowyKeyboardService] is responsible for processing shortcut keys,
/// like command, shift, control keys.
///
/// Usually, this service can be obtained by the following code.
/// ```dart
/// final keyboardService = editorState.service.keyboardService;
///
/// /** Simulates shortcut key input*/
/// keyboardService?.onKey(...);
///
/// /** Enables or disables this service */
/// keyboardService?.enable();
/// keyboardService?.disable();
/// ```
///
abstract class AppFlowyKeyboardService {
/// Processes shortcut key input.
@Deprecated('Not used anymore')
KeyEventResult onKey(RawKeyEvent event);
/// Enables shortcuts service.
void enable();
/// Disables shortcuts service.
///
/// In some cases, if your custom component needs to monitor
/// keyboard events separately,
/// you can disable the keyboard service of flowy_editor.
/// But you need to call the `enable` function to restore after exiting
/// your custom component, otherwise the keyboard service will fails.
void disable({
bool showCursor = false,
UnfocusDisposition disposition = UnfocusDisposition.scope,
});
}

View file

@ -109,9 +109,6 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
@override @override
KeyEventResult onKey(RawKeyEvent event) => throw UnimplementedError(); KeyEventResult onKey(RawKeyEvent event) => throw UnimplementedError();
@override
List<ShortcutEvent> get shortcutEvents => throw UnimplementedError();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = widget.child; Widget child = widget.child;

View file

@ -0,0 +1,52 @@
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scroller.dart';
import 'package:flutter/material.dart';
/// [AppFlowyScrollService] is responsible for processing document scrolling.
///
/// Usually, this service can be obtained by the following code.
/// ```dart
/// final keyboardService = editorState.service.scrollService;
/// ```
///
abstract class AppFlowyScrollService implements AutoScrollerService {
/// Returns the offset of the current document on the vertical axis.
double get dy;
/// Returns the height of the current document.
double? get onePageHeight;
/// Returns the number of pages in the current document.
int? get page;
/// Returns the maximum scroll height on the vertical axis.
double get maxScrollExtent;
/// Returns the minimum scroll height on the vertical axis.
double get minScrollExtent;
/// scroll controller
ScrollController get scrollController;
/// Scrolls to the specified position.
///
/// This function will filter illegal values.
/// Only within the range of minScrollExtent and maxScrollExtent are legal values.
void scrollTo(
double dy, {
Duration? duration,
});
void goBallistic(double velocity);
/// Enables scroll service.
void enable();
/// Disables scroll service.
///
/// In some cases, you can disable scroll service of flowy_editor
/// when your custom component appears,
///
/// But you need to call the `enable` function to restore after exiting
/// your custom component, otherwise the scroll service will fails.
void disable();
}

View file

@ -0,0 +1,81 @@
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/core/location/position.dart';
import 'package:appflowy_editor/src/core/location/selection.dart';
/// [AppFlowySelectionService] is responsible for processing
/// the [Selection] changes and updates.
///
/// Usually, this service can be obtained by the following code.
/// ```dart
/// final selectionService = editorState.service.selectionService;
///
/// /** get current selection value*/
/// final selection = selectionService.currentSelection.value;
///
/// /** get current selected nodes*/
/// final nodes = selectionService.currentSelectedNodes;
/// ```
///
abstract class AppFlowySelectionService {
/// The current [Selection] in editor.
///
/// The value is null if there is no nodes are selected.
ValueNotifier<Selection?> get currentSelection;
/// The current selected [Node]s in editor.
///
/// The order of the result is determined according to the [currentSelection].
/// The result are ordered from back to front if the selection is forward.
/// The result are ordered from front to back if the selection is backward.
///
/// For example, Here is an array of selected nodes, `[n1, n2, n3]`.
/// The result will be `[n3, n2, n1]` if the selection is forward,
/// and `[n1, n2, n3]` if the selection is backward.
///
/// Returns empty result if there is no nodes are selected.
List<Node> get currentSelectedNodes;
/// Updates the selection.
///
/// The editor will update selection area and toolbar area
/// if the [selection] is not collapsed,
/// otherwise, will update the cursor area.
void updateSelection(Selection? selection);
/// Clears the selection area, cursor area and the popup list area.
void clearSelection();
/// Clears the cursor area.
void clearCursor();
/// Returns the [Node] containing to the [offset].
///
/// [offset] must be under the global coordinate system.
Node? getNodeInOffset(Offset offset);
/// Returns the [Position] closest to the [offset].
///
/// Returns null if there is no nodes are selected.
///
/// [offset] must be under the global coordinate system.
Position? getPositionInOffset(Offset offset);
/// The current selection areas's rect in editor.
List<Rect> get selectionRects;
void registerGestureInterceptor(SelectionGestureInterceptor interceptor);
void unregisterGestureInterceptor(String key);
}
class SelectionGestureInterceptor {
SelectionGestureInterceptor({
required this.key,
this.canTap,
});
final String key;
bool Function(TapDownDetails details)? canTap;
}

View file

@ -1,5 +1,6 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/infra/clipboard.dart'; import 'package:appflowy_editor/src/infra/clipboard.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Paste. /// Paste.

View file

@ -0,0 +1,128 @@
import 'package:appflowy_editor/src/flutter/overlay.dart';
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
abstract class AppFlowyToolbarService {
/// Show the toolbar widget beside the offset.
void showInOffset(
Offset offset,
Alignment alignment,
LayerLink layerLink,
);
/// Hide the toolbar widget.
void hide();
/// Trigger the specified handler.
bool triggerHandler(String id);
}
class FlowyToolbar extends StatefulWidget {
const FlowyToolbar({
Key? key,
required this.editorState,
required this.child,
required this.showDefaultToolbar,
}) : super(key: key);
final bool showDefaultToolbar;
final EditorState editorState;
final Widget child;
@override
State<FlowyToolbar> createState() => _FlowyToolbarState();
}
class _FlowyToolbarState extends State<FlowyToolbar>
implements AppFlowyToolbarService {
OverlayEntry? _toolbarOverlay;
final _toolbarWidgetKey = GlobalKey(debugLabel: '_toolbar_widget');
late final List<ToolbarItem> toolbarItems;
@override
void initState() {
super.initState();
toolbarItems = widget.showDefaultToolbar
? [...defaultToolbarItems, ...widget.editorState.toolbarItems]
: [...widget.editorState.toolbarItems]
..sort((a, b) => a.type.compareTo(b.type));
}
@override
void showInOffset(
Offset offset,
Alignment alignment,
LayerLink layerLink,
) {
hide();
final items = _filterItems(toolbarItems);
if (items.isEmpty) {
return;
}
_toolbarOverlay = OverlayEntry(
builder: (context) => ToolbarWidget(
key: _toolbarWidgetKey,
editorState: widget.editorState,
layerLink: layerLink,
offset: offset,
items: items,
alignment: alignment,
),
);
Overlay.of(context)?.insert(_toolbarOverlay!);
}
@override
void hide() {
_toolbarWidgetKey.currentState?.unwrapOrNull<ToolbarMixin>()?.hide();
_toolbarOverlay?.remove();
_toolbarOverlay = null;
}
@override
bool triggerHandler(String id) {
final items = toolbarItems.where((item) => item.id == id);
if (items.length != 1) {
assert(items.length == 1, 'The toolbar item\'s id must be unique');
return false;
}
items.first.handler?.call(widget.editorState, context);
return true;
}
@override
Widget build(BuildContext context) {
return Container(
child: widget.child,
);
}
@override
void dispose() {
hide();
super.dispose();
}
// Filter items that should not be displayed, sort according to type,
// and insert dividers between different types.
List<ToolbarItem> _filterItems(List<ToolbarItem> items) {
final filterItems = items
.where((item) => item.validator?.call(widget.editorState) ?? false)
.toList(growable: false)
..sort((a, b) => a.type.compareTo(b.type));
if (filterItems.isEmpty) {
return [];
}
final List<ToolbarItem> dividedItems = [filterItems.first];
for (var i = 1; i < filterItems.length; i++) {
if (filterItems[i].type != filterItems[i - 1].type) {
dividedItems.add(ToolbarItem.divider());
}
dividedItems.add(filterItems[i]);
}
return dividedItems;
}
}

View file

@ -0,0 +1,384 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
Iterable<ThemeExtension<dynamic>> get lightEditorStyleExtension => [
EditorStyle.light,
];
Iterable<ThemeExtension<dynamic>> get darkEditorStyleExtension => [
EditorStyle.dark,
];
class EditorStyle extends ThemeExtension<EditorStyle> {
// Editor styles
final EdgeInsets? padding;
final Color cursorColor;
final Color selectionColor;
final TextStyleConfiguration textStyleConfiguration;
final TextSpanDecoratorForCustomAttributes? textSpanDecorator;
// @Deprecated('customize the editor\'s background color directly')
final Color? backgroundColor;
// Text styles
@Deprecated('customize the block component directly')
final EdgeInsets? textPadding;
@Deprecated('customize the block component directly')
final TextStyle? textStyle;
@Deprecated('customize the block component directly')
final TextStyle? placeholderTextStyle;
@Deprecated('customize the block component directly')
final double lineHeight;
// Rich text styles
@Deprecated('customize the text style configuration directly')
final TextStyle? bold;
@Deprecated('customize the text style configuration directly')
final TextStyle? italic;
@Deprecated('customize the text style configuration directly')
final TextStyle? underline;
@Deprecated('customize the text style configuration directly')
final TextStyle? strikethrough;
@Deprecated('customize the text style configuration directly')
final TextStyle? href;
@Deprecated('customize the text style configuration directly')
final TextStyle? code;
@Deprecated('customize the text style configuration directly')
final String? highlightColorHex;
// Selection menu styles
@Deprecated('customize the selection menu directly')
final Color? selectionMenuBackgroundColor;
@Deprecated('customize the selection menu directly')
final Color? selectionMenuItemTextColor;
@Deprecated('customize the selection menu directly')
final Color? selectionMenuItemIconColor;
@Deprecated('customize the selection menu directly')
final Color? selectionMenuItemSelectedTextColor;
@Deprecated('customize the selection menu directly')
final Color? selectionMenuItemSelectedIconColor;
@Deprecated('customize the selection menu directly')
final Color? selectionMenuItemSelectedColor;
@Deprecated('customize the selection menu directly')
final Color? toolbarColor;
@Deprecated('customize the selection menu directly')
final double toolbarElevation;
// Item's pop up menu styles
final Color? popupMenuFGColor;
final Color? popupMenuHoverColor;
const EditorStyle({
required this.padding,
required this.cursorColor,
required this.selectionColor,
required this.textStyleConfiguration,
required this.backgroundColor,
required this.selectionMenuBackgroundColor,
required this.selectionMenuItemTextColor,
required this.selectionMenuItemIconColor,
required this.selectionMenuItemSelectedTextColor,
required this.selectionMenuItemSelectedIconColor,
required this.selectionMenuItemSelectedColor,
required this.toolbarColor,
required this.toolbarElevation,
required this.textPadding,
required this.textStyle,
required this.placeholderTextStyle,
required this.bold,
required this.italic,
required this.underline,
required this.strikethrough,
required this.href,
required this.code,
required this.highlightColorHex,
required this.lineHeight,
required this.popupMenuFGColor,
required this.popupMenuHoverColor,
required this.textSpanDecorator,
});
const EditorStyle.desktop({
EdgeInsets? padding,
Color? backgroundColor,
Color? cursorColor,
Color? selectionColor,
TextStyleConfiguration? textStyleConfiguration,
TextSpanDecoratorForCustomAttributes? textSpanDecorator,
}) : this(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 100),
backgroundColor: backgroundColor ?? Colors.white,
cursorColor: cursorColor ?? const Color(0xFF00BCF0),
selectionColor:
selectionColor ?? const Color.fromARGB(53, 111, 201, 231),
textStyleConfiguration: textStyleConfiguration ??
const TextStyleConfiguration(
text: TextStyle(fontSize: 16, color: Colors.black),
),
textSpanDecorator: textSpanDecorator,
selectionMenuBackgroundColor: null,
selectionMenuItemTextColor: null,
selectionMenuItemIconColor: null,
selectionMenuItemSelectedTextColor: null,
selectionMenuItemSelectedIconColor: null,
selectionMenuItemSelectedColor: null,
toolbarColor: null,
toolbarElevation: 0,
textPadding: null,
textStyle: null,
placeholderTextStyle: null,
bold: null,
italic: null,
underline: null,
strikethrough: null,
href: null,
code: null,
highlightColorHex: null,
lineHeight: 0,
popupMenuFGColor: null,
popupMenuHoverColor: null,
);
const EditorStyle.mobile({
EdgeInsets? padding,
Color? backgroundColor,
Color? cursorColor,
Color? selectionColor,
TextStyleConfiguration? textStyleConfiguration,
TextSpanDecoratorForCustomAttributes? textSpanDecorator,
}) : this(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 20),
backgroundColor: backgroundColor ?? Colors.white,
cursorColor: cursorColor ?? const Color(0xFF00BCF0),
selectionColor:
selectionColor ?? const Color.fromARGB(53, 111, 201, 231),
textStyleConfiguration: textStyleConfiguration ??
const TextStyleConfiguration(
text: TextStyle(fontSize: 16, color: Colors.black),
),
textSpanDecorator: textSpanDecorator,
selectionMenuBackgroundColor: null,
selectionMenuItemTextColor: null,
selectionMenuItemIconColor: null,
selectionMenuItemSelectedTextColor: null,
selectionMenuItemSelectedIconColor: null,
selectionMenuItemSelectedColor: null,
toolbarColor: null,
toolbarElevation: 0,
textPadding: null,
textStyle: null,
placeholderTextStyle: null,
bold: null,
italic: null,
underline: null,
strikethrough: null,
href: null,
code: null,
highlightColorHex: null,
lineHeight: 0,
popupMenuFGColor: null,
popupMenuHoverColor: null,
);
@override
EditorStyle copyWith({
EdgeInsets? padding,
Color? backgroundColor,
Color? cursorColor,
Color? selectionColor,
TextStyleConfiguration? textStyleConfiguration,
TextSpanDecoratorForCustomAttributes? textSpanDecorator,
Color? selectionMenuBackgroundColor,
Color? selectionMenuItemTextColor,
Color? selectionMenuItemIconColor,
Color? selectionMenuItemSelectedTextColor,
Color? selectionMenuItemSelectedIconColor,
Color? selectionMenuItemSelectedColor,
Color? toolbarColor,
double? toolbarElevation,
TextStyle? textStyle,
TextStyle? placeholderTextStyle,
TextStyle? bold,
TextStyle? italic,
TextStyle? underline,
TextStyle? strikethrough,
TextStyle? href,
TextStyle? code,
String? highlightColorHex,
double? lineHeight,
Color? popupMenuFGColor,
Color? popupMenuHoverColor,
EdgeInsets? textPadding,
}) {
return EditorStyle(
padding: padding ?? this.padding,
backgroundColor: backgroundColor ?? this.backgroundColor,
cursorColor: cursorColor ?? this.cursorColor,
selectionColor: selectionColor ?? this.selectionColor,
textStyleConfiguration:
textStyleConfiguration ?? this.textStyleConfiguration,
textSpanDecorator: textSpanDecorator ?? this.textSpanDecorator,
selectionMenuBackgroundColor:
selectionMenuBackgroundColor ?? this.selectionMenuBackgroundColor,
selectionMenuItemTextColor:
selectionMenuItemTextColor ?? this.selectionMenuItemTextColor,
selectionMenuItemIconColor:
selectionMenuItemIconColor ?? this.selectionMenuItemIconColor,
selectionMenuItemSelectedTextColor: selectionMenuItemSelectedTextColor ??
this.selectionMenuItemSelectedTextColor,
selectionMenuItemSelectedIconColor: selectionMenuItemSelectedIconColor ??
this.selectionMenuItemSelectedIconColor,
selectionMenuItemSelectedColor:
selectionMenuItemSelectedColor ?? this.selectionMenuItemSelectedColor,
toolbarColor: toolbarColor ?? this.toolbarColor,
toolbarElevation: toolbarElevation ?? this.toolbarElevation,
textPadding: textPadding ?? this.textPadding,
textStyle: textStyle ?? this.textStyle,
placeholderTextStyle: placeholderTextStyle ?? this.placeholderTextStyle,
bold: bold ?? this.bold,
italic: italic ?? this.italic,
underline: underline ?? this.underline,
strikethrough: strikethrough ?? this.strikethrough,
href: href ?? this.href,
code: code ?? this.code,
highlightColorHex: highlightColorHex ?? this.highlightColorHex,
lineHeight: lineHeight ?? this.lineHeight,
popupMenuFGColor: popupMenuFGColor ?? this.popupMenuFGColor,
popupMenuHoverColor: popupMenuHoverColor ?? this.popupMenuHoverColor,
);
}
@override
ThemeExtension<EditorStyle> lerp(
ThemeExtension<EditorStyle>? other,
double t,
) {
if (other == null || other is! EditorStyle) {
return this;
}
return EditorStyle(
padding: EdgeInsets.lerp(padding, other.padding, t),
backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t),
cursorColor: Color.lerp(cursorColor, other.cursorColor, t)!,
textPadding: EdgeInsets.lerp(textPadding, other.textPadding, t),
textStyleConfiguration: other.textStyleConfiguration,
textSpanDecorator: other.textSpanDecorator,
selectionColor: Color.lerp(selectionColor, other.selectionColor, t)!,
selectionMenuBackgroundColor: Color.lerp(
selectionMenuBackgroundColor,
other.selectionMenuBackgroundColor,
t,
),
selectionMenuItemTextColor: Color.lerp(
selectionMenuItemTextColor,
other.selectionMenuItemTextColor,
t,
),
selectionMenuItemIconColor: Color.lerp(
selectionMenuItemIconColor,
other.selectionMenuItemIconColor,
t,
),
selectionMenuItemSelectedTextColor: Color.lerp(
selectionMenuItemSelectedTextColor,
other.selectionMenuItemSelectedTextColor,
t,
),
selectionMenuItemSelectedIconColor: Color.lerp(
selectionMenuItemSelectedIconColor,
other.selectionMenuItemSelectedIconColor,
t,
),
selectionMenuItemSelectedColor: Color.lerp(
selectionMenuItemSelectedColor,
other.selectionMenuItemSelectedColor,
t,
),
toolbarColor: Color.lerp(toolbarColor, other.toolbarColor, t),
toolbarElevation: toolbarElevation,
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,
popupMenuFGColor: Color.lerp(popupMenuFGColor, other.popupMenuFGColor, t),
popupMenuHoverColor:
Color.lerp(popupMenuHoverColor, other.popupMenuHoverColor, t),
);
}
static EditorStyle? of(BuildContext context) {
return Theme.of(context).extension<EditorStyle>();
}
static final light = EditorStyle(
padding: PlatformExtension.isMobile
? const EdgeInsets.symmetric(horizontal: 20)
: const EdgeInsets.symmetric(horizontal: 200),
backgroundColor: Colors.white,
cursorColor: const Color(0xFF00BCF0),
textStyleConfiguration: const TextStyleConfiguration(
text: TextStyle(fontSize: 16, color: Colors.black),
),
textSpanDecorator: (_, textSpan) => textSpan,
selectionColor: const Color.fromARGB(53, 111, 201, 231),
selectionMenuBackgroundColor: const Color(0xFFFFFFFF),
selectionMenuItemTextColor: const Color(0xFF333333),
selectionMenuItemIconColor: const Color(0xFF333333),
selectionMenuItemSelectedTextColor: const Color.fromARGB(255, 56, 91, 247),
selectionMenuItemSelectedIconColor: const Color.fromARGB(255, 56, 91, 247),
selectionMenuItemSelectedColor: const Color(0xFFE0F8FF),
toolbarColor: const Color(0xFF333333),
toolbarElevation: 0.0,
textPadding: const EdgeInsets.symmetric(vertical: 8.0),
textStyle: const TextStyle(fontSize: 16.0, color: Colors.black),
placeholderTextStyle: const TextStyle(fontSize: 16.0, color: Colors.grey),
bold: const TextStyle(fontWeight: FontWeight.bold),
italic: const TextStyle(fontStyle: FontStyle.italic),
underline: const TextStyle(decoration: TextDecoration.underline),
strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
href: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
code: const TextStyle(
fontFamily: 'monospace',
color: Color(0xFF00BCF0),
backgroundColor: Color(0xFFE0F8FF),
),
highlightColorHex: '0x6000BCF0',
lineHeight: 1.5,
popupMenuFGColor: const Color(0xFF333333),
popupMenuHoverColor: const Color(0xFFE0F8FF),
);
@Deprecated(
'EditorStyle.dark has been deprecated. You can now customize the editor style by utilizing the TextStyleConfiguration.',
)
static final dark = light.copyWith(
backgroundColor: Colors.black,
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),
toolbarColor: const Color(0xFF131720),
toolbarElevation: 0.0,
popupMenuFGColor: Colors.white,
popupMenuHoverColor: const Color(0xFF00BCF0),
);
}

Some files were not shown because too many files have changed in this diff Show more