Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [11.5.0] - 2026-10-07

### Added

- `QuillFocusNode`, which can disable the keyboard while the editor keeps focus.

### Fixed

- Memory leaks in the editor, selection overlay, and toolbar. Notifiers, listeners, and overlay entries the editor creates are now disposed.
- Crash when a focus retry runs after the editor is disposed.
- Implement `onFocusReceived()` so the editor builds on Flutter 3.44.0.
- Stop exporting the deprecated internal `linkPrefixes` constant.

### Changed

- Depend on the T-Pro `quill_native_bridge` fork with `xclip` removed.

## [11.4.2] - 2025-07-22

### Fixed
Expand Down Expand Up @@ -173,7 +190,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Apple-specific font dependency for subscript and superscript functionality from the example.
- **BREAKING**: The [`super_clipboard`](https://pub.dev/packages/super_clipboard) plugin, To restore legacy behavior for `super_clipboard`, use [`flutter_quill_extensions`](https://pub.dev/packages/flutter_quill_extensions) package and `FlutterQuillExtensions.useSuperClipboardPlugin()`.

[unreleased]: https://github.com/singerdmx/flutter-quill/compare/v11.4.2...HEAD
[unreleased]: https://github.com/singerdmx/flutter-quill/compare/v11.5.0...HEAD
[11.5.0]: https://github.com/singerdmx/flutter-quill/compare/v11.4.2...v11.5.0
[11.4.2]: https://github.com/singerdmx/flutter-quill/compare/v10.0.0...v11.4.2
[11.4.1]: https://github.com/singerdmx/flutter-quill/compare/v10.0.0...v11.4.1
[11.4.0]: https://github.com/singerdmx/flutter-quill/compare/v10.0.0...v11.4.0
Expand Down
5 changes: 5 additions & 0 deletions example/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
include: package:flutter_lints/flutter.yaml

analyzer:
errors:
# Example intentionally demos experimental clipboard APIs.
experimental_member_use: ignore
6 changes: 3 additions & 3 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ android {
ndkVersion = flutter.ndkVersion

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_11
jvmTarget = JavaVersion.VERSION_17.toString()
}

defaultConfig {
Expand Down
2 changes: 1 addition & 1 deletion example/android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
4 changes: 2 additions & 2 deletions example/android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ pluginManagement {

plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.3.1" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
id "com.android.application" version "8.11.1" apply false
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
}

include ":app"
24 changes: 12 additions & 12 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -373,26 +373,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.9"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
Expand Down Expand Up @@ -429,10 +429,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.17.0"
mime:
dependency: transitive
description:
Expand Down Expand Up @@ -618,10 +618,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.7"
typed_data:
dependency: transitive
description:
Expand Down Expand Up @@ -698,10 +698,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
video_player:
dependency: transitive
description:
Expand Down
6 changes: 6 additions & 0 deletions flutter_quill_extensions/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
include: ../analysis_options.yaml

analyzer:
errors:
# First-party package. Uses flutter_quill experimental and internal APIs on purpose.
experimental_member_use: ignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_quill/internal.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;

import 'image_load_utils.dart';
Expand Down
9 changes: 4 additions & 5 deletions flutter_quill_test/lib/src/test/widget_tester_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ extension QuillWidgetTesterExt on WidgetTester {
/// ```
Future<void> quillGiveFocus(Finder finder) {
return TestAsyncUtils.guard(() async {
final editor = findEditor(finder);
editor.widget.focusNode.requestFocus();
final focusNode = findRawEditor(finder).widget.config.focusNode
..requestFocus();
await pump();
expect(
editor.widget.focusNode.hasFocus,
focusNode.hasFocus,
isTrue,
);
});
Expand All @@ -31,8 +31,7 @@ extension QuillWidgetTesterExt on WidgetTester {
/// ```
Future<bool> quillHasFocusEditor(Finder finder) {
return TestAsyncUtils.guard(() async {
final editor = findEditor(finder);
return editor.widget.focusNode.hasFocus;
return findRawEditor(finder).widget.config.focusNode.hasFocus;
});
}

Expand Down
2 changes: 1 addition & 1 deletion lib/flutter_quill.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export 'src/editor/raw_editor/raw_editor_state.dart';
export 'src/editor/style_widgets/style_widgets.dart';
export 'src/editor/widgets/cursor.dart';
export 'src/editor/widgets/default_styles.dart';
export 'src/editor/widgets/link.dart';
export 'src/editor/widgets/link.dart' hide linkPrefixes;
export 'src/editor/widgets/text/magnifier.dart';
export 'src/editor/widgets/text/utils/text_block_utils.dart';
export 'src/editor_toolbar_controller_shared/copy_cut_service/copy_cut_service.dart';
Expand Down
4 changes: 2 additions & 2 deletions lib/src/common/utils/quill_node_focus.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ class QuillFocusNode extends FocusNode {
super.descendantsAreFocusable,
super.descendantsAreTraversable,
bool keyboardEnabled = true,
}) : _keyboardEnabled = keyboardEnabled;
}) : _keyboardEnabled = keyboardEnabled; // ignore: prefer_initializing_formals

bool _keyboardEnabled = true;
bool _keyboardEnabled;

bool get keyboardEnabled => _keyboardEnabled;

Expand Down
96 changes: 70 additions & 26 deletions lib/src/editor/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ class QuillEditor extends StatefulWidget {
/// ```
///
QuillEditor({
required this.focusNode,
required this.scrollController,
required this.controller,
this.focusNode,
this.scrollController,
this.config = const QuillEditorConfig(),
super.key,
}) {
Expand All @@ -157,15 +157,14 @@ class QuillEditor extends StatefulWidget {
QuillEditorConfig config = const QuillEditorConfig(),
FocusNode? focusNode,
ScrollController? scrollController,
}) {
return QuillEditor(
key: key,
scrollController: scrollController ?? ScrollController(),
focusNode: focusNode ?? FocusNode(),
controller: controller,
config: config,
);
}
}) =>
QuillEditor(
key: key,
controller: controller,
focusNode: focusNode,
scrollController: scrollController,
config: config,
);

/// Controller object which establishes a link between a rich text document
/// and this editor.
Expand All @@ -175,10 +174,15 @@ class QuillEditor extends StatefulWidget {
final QuillEditorConfig config;

/// Controls whether this editor has keyboard focus.
final FocusNode focusNode;
///
/// When null, the editor creates and disposes its own. Use
/// [QuillEditorState.focusNode] to access the effective node.
final FocusNode? focusNode;

/// The [ScrollController] to use when vertically scrolling the contents.
final ScrollController scrollController;
///
/// When null, the editor creates and disposes its own.
final ScrollController? scrollController;

@override
QuillEditorState createState() => QuillEditorState();
Expand All @@ -199,6 +203,48 @@ class QuillEditorState extends State<QuillEditor>
/// {@macro drag_offset_notifier}
final dragOffsetNotifier = isMobileApp ? ValueNotifier<Offset?>(null) : null;

final _webKeyboardFocusNode = kIsWeb
? FocusNode(
onKeyEvent: (node, event) => KeyEventResult.skipRemainingHandlers,
)
: null;

FocusNode? _ownFocusNode;
ScrollController? _ownScrollController;

FocusNode get focusNode => widget.focusNode ?? (_ownFocusNode ??= FocusNode());

ScrollController get scrollController =>
widget.scrollController ??
(_ownScrollController ??= ScrollController());

@override
void dispose() {
focusNode.removeListener(_onFocusChanged);
dragOffsetNotifier?.dispose();
_webKeyboardFocusNode?.dispose();
_ownFocusNode?.dispose();
_ownScrollController?.dispose();
super.dispose();
}

@override
void didUpdateWidget(QuillEditor oldWidget) {
super.didUpdateWidget(oldWidget);
final oldFocusNode = oldWidget.focusNode ?? _ownFocusNode;
if (oldFocusNode != focusNode) {
oldFocusNode?.removeListener(_onFocusChanged);
focusNode.addListener(_onFocusChanged);
}
}

// Hide toolbar when the editor loses focus.
void _onFocusChanged() {
if (!focusNode.hasFocus) {
_editorKey.currentState?.hideToolbar();
}
}

@override
void initState() {
super.initState();
Expand All @@ -209,18 +255,11 @@ class QuillEditorState extends State<QuillEditor>
config.detectWordBoundary,
);

final focusNode = widget.focusNode;

if (config.autoFocus) {
focusNode.requestFocus();
}

// Hide toolbar when the editor loses focus.
focusNode.addListener(() {
if (!focusNode.hasFocus) {
_editorKey.currentState?.hideToolbar();
}
});
focusNode.addListener(_onFocusChanged);
}

@override
Expand Down Expand Up @@ -269,8 +308,8 @@ class QuillEditorState extends State<QuillEditor>
spaceShortcutEvents: widget.config.spaceShortcutEvents,
onKeyPressed: widget.config.onKeyPressed,
customLeadingBuilder: widget.config.customLeadingBlockBuilder,
focusNode: widget.focusNode,
scrollController: widget.scrollController,
focusNode: focusNode,
scrollController: scrollController,
scrollable: config.scrollable,
enableAlwaysIndentOnTab: config.enableAlwaysIndentOnTab,
scrollBottomInset: config.scrollBottomInset,
Expand Down Expand Up @@ -347,9 +386,7 @@ class QuillEditorState extends State<QuillEditor>
// See issue https://github.com/singerdmx/flutter-quill/issues/304
return KeyboardListener(
onKeyEvent: (_) {},
focusNode: FocusNode(
onKeyEvent: (node, event) => KeyEventResult.skipRemainingHandlers,
),
focusNode: _webKeyboardFocusNode!,
child: editor,
);
}
Expand Down Expand Up @@ -717,6 +754,13 @@ class RenderEditor extends RenderEditableContainerBox
ValueListenable<bool> get selectionEndInViewport => _selectionEndInViewport;
final ValueNotifier<bool> _selectionEndInViewport = ValueNotifier<bool>(true);

@override
void dispose() {
_selectionStartInViewport.dispose();
_selectionEndInViewport.dispose();
super.dispose();
}

void _updateSelectionExtentsVisibility(Offset effectiveOffset) {
final visibleRegion = Offset.zero & size;
final startPosition =
Expand Down
2 changes: 2 additions & 0 deletions lib/src/editor/raw_editor/quill_single_child_scroll_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ class _RenderSingleChildViewport extends RenderBox
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
final paintOffset = _paintOffset;
// ponytail: translateByDouble needs Flutter 3.32; SDK floor is 3.0
// ignore: deprecated_member_use
transform.translate(paintOffset.dx, paintOffset.dy);
}

Expand Down
Loading
Loading