fix: analysis issues (#283)
This commit is contained in:
parent
13805afc33
commit
2b2bce4f3c
82 changed files with 306 additions and 4766 deletions
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
|
|
@ -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
|
||||||
fail_ci_if_error: true
|
with: |
|
||||||
verbose: true
|
fail_ci_if_error: true
|
||||||
|
verbose: true
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
attempt_limit: 10
|
||||||
|
attempt_delay: 10000
|
||||||
|
|
|
||||||
|
|
@ -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/**
|
|
||||||
|
|
|
||||||
|
|
@ -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
13
lib/src/core/core.dart
Normal 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';
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
128
lib/src/editor/block_component/standard_block_components.dart
Normal file
128
lib/src/editor/block_component/standard_block_components.dart
Normal 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,
|
||||||
|
];
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
5
lib/src/editor/editor.dart
Normal file
5
lib/src/editor/editor.dart
Normal 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';
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
261
lib/src/editor/editor_component/service/editor.dart
Normal file
261
lib/src/editor/editor_component/service/editor.dart
Normal 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},
|
||||||
|
);
|
||||||
|
}
|
||||||
50
lib/src/editor/editor_component/service/editor_service.dart
Normal file
50
lib/src/editor/editor_component/service/editor_service.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
52
lib/src/editor/editor_component/service/scroll_service.dart
Normal file
52
lib/src/editor/editor_component/service/scroll_service.dart
Normal 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();
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
128
lib/src/editor/editor_component/service/toolbar_service.dart
Normal file
128
lib/src/editor/editor_component/service/toolbar_service.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
384
lib/src/editor/editor_component/style/editor_style.dart
Normal file
384
lib/src/editor/editor_component/style/editor_style.dart
Normal 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
Loading…
Reference in a new issue