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:
parent
5f4e9ef17e
commit
30ce149371
27 changed files with 158 additions and 113 deletions
|
|
@ -4,8 +4,8 @@
|
|||
# This file should be version controlled.
|
||||
|
||||
version:
|
||||
revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||
channel: stable
|
||||
revision: c07f7888888435fd9df505aa2efc38d3cf65681b
|
||||
channel: unknown
|
||||
|
||||
project_type: app
|
||||
|
||||
|
|
@ -13,26 +13,11 @@ project_type: app
|
|||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||
- 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
|
||||
create_revision: c07f7888888435fd9df505aa2efc38d3cf65681b
|
||||
base_revision: c07f7888888435fd9df505aa2efc38d3cf65681b
|
||||
- platform: web
|
||||
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||
- platform: windows
|
||||
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||
create_revision: c07f7888888435fd9df505aa2efc38d3cf65681b
|
||||
base_revision: c07f7888888435fd9df505aa2efc38d3cf65681b
|
||||
|
||||
# User provided section
|
||||
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
|||
|
||||
// Theme Demo
|
||||
_buildSeparator(context, 'Theme Demo'),
|
||||
_buildListTile(context, 'Bulit In Dark Mode', () {
|
||||
_buildListTile(context, 'Built In Dark Mode', () {
|
||||
_jsonString = Future<String>.value(
|
||||
jsonEncode(_editorState.document.toJson()).toString(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class SimpleEditor extends StatelessWidget {
|
|||
);
|
||||
editorState.logConfiguration
|
||||
..handler = debugPrint
|
||||
..level = LogLevel.all;
|
||||
..level = LogLevel.off;
|
||||
onEditorStateChange(editorState);
|
||||
final scrollController = ScrollController();
|
||||
if (PlatformExtension.isDesktopOrWeb) {
|
||||
|
|
@ -41,13 +41,10 @@ class SimpleEditor extends StatelessWidget {
|
|||
items: [
|
||||
paragraphItem,
|
||||
...headingItems,
|
||||
placeholderItem,
|
||||
...markdownFormatItems,
|
||||
placeholderItem,
|
||||
quoteItem,
|
||||
bulletedListItem,
|
||||
numberedListItem,
|
||||
placeholderItem,
|
||||
linkItem,
|
||||
textColorItem,
|
||||
highlightColorItem
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ class _BulletedListBlockComponentWidgetState
|
|||
),
|
||||
);
|
||||
|
||||
if (widget.actionBuilder != null) {
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class _HeadingBlockComponentWidgetState
|
|||
),
|
||||
);
|
||||
|
||||
if (widget.actionBuilder != null) {
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class _NumberedListBlockComponentWidgetState
|
|||
),
|
||||
);
|
||||
|
||||
if (widget.actionBuilder != null) {
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
|||
),
|
||||
);
|
||||
|
||||
if (widget.actionBuilder != null) {
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class _TextBlockComponentWidgetState extends State<TextBlockComponentWidget>
|
|||
),
|
||||
),
|
||||
);
|
||||
if (widget.actionBuilder != null) {
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ class _TodoListBlockComponentWidgetState
|
|||
),
|
||||
);
|
||||
|
||||
if (widget.actionBuilder != null) {
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -105,17 +106,30 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.commandShortcutEvents.isNotEmpty) {
|
||||
// the Focus widget is used to handle hardware keyboard.
|
||||
return Focus(
|
||||
focusNode: focusNode,
|
||||
onKey: _onKey,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
Widget child = widget.child;
|
||||
// if there is no command shortcut event, we don't need to handle hardware keyboard.
|
||||
// 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
|
||||
|
|
@ -211,5 +225,10 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
|
|||
Log.editor.debug(
|
||||
'keyboard service - focus changed: ${focusNode.hasFocus}}',
|
||||
);
|
||||
|
||||
// clear the selection when the focus is lost.
|
||||
if (!focusNode.hasFocus) {
|
||||
editorState.selection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
return Padding(
|
||||
padding: padding,
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ class _DesktopSelectionServiceWidgetState
|
|||
if (selection.isCollapsed) {
|
||||
// updates cursor area.
|
||||
Log.selection.debug('update cursor area, $selection');
|
||||
_forceShowCursor();
|
||||
_updateCursorAreas(selection.start);
|
||||
} else {
|
||||
// updates selection area.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
export 'insert_newline.dart';
|
||||
export 'slash_command.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';
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export 'format_bold.dart';
|
||||
export 'format_strikethrough.dart';
|
||||
export 'handle_format_by_wrapping_with_double_character.dart';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
@ -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
Loading…
Reference in a new issue