Skip to content

Commit

Permalink
[SuperEditor][web] Fix text input after a deletion (Resolves superlis…
Browse files Browse the repository at this point in the history
  • Loading branch information
angelosilvestre committed Aug 17, 2023
1 parent f94aa58 commit 0e8feee
Show file tree
Hide file tree
Showing 10 changed files with 476 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:super_editor/src/default_editor/paragraph.dart';
import 'package:super_editor/src/default_editor/text.dart';
import 'package:super_editor/src/infrastructure/_logging.dart';
import 'package:super_editor/src/infrastructure/keyboard.dart';
import 'package:super_editor/src/infrastructure/text_input.dart';

ExecutionInstruction toggleInteractionModeWhenCmdOrCtrlPressed({
required SuperEditorContext editContext,
Expand Down Expand Up @@ -301,7 +302,7 @@ ExecutionInstruction mergeNodeWithNextWhenDeleteIsPressed({
return ExecutionInstruction.haltExecution;
}

ExecutionInstruction moveUpDownLeftAndRightWithArrowKeys({
ExecutionInstruction moveUpAndDownWithArrowKeys({
required SuperEditorContext editContext,
required RawKeyEvent keyEvent,
}) {
Expand All @@ -310,8 +311,6 @@ ExecutionInstruction moveUpDownLeftAndRightWithArrowKeys({
}

const arrowKeys = [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowUp,
LogicalKeyboardKey.arrowDown,
];
Expand All @@ -323,46 +322,134 @@ ExecutionInstruction moveUpDownLeftAndRightWithArrowKeys({
return ExecutionInstruction.continueExecution;
}

if (defaultTargetPlatform == TargetPlatform.linux &&
keyEvent.isAltPressed &&
(keyEvent.logicalKey == LogicalKeyboardKey.arrowUp || keyEvent.logicalKey == LogicalKeyboardKey.arrowDown)) {
if (defaultTargetPlatform == TargetPlatform.linux && keyEvent.isAltPressed) {
return ExecutionInstruction.continueExecution;
}

bool didMove = false;
if (keyEvent.logicalKey == LogicalKeyboardKey.arrowLeft || keyEvent.logicalKey == LogicalKeyboardKey.arrowRight) {
MovementModifier? movementModifier;
if ((defaultTargetPlatform == TargetPlatform.windows || defaultTargetPlatform == TargetPlatform.linux) &&
keyEvent.isControlPressed) {
movementModifier = MovementModifier.word;
} else if (defaultTargetPlatform == TargetPlatform.macOS && keyEvent.isMetaPressed) {
movementModifier = MovementModifier.line;
} else if (defaultTargetPlatform == TargetPlatform.macOS && keyEvent.isAltPressed) {
movementModifier = MovementModifier.word;
}

if (keyEvent.logicalKey == LogicalKeyboardKey.arrowLeft) {
// Move the caret left/upstream.
didMove = editContext.commonOps.moveCaretUpstream(
expand: keyEvent.isShiftPressed,
movementModifier: movementModifier,
);
} else {
// Move the caret right/downstream.
didMove = editContext.commonOps.moveCaretDownstream(
expand: keyEvent.isShiftPressed,
movementModifier: movementModifier,
);
}
} else if (keyEvent.logicalKey == LogicalKeyboardKey.arrowUp) {
if (keyEvent.logicalKey == LogicalKeyboardKey.arrowUp) {
didMove = editContext.commonOps.moveCaretUp(expand: keyEvent.isShiftPressed);
} else if (keyEvent.logicalKey == LogicalKeyboardKey.arrowDown) {
} else {
didMove = editContext.commonOps.moveCaretDown(expand: keyEvent.isShiftPressed);
}

return didMove ? ExecutionInstruction.haltExecution : ExecutionInstruction.continueExecution;
}

ExecutionInstruction moveLeftAndRightWithArrowKeys({
required SuperEditorContext editContext,
required RawKeyEvent keyEvent,
}) {
if (keyEvent is! RawKeyDownEvent) {
return ExecutionInstruction.continueExecution;
}

const arrowKeys = [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowRight,
];
if (!arrowKeys.contains(keyEvent.logicalKey)) {
return ExecutionInstruction.continueExecution;
}

if (defaultTargetPlatform == TargetPlatform.windows && keyEvent.isAltPressed) {
return ExecutionInstruction.continueExecution;
}

bool didMove = false;
MovementModifier? movementModifier;
if ((defaultTargetPlatform == TargetPlatform.windows || defaultTargetPlatform == TargetPlatform.linux) &&
keyEvent.isControlPressed) {
movementModifier = MovementModifier.word;
} else if (defaultTargetPlatform == TargetPlatform.macOS && keyEvent.isMetaPressed) {
movementModifier = MovementModifier.line;
} else if (defaultTargetPlatform == TargetPlatform.macOS && keyEvent.isAltPressed) {
movementModifier = MovementModifier.word;
}

if (keyEvent.logicalKey == LogicalKeyboardKey.arrowLeft) {
// Move the caret left/upstream.
didMove = editContext.commonOps.moveCaretUpstream(
expand: keyEvent.isShiftPressed,
movementModifier: movementModifier,
);
} else {
// Move the caret right/downstream.
didMove = editContext.commonOps.moveCaretDownstream(
expand: keyEvent.isShiftPressed,
movementModifier: movementModifier,
);
}

return didMove ? ExecutionInstruction.haltExecution : ExecutionInstruction.continueExecution;
}

ExecutionInstruction doNothingWithLeftRightArrowKeysAtMiddleOfTextOnWeb({
required SuperEditorContext editContext,
required RawKeyEvent keyEvent,
}) {
if (!isWeb) {
return ExecutionInstruction.continueExecution;
}

if (keyEvent is! RawKeyDownEvent) {
return ExecutionInstruction.continueExecution;
}

const arrowKeys = [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowRight,
];
if (!arrowKeys.contains(keyEvent.logicalKey)) {
return ExecutionInstruction.continueExecution;
}

if (defaultTargetPlatform == TargetPlatform.windows && keyEvent.isAltPressed) {
return ExecutionInstruction.continueExecution;
}

if (defaultTargetPlatform == TargetPlatform.linux &&
keyEvent.isAltPressed &&
(keyEvent.logicalKey == LogicalKeyboardKey.arrowUp || keyEvent.logicalKey == LogicalKeyboardKey.arrowDown)) {
return ExecutionInstruction.continueExecution;
}

// On web, pressing left or right arrow keys generates non-text deltas.
// We handle those deltas to change the selection. However, if the caret sits at the beginning
// or end of a node, pressing these arrow keys doesn't generate any deltas.
// Therefore, we need to handle the key events to move the selection to the previous/next node.

final currentExtent = editContext.composer.selection!.extent;
final nodeId = currentExtent.nodeId;
final node = editContext.document.getNodeById(nodeId);
if (node == null) {
return ExecutionInstruction.continueExecution;
}

if (node is! TextNode) {
return ExecutionInstruction.continueExecution;
}

if (currentExtent.nodePosition is! TextNodePosition) {
return ExecutionInstruction.continueExecution;
}

final textNodePosition = currentExtent.nodePosition as TextNodePosition;
if (keyEvent.logicalKey == LogicalKeyboardKey.arrowLeft && textNodePosition.offset > 0) {
// We are not at the beginning of the node.
// Let the IME handle the key event.
return ExecutionInstruction.blocked;
}

if (keyEvent.logicalKey == LogicalKeyboardKey.arrowRight && textNodePosition.offset < node.text.text.length) {
// We are not at the end of the node.
// Let the IME handle the key event.
return ExecutionInstruction.blocked;
}

return ExecutionInstruction.continueExecution;
}

ExecutionInstruction moveToLineStartOrEndWithCtrlAOrE({
required SuperEditorContext editContext,
required RawKeyEvent keyEvent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ class TextDeltasDocumentEditor {

if (delta.textInserted == "\n") {
// On iOS, newlines are reported here and also to performAction().
// On Android and web, newlines are only reported here. So, on Android and web,
// On Android, newlines are only reported here. So, on Android,
// we forward the newline action to performAction.
if (defaultTargetPlatform == TargetPlatform.android || kIsWeb) {
if (defaultTargetPlatform == TargetPlatform.android) {
editorImeLog.fine("Received a newline insertion on Android. Forwarding to newline input action.");
onPerformAction(TextInputAction.newline);
} else {
Expand Down Expand Up @@ -175,9 +175,9 @@ class TextDeltasDocumentEditor {

if (delta.replacementText == "\n") {
// On iOS, newlines are reported here and also to performAction().
// On Android and web, newlines are only reported here. So, on Android and web,
// On Android, newlines are only reported here. So, on Android,
// we forward the newline action to performAction.
if (defaultTargetPlatform == TargetPlatform.android || kIsWeb) {
if (defaultTargetPlatform == TargetPlatform.android) {
editorImeLog.fine("Received a newline replacement on Android. Forwarding to newline input action.");
onPerformAction(TextInputAction.newline);
} else {
Expand Down
67 changes: 67 additions & 0 deletions super_editor/lib/src/default_editor/paragraph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:super_editor/src/infrastructure/_logging.dart';
import 'package:super_editor/src/infrastructure/attributed_text_styles.dart';
import 'package:super_editor/src/infrastructure/keyboard.dart';
import 'package:super_editor/src/infrastructure/raw_key_event_extensions.dart';
import 'package:super_editor/src/infrastructure/text_input.dart';

import 'layout_single_column/layout_single_column.dart';
import 'text_tools.dart';
Expand Down Expand Up @@ -811,3 +812,69 @@ ExecutionInstruction moveParagraphSelectionUpWhenBackspaceIsPressed({

return ExecutionInstruction.haltExecution;
}

ExecutionInstruction doNothingWithEnterOnWeb({
required SuperEditorContext editContext,
required RawKeyEvent keyEvent,
}) {
if (keyEvent is! RawKeyDownEvent) {
return ExecutionInstruction.continueExecution;
}

if (keyEvent.logicalKey != LogicalKeyboardKey.enter && keyEvent.logicalKey != LogicalKeyboardKey.numpadEnter) {
return ExecutionInstruction.continueExecution;
}

if (isWeb) {
// On web, pressing enter generates both a key event and a `TextInputAction.newline` action.
// We handle the newline action and ignore the key event.
// We return blocked so the OS can process it.
return ExecutionInstruction.blocked;
}

return ExecutionInstruction.continueExecution;
}

ExecutionInstruction doNothingWithBackspaceOnWeb({
required SuperEditorContext editContext,
required RawKeyEvent keyEvent,
}) {
if (keyEvent is! RawKeyDownEvent) {
return ExecutionInstruction.continueExecution;
}

if (keyEvent.logicalKey != LogicalKeyboardKey.backspace) {
return ExecutionInstruction.continueExecution;
}

if (isWeb) {
// On web, pressing backspace generates both a key event and a deletion delta.
// We handle the deletion delta and ignore the key event.
// We return blocked so the OS can process it.
return ExecutionInstruction.blocked;
}

return ExecutionInstruction.continueExecution;
}

ExecutionInstruction doNothingWithDeleteOnWeb({
required SuperEditorContext editContext,
required RawKeyEvent keyEvent,
}) {
if (keyEvent is! RawKeyDownEvent) {
return ExecutionInstruction.continueExecution;
}

if (keyEvent.logicalKey != LogicalKeyboardKey.delete) {
return ExecutionInstruction.continueExecution;
}

if (isWeb) {
// On web, pressing delete generates both a key event and a deletion delta.
// We handle the deletion delta and ignore the key event.
// We return blocked so the OS can process it.
return ExecutionInstruction.blocked;
}

return ExecutionInstruction.continueExecution;
}
10 changes: 8 additions & 2 deletions super_editor/lib/src/default_editor/super_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,8 @@ final defaultKeyboardActions = <DocumentKeyboardAction>[
cutWhenCmdXIsPressed,
collapseSelectionWhenEscIsPressed,
selectAllWhenCmdAIsPressed,
moveUpDownLeftAndRightWithArrowKeys,
moveLeftAndRightWithArrowKeys,
moveUpAndDownWithArrowKeys,
moveToLineStartWithHome,
moveToLineEndWithEnd,
tabToIndentListItem,
Expand Down Expand Up @@ -904,9 +905,12 @@ final defaultImeKeyboardActions = <DocumentKeyboardAction>[
copyWhenCmdCIsPressed,
cutWhenCmdXIsPressed,
selectAllWhenCmdAIsPressed,
moveUpDownLeftAndRightWithArrowKeys,
moveUpAndDownWithArrowKeys,
doNothingWithLeftRightArrowKeysAtMiddleOfTextOnWeb,
moveLeftAndRightWithArrowKeys,
moveToLineStartWithHome,
moveToLineEndWithEnd,
doNothingWithEnterOnWeb,
enterToInsertNewTask,
enterToInsertBlockNewline,
tabToIndentListItem,
Expand All @@ -920,10 +924,12 @@ final defaultImeKeyboardActions = <DocumentKeyboardAction>[
deleteToStartOfLineWithCmdBackspaceOnMac,
deleteWordUpstreamWithAltBackspaceOnMac,
deleteWordUpstreamWithControlBackspaceOnWindowsAndLinux,
doNothingWithBackspaceOnWeb,
deleteUpstreamContentWithBackspace,
deleteToEndOfLineWithCmdDeleteOnMac,
deleteWordDownstreamWithAltDeleteOnMac,
deleteWordDownstreamWithControlDeleteOnWindowsAndLinux,
doNothingWithDeleteOnWeb,
deleteDownstreamContentWithDelete,
];

Expand Down
1 change: 1 addition & 0 deletions super_editor/lib/src/default_editor/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import 'package:super_editor/src/infrastructure/composable_text.dart';
import 'package:super_editor/src/infrastructure/keyboard.dart';
import 'package:super_editor/src/infrastructure/raw_key_event_extensions.dart';
import 'package:super_editor/src/infrastructure/strings.dart';
import 'package:super_editor/super_editor.dart';
import 'package:super_text_layout/super_text_layout.dart';

import 'layout_single_column/layout_single_column.dart';
Expand Down
32 changes: 32 additions & 0 deletions super_editor/lib/src/infrastructure/text_input.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
import 'package:flutter/foundation.dart';

/// The mode of user text input.
enum TextInputSource {
keyboard,
ime,
}

/// Whether or not we are running on web.
///
/// By default this is the same as [kIsWeb].
///
/// [debugIsWebOverride] may be used to override the natural value of [isWeb].
bool get isWeb => debugIsWebOverride == null //
? kIsWeb
: debugIsWebOverride == WebPlatformOverride.web;

/// Overrides the value of [isWeb].
///
/// This is intended to be used in tests.
///
/// Set it to `null` to use the default value of [isWeb].
///
/// Set it to [WebPlatformOverride.web] to configure to run as if we are on web.
///
/// Set it to [WebPlatformOverride.native] to configure to run as if we are NOT on web.
@visibleForTesting
WebPlatformOverride? debugIsWebOverride;

@visibleForTesting
enum WebPlatformOverride {
/// Configuration to run the app as if we are a native app.
native,

/// Configuration to run the app as if we are on web.
web,
}
Loading

0 comments on commit 0e8feee

Please sign in to comment.