fix: new editor bugs (#137)

* chore: rename file to avoid pub get error

* fix: cursor blink

* fix: remove redundant padding on the mobile platform

* feat: customize the style of the selection menu
This commit is contained in:
Lucas.Xu 2023-05-27 21:15:05 +08:00 • committed by GitHub
commit 30ce149371
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 158 additions and 113 deletions

View file

@ -4,8 +4,8 @@
# This file should be version controlled. # This file should be version controlled.
version: version:
revision: cd41fdd495f6944ecd3506c21e94c6567b073278 revision: c07f7888888435fd9df505aa2efc38d3cf65681b
channel: stable channel: unknown
project_type: app project_type: app
@ -13,26 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 create_revision: c07f7888888435fd9df505aa2efc38d3cf65681b
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 base_revision: c07f7888888435fd9df505aa2efc38d3cf65681b
- platform: android
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
- platform: ios
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
- platform: linux
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
- platform: macos
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
- platform: web - platform: web
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 create_revision: c07f7888888435fd9df505aa2efc38d3cf65681b
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 base_revision: c07f7888888435fd9df505aa2efc38d3cf65681b
- platform: windows
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
# User provided section # User provided section

View file

@ -151,7 +151,7 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
// Theme Demo // Theme Demo
_buildSeparator(context, 'Theme Demo'), _buildSeparator(context, 'Theme Demo'),
_buildListTile(context, 'Bulit In Dark Mode', () { _buildListTile(context, 'Built In Dark Mode', () {
_jsonString = Future<String>.value( _jsonString = Future<String>.value(
jsonEncode(_editorState.document.toJson()).toString(), jsonEncode(_editorState.document.toJson()).toString(),
); );

View file

@ -33,7 +33,7 @@ class SimpleEditor extends StatelessWidget {
); );
editorState.logConfiguration editorState.logConfiguration
..handler = debugPrint ..handler = debugPrint
..level = LogLevel.all; ..level = LogLevel.off;
onEditorStateChange(editorState); onEditorStateChange(editorState);
final scrollController = ScrollController(); final scrollController = ScrollController();
if (PlatformExtension.isDesktopOrWeb) { if (PlatformExtension.isDesktopOrWeb) {
@ -41,13 +41,10 @@ class SimpleEditor extends StatelessWidget {
items: [ items: [
paragraphItem, paragraphItem,
...headingItems, ...headingItems,
placeholderItem,
...markdownFormatItems, ...markdownFormatItems,
placeholderItem,
quoteItem, quoteItem,
bulletedListItem, bulletedListItem,
numberedListItem, numberedListItem,
placeholderItem,
linkItem, linkItem,
textColorItem, textColorItem,
highlightColorItem highlightColorItem

View file

@ -136,7 +136,7 @@ class _BulletedListBlockComponentWidgetState
), ),
); );
if (widget.actionBuilder != null) { if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper( child = BlockComponentActionWrapper(
node: node, node: node,
actionBuilder: widget.actionBuilder!, actionBuilder: widget.actionBuilder!,

View file

@ -132,7 +132,7 @@ class _HeadingBlockComponentWidgetState
), ),
); );
if (widget.actionBuilder != null) { if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper( child = BlockComponentActionWrapper(
node: node, node: node,
actionBuilder: widget.actionBuilder!, actionBuilder: widget.actionBuilder!,

View file

@ -132,7 +132,7 @@ class _NumberedListBlockComponentWidgetState
), ),
); );
if (widget.actionBuilder != null) { if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper( child = BlockComponentActionWrapper(
node: node, node: node,
actionBuilder: widget.actionBuilder!, actionBuilder: widget.actionBuilder!,

View file

@ -114,7 +114,7 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
), ),
); );
if (widget.actionBuilder != null) { if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper( child = BlockComponentActionWrapper(
node: node, node: node,
actionBuilder: widget.actionBuilder!, actionBuilder: widget.actionBuilder!,

View file

@ -128,7 +128,7 @@ class _TextBlockComponentWidgetState extends State<TextBlockComponentWidget>
), ),
), ),
); );
if (widget.actionBuilder != null) { if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper( child = BlockComponentActionWrapper(
node: node, node: node,
actionBuilder: widget.actionBuilder!, actionBuilder: widget.actionBuilder!,

View file

@ -165,7 +165,7 @@ class _TodoListBlockComponentWidgetState
), ),
); );
if (widget.actionBuilder != null) { if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper( child = BlockComponentActionWrapper(
node: node, node: node,
actionBuilder: widget.actionBuilder!, actionBuilder: widget.actionBuilder!,

View file

@ -1,4 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -105,17 +106,30 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.commandShortcutEvents.isNotEmpty) { Widget child = widget.child;
// the Focus widget is used to handle hardware keyboard.
return Focus(
focusNode: focusNode,
onKey: _onKey,
child: widget.child,
);
}
// if there is no command shortcut event, we don't need to handle hardware keyboard. // if there is no command shortcut event, we don't need to handle hardware keyboard.
// like in read-only mode. // like in read-only mode.
return widget.child; if (widget.commandShortcutEvents.isNotEmpty) {
// the Focus widget is used to handle hardware keyboard.
child = Focus(
focusNode: focusNode,
onKey: _onKey,
child: child,
);
}
// ignore the default behavior of the space key on web
if (kIsWeb) {
child = Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.space):
const DoNothingAndStopPropagationIntent(),
},
child: child,
);
}
return child;
} }
/// handle hardware keyboard /// handle hardware keyboard
@ -211,5 +225,10 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
Log.editor.debug( Log.editor.debug(
'keyboard service - focus changed: ${focusNode.hasFocus}}', 'keyboard service - focus changed: ${focusNode.hasFocus}}',
); );
// clear the selection when the focus is lost.
if (!focusNode.hasFocus) {
editorState.selection = null;
}
} }
} }

View file

@ -65,31 +65,6 @@ class BlockComponentContainerState extends State<BlockComponentContainer>
), ),
); );
// if (widget.showBlockComponentActions) {
// child = MouseRegion(
// onEnter: (_) => showActionsNotifier.value = true,
// onExit: (_) => showActionsNotifier.value = alwaysShowActions || false,
// hitTestBehavior: HitTestBehavior.deferToChild,
// opaque: false,
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.start,
// mainAxisSize: MainAxisSize.min,
// children: [
// ValueListenableBuilder<bool>(
// valueListenable: showActionsNotifier,
// builder: (context, value, child) => BlockComponentActionContainer(
// node: widget.node,
// showActions: value,
// actionBuilder: (context) => widget.actionBuilder(context, this),
// ),
// ),
// Expanded(child: child),
// ],
// ),
// );
// }
final padding = widget.configuration.padding(widget.node); final padding = widget.configuration.padding(widget.node);
return Padding( return Padding(
padding: padding, padding: padding,

View file

@ -137,6 +137,7 @@ class _DesktopSelectionServiceWidgetState
if (selection.isCollapsed) { if (selection.isCollapsed) {
// updates cursor area. // updates cursor area.
Log.selection.debug('update cursor area, $selection'); Log.selection.debug('update cursor area, $selection');
_forceShowCursor();
_updateCursorAreas(selection.start); _updateCursorAreas(selection.start);
} else { } else {
// updates selection area. // updates selection area.

View file

@ -1,5 +1,12 @@
export 'insert_newline.dart'; export 'insert_newline.dart';
export 'slash_command.dart'; export 'slash_command.dart';
export 'markdown_syntax_character_shortcut_events.dart'; export 'markdown_syntax_character_shortcut_events.dart';
export 'format_by_wrapping_with_single_character/format_by_wrapping_with_single_character.dart';
export 'format_by_wrapping_with_double_character/format_by_wrapping_with_double_character.dart'; export 'format_single_character/format_single_character.dart';
export 'format_single_character/format_italic.dart';
export 'format_single_character/format_code.dart';
export 'format_single_character/format_strikethrough.dart';
export 'format_double_characters/format_double_characters.dart';
export 'format_double_characters/format_bold.dart';
export 'format_double_characters/format_strikethrough.dart';

View file

@ -1,3 +0,0 @@
export 'format_bold.dart';
export 'format_strikethrough.dart';
export 'handle_format_by_wrapping_with_double_character.dart';

View file

@ -1,4 +0,0 @@
export 'format_code.dart';
export 'format_italic.dart';
export 'format_strikethrough.dart';
export 'handle_format_by_wrapping_with_single_character.dart';

View file

@ -0,0 +1,41 @@
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_events/format_double_characters/format_double_characters.dart';
const _asterisk = '*';
const _underscore = '_';
/// format the text surrounded by double asterisks to bold
///
/// - support
/// - desktop
/// - mobile
/// - web
///
final CharacterShortcutEvent formatDoubleAsterisksToBold =
CharacterShortcutEvent(
key: 'format the text surrounded by double asterisks to bold',
character: _asterisk,
handler: (editorState) async => handleFormatByWrappingWithDoubleCharacter(
editorState: editorState,
character: _asterisk,
formatStyle: DoubleCharacterFormatStyle.bold,
),
);
/// format the text surrounded by double underscores to bold
///
/// - support
/// - desktop
/// - mobile
/// - web
///
final CharacterShortcutEvent formatDoubleUnderscoresToBold =
CharacterShortcutEvent(
key: 'format the text surrounded by double underscores to bold',
character: _underscore,
handler: (editorState) async => handleFormatByWrappingWithDoubleCharacter(
editorState: editorState,
character: _underscore,
formatStyle: DoubleCharacterFormatStyle.bold,
),
);

View file

@ -0,0 +1,115 @@
import 'package:appflowy_editor/appflowy_editor.dart';
// We currently have only one format style is triggered by double characters.
// **abc** or __abc__ -> bold abc
// If we have more in the future, we should add them in this enum and update the [style] variable in [handleDoubleCharactersFormat].
enum DoubleCharacterFormatStyle {
bold,
strikethrough,
}
bool handleFormatByWrappingWithDoubleCharacter({
// for demonstration purpose, the following comments use * to represent the character from the parameter [char].
required EditorState editorState,
required String character,
required DoubleCharacterFormatStyle formatStyle,
}) {
assert(character.length == 1);
final selection = editorState.selection;
// if the selection is not collapsed or the cursor is at the first three index range, we don't need to format it.
// we should return false to let the IME handle it.
if (selection == null || !selection.isCollapsed || selection.end.offset < 4) {
return false;
}
final path = selection.end.path;
final node = editorState.getNodeAtPath(path);
final delta = node?.delta;
// if the node doesn't contain the delta(which means it isn't a text),
// we don't need to format it.
if (node == null || delta == null) {
return false;
}
final plainText = delta.toPlainText();
// The plainText should have at least 4 characters,like **a*.
// The last char in the plainText should be *[char]. Otherwise, we don't need to format it.
if (plainText.length < 4 ||
plainText[selection.end.offset - 1] != character) {
return false;
}
// find all the index of *[char]
final charIndexList = <int>[];
for (var i = 0; i < plainText.length; i++) {
if (plainText[i] == character) {
charIndexList.add(i);
}
}
if (charIndexList.length < 3) {
return false;
}
// for example: **abc* -> [0, 1, 5]
// thirdLastCharIndex = 0, secondLastCharIndex = 1, lastCharIndex = 5
// make sure the third *[char] and second *[char] are connected
// make sure the second *[char] and last *[char] are split by at least one character
final thirdLastCharIndex = charIndexList[charIndexList.length - 3];
final secondLastCharIndex = charIndexList[charIndexList.length - 2];
final lastCharIndex = charIndexList[charIndexList.length - 1];
if (secondLastCharIndex != thirdLastCharIndex + 1 ||
lastCharIndex == secondLastCharIndex + 1) {
return false;
}
// if all the conditions are met, we should format the text.
// 1. delete all the *[char]
// 2. update the style of the text surrounded by the double *[char] to [formatStyle]
// 3. update the cursor position.
final deletion = editorState.transaction
..deleteText(node, lastCharIndex, 1)
..deleteText(node, thirdLastCharIndex, 2);
editorState.apply(deletion);
// To minimize errors, retrieve the format style from an enum that is specific to double characters.
final String style;
switch (formatStyle) {
case DoubleCharacterFormatStyle.bold:
style = 'bold';
break;
case DoubleCharacterFormatStyle.strikethrough:
style = 'strikethrough';
break;
default:
style = '';
assert(false, 'Invalid format style');
}
// if the text is already formatted, we should remove the format.
final sliced = delta.slice(
thirdLastCharIndex + 2,
selection.end.offset - 1,
);
final result = sliced.everyAttributes((element) => element[style] == true);
final format = editorState.transaction
..formatText(
node,
thirdLastCharIndex,
selection.end.offset - thirdLastCharIndex - 3,
{
style: !result,
},
)
..afterSelection = Selection.collapsed(
Position(
path: path,
offset: selection.end.offset - 3,
),
);
editorState.apply(format);
return true;
}

View file

@ -0,0 +1,22 @@
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_events/format_double_characters/format_double_characters.dart';
const _tile = '~';
/// format the text surrounded by double asterisks to bold
///
/// - support
/// - desktop
/// - mobile
/// - web
///
final CharacterShortcutEvent formatDoubleTilesToStrikethrough =
CharacterShortcutEvent(
key: 'format the text surrounded by double asterisks to bold',
character: _tile,
handler: (editorState) async => handleFormatByWrappingWithDoubleCharacter(
editorState: editorState,
character: _tile,
formatStyle: DoubleCharacterFormatStyle.strikethrough,
),
);

View file

@ -0,0 +1,21 @@
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_events/format_single_character/format_single_character.dart';
const _backquote = '`';
/// format the text surrounded by single backquote to code
///
/// - support
/// - desktop
/// - mobile
/// - web
///
final CharacterShortcutEvent formatBackquoteToCode = CharacterShortcutEvent(
key: 'format the text surrounded by single backquote to code',
character: _backquote,
handler: (editorState) async => handleFormatByWrappingWithSingleCharacter(
editorState: editorState,
character: _backquote,
formatStyle: FormatStyleByWrappingWithSingleChar.code,
),
);

View file

@ -0,0 +1,39 @@
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
import 'format_single_character.dart';
const _underscore = '_';
const _asterisk = '*';
/// format the text surrounded by single underscore to italic
///
/// - support
/// - desktop
/// - mobile
/// - web
///
CharacterShortcutEvent formatUnderscoreToItalic = CharacterShortcutEvent(
key: 'format the text surrounded by single underscore to italic',
character: _underscore,
handler: (editorState) async => handleFormatByWrappingWithSingleCharacter(
editorState: editorState,
character: _underscore,
formatStyle: FormatStyleByWrappingWithSingleChar.italic,
),
);
/// format the text surrounded by single asterisk to italic
///
/// - support
/// - desktop
/// - mobile
/// - web
///
final CharacterShortcutEvent formatAsteriskToItalic = CharacterShortcutEvent(
key: 'format the text surrounded by single asterisk to italic',
character: _asterisk,
handler: (editorState) async => handleFormatByWrappingWithSingleCharacter(
editorState: editorState,
character: _asterisk,
formatStyle: FormatStyleByWrappingWithSingleChar.italic,
),
);

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