Skip to content

Commit

Permalink
[SuperEditor][Mobile] Fix task and list item splitting on ENTER (Reso…
Browse files Browse the repository at this point in the history
…lves #1157) (#1184)
  • Loading branch information
angelosilvestre authored and matthew-carroll committed Jul 20, 2023
1 parent faab04f commit 0e3e088
Show file tree
Hide file tree
Showing 6 changed files with 734 additions and 30 deletions.
21 changes: 21 additions & 0 deletions super_editor/lib/src/default_editor/common_editor_operations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:super_editor/src/default_editor/paragraph.dart';
import 'package:super_editor/src/default_editor/selection_upstream_downstream.dart';
import 'package:super_editor/src/default_editor/text.dart';
import 'package:super_editor/src/infrastructure/_logging.dart';
import 'package:super_editor/super_editor.dart';

import 'attributions.dart';
import 'horizontal_rule.dart';
Expand Down Expand Up @@ -1668,6 +1669,26 @@ class CommonEditorOperations {
),
]);
}
} else if (extentNode is TaskNode) {
final splitOffset = (composer.selection!.extent.nodePosition as TextNodePosition).offset;

editor.execute([
SplitExistingTaskRequest(
existingNodeId: extentNode.id,
splitOffset: splitOffset,
newNodeId: newNodeId,
),
ChangeSelectionRequest(
DocumentSelection.collapsed(
position: DocumentPosition(
nodeId: newNodeId,
nodePosition: const TextNodePosition(offset: 0),
),
),
SelectionChangeType.insertContent,
SelectionReason.userInteraction,
),
]);
} else {
// We don't know how to handle this type of node position. Do nothing.
editorOpsLog.fine("Can't insert new block-level inline because we don't recognize the selected content type.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,9 @@ final defaultRequestHandlers = [
: null,
(request) => request is SplitExistingTaskRequest
? SplitExistingTaskCommand(
nodeId: request.nodeId,
nodeId: request.existingNodeId,
splitOffset: request.splitOffset,
newNodeId: request.newNodeId,
)
: null,
(request) => request is SplitListItemRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import 'package:super_editor/src/core/document_layout.dart';
import 'package:super_editor/src/core/document_selection.dart';
import 'package:super_editor/src/core/editor.dart';
import 'package:super_editor/src/default_editor/common_editor_operations.dart';
import 'package:super_editor/src/default_editor/list_items.dart';
import 'package:super_editor/src/default_editor/multi_node_editing.dart';
import 'package:super_editor/src/default_editor/paragraph.dart';
import 'package:super_editor/src/default_editor/selection_upstream_downstream.dart';
import 'package:super_editor/src/default_editor/tasks.dart';
import 'package:super_editor/src/default_editor/text.dart';
import 'package:super_editor/src/infrastructure/_logging.dart';

Expand Down Expand Up @@ -488,7 +490,40 @@ class TextDeltasDocumentEditor {

final newNodeId = Editor.createNodeId();

if (extentNode is ParagraphNode) {
if (extentNode is ListItemNode) {
if (extentNode.text.text.isEmpty) {
// The list item is empty. Convert it to a paragraph.
editorOpsLog.finer(
"The current node is an empty list item. Converting it to a paragraph instead of inserting block-level newline.");
editor.execute([
ConvertTextNodeToParagraphRequest(nodeId: extentNode.id),
]);
return;
}

// Split the list item into two.
editorOpsLog.finer("Splitting list item in two.");
editor.execute([
SplitListItemRequest(
nodeId: extentNode.id,
splitPosition: caretPosition.nodePosition as TextNodePosition,
newNodeId: newNodeId,
),
ChangeSelectionRequest(
DocumentSelection.collapsed(
position: DocumentPosition(
nodeId: newNodeId,
nodePosition: const TextNodePosition(offset: 0),
),
),
SelectionChangeType.insertContent,
SelectionReason.userInteraction,
),
]);
final newListItemNode = document.getNodeById(newNodeId)!;

_updateImeRangeMappingAfterNodeSplit(originNode: extentNode, newNode: newListItemNode);
} else if (extentNode is ParagraphNode) {
// Split the paragraph into two. This includes headers, blockquotes, and
// any other block-level paragraph.
final currentExtentPosition = caretPosition.nodePosition as TextNodePosition;
Expand Down Expand Up @@ -518,30 +553,7 @@ class TextDeltasDocumentEditor {
),
]);

final newImeValue = _nextImeValue!;
final imeNewlineIndex = newImeValue.text.indexOf("\n");
final topImeToDocTextRange = TextRange(start: 0, end: imeNewlineIndex);
final bottomImeToDocTextRange = TextRange(start: imeNewlineIndex + 1, end: newImeValue.text.length);

// Update mapping from Document nodes to IME ranges.
_serializedDoc.docTextNodesToImeRanges[extentNode.id] = topImeToDocTextRange;
_serializedDoc.docTextNodesToImeRanges[newTextNode.id] = bottomImeToDocTextRange;

// Remove old mapping from IME TextRange to Document node.
late final MapEntry<TextRange, String> oldImeToDoc;
for (final entry in _serializedDoc.imeRangesToDocTextNodes.entries) {
if (entry.value != extentNode.id) {
continue;
}

oldImeToDoc = entry;
break;
}
_serializedDoc.imeRangesToDocTextNodes.remove(oldImeToDoc.key);

// Update and add mapping from IME TextRanges to Document nodes.
_serializedDoc.imeRangesToDocTextNodes[topImeToDocTextRange] = extentNode.id;
_serializedDoc.imeRangesToDocTextNodes[bottomImeToDocTextRange] = newTextNode.id;
_updateImeRangeMappingAfterNodeSplit(originNode: extentNode, newNode: newTextNode);
} else if (caretPosition.nodePosition is UpstreamDownstreamNodePosition) {
final extentPosition = caretPosition.nodePosition as UpstreamDownstreamNodePosition;
if (extentPosition.affinity == TextAffinity.downstream) {
Expand Down Expand Up @@ -571,6 +583,29 @@ class TextDeltasDocumentEditor {
),
]);
}
} else if (extentNode is TaskNode) {
final splitOffset = (caretPosition.nodePosition as TextNodePosition).offset;

editor.execute([
SplitExistingTaskRequest(
existingNodeId: extentNode.id,
splitOffset: splitOffset,
newNodeId: newNodeId,
),
ChangeSelectionRequest(
DocumentSelection.collapsed(
position: DocumentPosition(
nodeId: newNodeId,
nodePosition: const TextNodePosition(offset: 0),
),
),
SelectionChangeType.insertContent,
SelectionReason.userInteraction,
),
]);
final newTaskNode = document.getNodeById(newNodeId)!;

_updateImeRangeMappingAfterNodeSplit(originNode: extentNode, newNode: newTaskNode);
} else {
// We don't know how to handle this type of node position. Do nothing.
editorOpsLog.fine("Can't insert new block-level inline because we don't recognize the selected content type.");
Expand All @@ -584,4 +619,35 @@ class TextDeltasDocumentEditor {
}
commonOps.insertBlockLevelNewline();
}

/// Updates mappings from Document nodes to IME ranges and IME ranges to Document nodes.
void _updateImeRangeMappingAfterNodeSplit({
required DocumentNode originNode,
required DocumentNode newNode,
}) {
final newImeValue = _nextImeValue!;
final imeNewlineIndex = newImeValue.text.indexOf("\n");
final topImeToDocTextRange = TextRange(start: 0, end: imeNewlineIndex);
final bottomImeToDocTextRange = TextRange(start: imeNewlineIndex + 1, end: newImeValue.text.length);

// Update mapping from Document nodes to IME ranges.
_serializedDoc.docTextNodesToImeRanges[originNode.id] = topImeToDocTextRange;
_serializedDoc.docTextNodesToImeRanges[newNode.id] = bottomImeToDocTextRange;

// Remove old mapping from IME TextRange to Document node.
late final MapEntry<TextRange, String> oldImeToDoc;
for (final entry in _serializedDoc.imeRangesToDocTextNodes.entries) {
if (entry.value != originNode.id) {
continue;
}

oldImeToDoc = entry;
break;
}
_serializedDoc.imeRangesToDocTextNodes.remove(oldImeToDoc.key);

// Update and add mapping from IME TextRanges to Document nodes.
_serializedDoc.imeRangesToDocTextNodes[topImeToDocTextRange] = originNode.id;
_serializedDoc.imeRangesToDocTextNodes[bottomImeToDocTextRange] = newNode.id;
}
}
12 changes: 8 additions & 4 deletions super_editor/lib/src/default_editor/tasks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ ExecutionInstruction enterToInsertNewTask({

editContext.editor.execute([
SplitExistingTaskRequest(
nodeId: node.id,
existingNodeId: node.id,
splitOffset: splitOffset,
),
]);
Expand Down Expand Up @@ -401,22 +401,26 @@ class ConvertParagraphToTaskCommand implements EditCommand {

class SplitExistingTaskRequest implements EditRequest {
const SplitExistingTaskRequest({
required this.nodeId,
required this.existingNodeId,
required this.splitOffset,
this.newNodeId,
});

final String nodeId;
final String existingNodeId;
final int splitOffset;
final String? newNodeId;
}

class SplitExistingTaskCommand implements EditCommand {
const SplitExistingTaskCommand({
required this.nodeId,
required this.splitOffset,
this.newNodeId,
});

final String nodeId;
final int splitOffset;
final String? newNodeId;

@override
void execute(EditContext editContext, CommandExecutor executor) {
Expand All @@ -441,7 +445,7 @@ class SplitExistingTaskCommand implements EditCommand {
}

final newTaskNode = TaskNode(
id: Editor.createNodeId(),
id: newNodeId ?? Editor.createNodeId(),
text: node.text.copyText(splitOffset),
isComplete: false,
);
Expand Down
Loading

0 comments on commit 0e3e088

Please sign in to comment.