Skip to content

Commit b977342

Browse files
[SuperEditor][iOS] Fix selecting all document when using the native toolbar (Resolves #2579) (#2582)
1 parent 10ad5f2 commit b977342

File tree

2 files changed

+99
-1
lines changed

2 files changed

+99
-1
lines changed

super_editor/lib/src/default_editor/document_ime/document_delta_editing.dart

+43-1
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,11 @@ class TextDeltasDocumentEditor {
258258
if (docSelection != null) {
259259
// We got a selection from the platform.
260260
// This could happen in some software keyboards, like GBoard,
261-
// where the user can swipe over the spacebar to change the selection.
261+
// where the user can swipe over the spacebar to change the selection. This also happens
262+
// when the app uses the native iOS text selection toolbar and the user presses "Select all".
263+
264+
docSelection = _maybeSelectAllOnIos(docSelection);
265+
262266
editor.execute([
263267
ChangeSelectionRequest(
264268
docSelection,
@@ -273,6 +277,44 @@ class TextDeltasDocumentEditor {
273277
_previousImeValue = delta.apply(_previousImeValue);
274278
}
275279

280+
/// Performs a workaround to select all text in the document on iOS when the user presses "Select all".
281+
///
282+
/// On iOS, when the app uses the native text selection toolbar and the user presses "Select all",
283+
/// Flutter sends us a non-text delta with the selection change. However, since we only send to the IME the text
284+
/// of the currently selected nodes, the delta reports the node being selected, not the entire
285+
/// document.
286+
///
287+
/// To workaroud this, whenever iOS reports a selection change that selects an entire node,
288+
/// we select the entire document instead.
289+
DocumentSelection _maybeSelectAllOnIos(DocumentSelection documentSelection) {
290+
if (defaultTargetPlatform != TargetPlatform.iOS) {
291+
return documentSelection;
292+
}
293+
294+
final extentNode = document.getNodeById(documentSelection.extent.nodeId)!;
295+
final isWholeNodeSelected = documentSelection.start.nodeId == documentSelection.end.nodeId &&
296+
documentSelection.start.nodePosition == extentNode.beginningPosition &&
297+
documentSelection.end.nodePosition == extentNode.endPosition;
298+
299+
if (!isWholeNodeSelected) {
300+
// The selection is either across multiple nodes or not the entire node.
301+
// The user didn't press "Select all".
302+
return documentSelection;
303+
}
304+
305+
// The IME reported a selection that selects an entire node. Select the entire document instead.
306+
return DocumentSelection(
307+
base: DocumentPosition(
308+
nodeId: document.first.id,
309+
nodePosition: document.first.beginningPosition,
310+
),
311+
extent: DocumentPosition(
312+
nodeId: document.last.id,
313+
nodePosition: document.last.endPosition,
314+
),
315+
);
316+
}
317+
276318
void insert(DocumentSelection insertionSelection, String textInserted) {
277319
editorImeLog.fine('Inserting "$textInserted" at position "$insertionSelection"');
278320
editorImeLog

super_editor/test/super_editor/mobile/super_editor_ios_selection_test.dart

+56
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'package:flutter/gestures.dart';
2+
import 'package:flutter/services.dart';
23
import 'package:flutter/widgets.dart';
34
import 'package:flutter_test/flutter_test.dart';
5+
import 'package:flutter_test_robots/flutter_test_robots.dart';
46
import 'package:flutter_test_runners/flutter_test_runners.dart';
57
import 'package:super_editor/src/infrastructure/platforms/ios/selection_handles.dart';
68
import 'package:super_editor/super_editor.dart';
@@ -496,6 +498,60 @@ void main() {
496498
await tester.pumpAndSettle();
497499
});
498500
});
501+
502+
testWidgetsOnIos("converts entire paragraph selection to entire document selection", (tester) async {
503+
// This test locks the behavior of a workaround for https://github.com/superlistapp/super_editor/issues/2579.
504+
//
505+
// When using the native text selection toolbar to select all text in a document, the IME sends a delta
506+
// that selects only the text of the currently selected nodes. This is because we only send the text of the
507+
// currently selected nodes to the IME. This test ensures that we select the entire document when we
508+
// receive a delta that selects an entire node.
509+
510+
await tester //
511+
.createDocument()
512+
.withCustomContent(MutableDocument(
513+
nodes: [
514+
ParagraphNode(
515+
id: '1',
516+
text: AttributedText('First paragraph'),
517+
),
518+
ParagraphNode(
519+
id: '2',
520+
text: AttributedText('Second paragraph'),
521+
),
522+
],
523+
))
524+
.pump();
525+
526+
// Place the caret at the beginning of the document.
527+
await tester.placeCaretInParagraph('1', 0);
528+
529+
// Simulate the user pressing "Select all" on the native text selection toolbar.
530+
// Since we send only the text of the currently selected nodes to the IME, this results in a delta
531+
// that selects only the first paragraph.
532+
await tester.ime.sendDeltas(const [
533+
TextEditingDeltaNonTextUpdate(
534+
oldText: '. First paragraph',
535+
selection: TextSelection(baseOffset: 0, extentOffset: 17),
536+
composing: TextRange(start: -1, end: -1),
537+
),
538+
], getter: imeClientGetter);
539+
540+
// Ensure that we selected the entire document.
541+
expect(
542+
SuperEditorInspector.findDocumentSelection(),
543+
const DocumentSelection(
544+
base: DocumentPosition(
545+
nodeId: '1',
546+
nodePosition: TextNodePosition(offset: 0),
547+
),
548+
extent: DocumentPosition(
549+
nodeId: '2',
550+
nodePosition: TextNodePosition(offset: 16),
551+
),
552+
),
553+
);
554+
});
499555
});
500556

501557
group('within ancestor scrollable', () {

0 commit comments

Comments
 (0)