Compare commits

...

22 Commits

Author SHA1 Message Date
Huy Panha f486278d99 Khmer language 8 months ago
Huy Panha d6dfea1c5b
Create quill_km.arb 8 months ago
singerdmx 008d262b70 chore(version): update to version 10.4.1 8 months ago
Cat 82308daa0e
Chore: improve Spell checker API to the example (#2133) 8 months ago
singerdmx f9bcf83aae chore(version): update to version 10.4.0 8 months ago
Cat 7ae1afe068
Feat: include spell checker for example app (#2127) 8 months ago
Łukasz Wiśniewski 11c8bc7d75
enhance stringToColor with a custom defined palette from `DefaultStyles` (#2095) 8 months ago
License name 2937dc8c95
Copy TapAndPanGestureRecognizer from TextField (#2128) 8 months ago
singerdmx bb29a50050 chore(version): update to version 10.3.2 8 months ago
AtlasAutocode dba979e2fe
Fix: Loss of style when backspace (#2125) 8 months ago
singerdmx b0ffb5f9fc chore(version): update to version 10.3.1 8 months ago
Cat da4f0efeaf
Chore: Move spellchecker service to extensions (#2120) 8 months ago
singerdmx b62f413bf7 chore(version): update to version 10.3.0 8 months ago
Cat 0d12456b5f
Feat: Spellchecker for Flutter Quill (#2118) 8 months ago
singerdmx 08856d147e chore(version): update to version 10.2.1 8 months ago
Cat dad352cbf2
Fix: unsafe operation while getting overlayEntry in text_selection (#2117) 8 months ago
Cat b894c5fdd6
Fix: context menu is visible even when selection is collapsed (#2116) 8 months ago
singerdmx 2342509737 chore(version): update to version 10.2.0 8 months ago
AtlasAutocode 56d7d48d57
Fix: Link selection and editing (#2114) 8 months ago
Ellet c7eca10955
docs: add const keyword for QuillSimpleToolbarConfigurations in README 8 months ago
Ellet cdcbd65530 chore: remove outdated TODO in flutter_quill_extensions 8 months ago
Ellet 7f9d5df8f6
refactor!: restructure project into modular architecture for flutter_quill_extensions (#2106) 8 months ago
  1. 55
      CHANGELOG.md
  2. 2
      CHANGELOG_DATA.json
  3. 63
      README.md
  4. 55
      dart_quill_delta/CHANGELOG.md
  5. 2
      dart_quill_delta/pubspec.yaml
  6. 13
      example/lib/screens/home/widgets/home_screen.dart
  7. 6
      example/lib/screens/quill/my_quill_editor.dart
  8. 25
      example/lib/screens/quill/quill_screen.dart
  9. 14
      example/macos/Podfile.lock
  10. 2
      example/macos/Runner/AppDelegate.swift
  11. 55
      flutter_quill_extensions/CHANGELOG.md
  12. 12
      flutter_quill_extensions/lib/embeds/embed_types.dart
  13. 277
      flutter_quill_extensions/lib/embeds/others/image_video_utils.dart
  14. 537
      flutter_quill_extensions/lib/embeds/others/media_button/media_button.dart
  15. 19
      flutter_quill_extensions/lib/embeds/unknown/editor/unknown_embed.dart
  16. 87
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  17. 71
      flutter_quill_extensions/lib/models/config/media/media_button_configurations.dart
  18. 0
      flutter_quill_extensions/lib/src/common/extensions/attribute.dart
  19. 0
      flutter_quill_extensions/lib/src/common/extensions/controller_ext.dart
  20. 122
      flutter_quill_extensions/lib/src/common/image_video_utils.dart
  21. 0
      flutter_quill_extensions/lib/src/common/utils/dart_ui/dart_ui_fake.dart
  22. 0
      flutter_quill_extensions/lib/src/common/utils/dart_ui/dart_ui_real.dart
  23. 0
      flutter_quill_extensions/lib/src/common/utils/element_utils/element_shared_utils.dart
  24. 0
      flutter_quill_extensions/lib/src/common/utils/element_utils/element_utils.dart
  25. 0
      flutter_quill_extensions/lib/src/common/utils/element_utils/element_web_utils.dart
  26. 0
      flutter_quill_extensions/lib/src/common/utils/patterns.dart
  27. 0
      flutter_quill_extensions/lib/src/common/utils/quill_image_utils.dart
  28. 0
      flutter_quill_extensions/lib/src/common/utils/quill_table_utils.dart
  29. 0
      flutter_quill_extensions/lib/src/common/utils/string.dart
  30. 4
      flutter_quill_extensions/lib/src/common/utils/utils.dart
  31. 0
      flutter_quill_extensions/lib/src/editor/formula/formula_embed.dart
  32. 8
      flutter_quill_extensions/lib/src/editor/image/image_embed.dart
  33. 4
      flutter_quill_extensions/lib/src/editor/image/image_embed_types.dart
  34. 16
      flutter_quill_extensions/lib/src/editor/image/image_menu.dart
  35. 10
      flutter_quill_extensions/lib/src/editor/image/image_web_embed.dart
  36. 2
      flutter_quill_extensions/lib/src/editor/image/models/image_configurations.dart
  37. 0
      flutter_quill_extensions/lib/src/editor/image/models/image_web_configurations.dart
  38. 6
      flutter_quill_extensions/lib/src/editor/image/widgets/image.dart
  39. 0
      flutter_quill_extensions/lib/src/editor/image/widgets/image_resizer.dart
  40. 65
      flutter_quill_extensions/lib/src/editor/spell_checker/simple_spell_checker_service.dart
  41. 0
      flutter_quill_extensions/lib/src/editor/table/table_cell_embed.dart
  42. 2
      flutter_quill_extensions/lib/src/editor/table/table_embed.dart
  43. 0
      flutter_quill_extensions/lib/src/editor/table/table_models.dart
  44. 0
      flutter_quill_extensions/lib/src/editor/video/models/video_configurations.dart
  45. 0
      flutter_quill_extensions/lib/src/editor/video/models/video_web_configurations.dart
  46. 0
      flutter_quill_extensions/lib/src/editor/video/models/youtube_video_support_mode.dart
  47. 10
      flutter_quill_extensions/lib/src/editor/video/video_embed.dart
  48. 10
      flutter_quill_extensions/lib/src/editor/video/video_web_embed.dart
  49. 2
      flutter_quill_extensions/lib/src/editor/video/widgets/video_app.dart
  50. 2
      flutter_quill_extensions/lib/src/editor/video/widgets/youtube_video_app.dart
  51. 0
      flutter_quill_extensions/lib/src/editor_toolbar_controller_shared/clipboard/super_clipboard_service.dart
  52. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_picker/image_options.dart
  53. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_picker/image_picker.dart
  54. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_picker/packages/image_picker.dart
  55. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_picker/s_image_picker.dart
  56. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_saver/exceptions.dart
  57. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_saver/image_saver.dart
  58. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_saver/packages/gal.dart
  59. 0
      flutter_quill_extensions/lib/src/editor_toolbar_shared/image_saver/s_image_saver.dart
  60. 4
      flutter_quill_extensions/lib/src/editor_toolbar_shared/shared_configurations.dart
  61. 36
      flutter_quill_extensions/lib/src/flutter_quill_embeds.dart
  62. 6
      flutter_quill_extensions/lib/src/toolbar/camera/camera_button.dart
  63. 4
      flutter_quill_extensions/lib/src/toolbar/camera/camera_types.dart
  64. 2
      flutter_quill_extensions/lib/src/toolbar/camera/models/camera_configurations.dart
  65. 0
      flutter_quill_extensions/lib/src/toolbar/camera/select_camera_action.dart
  66. 2
      flutter_quill_extensions/lib/src/toolbar/formula/formula_button.dart
  67. 0
      flutter_quill_extensions/lib/src/toolbar/formula/models/formula_configurations.dart
  68. 10
      flutter_quill_extensions/lib/src/toolbar/image/image_button.dart
  69. 2
      flutter_quill_extensions/lib/src/toolbar/image/models/image_configurations.dart
  70. 2
      flutter_quill_extensions/lib/src/toolbar/image/select_image_source.dart
  71. 0
      flutter_quill_extensions/lib/src/toolbar/table/models/table_configurations.dart
  72. 5
      flutter_quill_extensions/lib/src/toolbar/table/table_button.dart
  73. 4
      flutter_quill_extensions/lib/src/toolbar/video/models/video.dart
  74. 2
      flutter_quill_extensions/lib/src/toolbar/video/models/video_configurations.dart
  75. 2
      flutter_quill_extensions/lib/src/toolbar/video/select_video_source.dart
  76. 10
      flutter_quill_extensions/lib/src/toolbar/video/video_button.dart
  77. 5
      flutter_quill_extensions/pubspec.yaml
  78. 55
      flutter_quill_test/CHANGELOG.md
  79. 2
      flutter_quill_test/pubspec.yaml
  80. 2
      lib/flutter_quill.dart
  81. 13
      lib/src/common/utils/color.dart
  82. 6
      lib/src/controller/quill_controller.dart
  83. 21
      lib/src/document/document.dart
  84. 27
      lib/src/document/nodes/line.dart
  85. 6
      lib/src/editor/config/editor_configurations.dart
  86. 46
      lib/src/editor/editor.dart
  87. 2
      lib/src/editor/raw_editor/raw_editor.dart
  88. 87
      lib/src/editor/raw_editor/raw_editor_actions.dart
  89. 27
      lib/src/editor/raw_editor/raw_editor_state.dart
  90. 13
      lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart
  91. 37
      lib/src/editor/spellchecker/default_spellchecker_service.dart
  92. 39
      lib/src/editor/spellchecker/spellchecker_service.dart
  93. 36
      lib/src/editor/spellchecker/spellchecker_service_provider.dart
  94. 5
      lib/src/editor/widgets/default_styles.dart
  95. 684
      lib/src/editor/widgets/delegate.dart
  96. 25
      lib/src/editor/widgets/text/text_line.dart
  97. 397
      lib/src/editor/widgets/text/text_selection.dart
  98. 7
      lib/src/l10n/generated/quill_localizations.dart
  99. 8
      lib/src/l10n/generated/quill_localizations_ar.dart
  100. 8
      lib/src/l10n/generated/quill_localizations_bg.dart
  101. Some files were not shown because too many files have changed in this diff Show More

@ -4,6 +4,61 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## 10.4.1
* Chore: improve Spell checker API to the example by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2133
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.4.0...v10.4.1
## 10.4.0
* Copy TapAndPanGestureRecognizer from TextField by @demoYang in https://github.com/singerdmx/flutter-quill/pull/2128
* enhance stringToColor with a custom defined palette from `DefaultStyles` by @vishna in https://github.com/singerdmx/flutter-quill/pull/2095
* Feat: include spell checker for example app by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2127
## New Contributors
* @vishna made their first contribution in https://github.com/singerdmx/flutter-quill/pull/2095
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.3.3...v10.4.0
## 10.3.2
* Fix: Loss of style when backspace by @AtlasAutocode in https://github.com/singerdmx/flutter-quill/pull/2125
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.3.1...v10.3.2
## 10.3.1
* Chore: Move spellchecker service to extensions by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2120
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.3.0...v10.3.1
## 10.3.0
* Feat: Spellchecker for Flutter Quill by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2118
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.2.1...v10.3.0
## 10.2.1
* Fix: context menu is visible even when selection is collapsed by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2116
* Fix: unsafe operation while getting overlayEntry in text_selection by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2117
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.2.0...v10.2.1
## 10.2.0
* refactor!: restructure project into modular architecture for flutter_quill_extensions by @EchoEllet in https://github.com/singerdmx/flutter-quill/pull/2106
* Fix: Link selection and editing by @AtlasAutocode in https://github.com/singerdmx/flutter-quill/pull/2114
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.1.10...v10.2.0
## 10.1.10 ## 10.1.10
* Fix(example): image_cropper outdated version by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2100 * Fix(example): image_cropper outdated version by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2100

File diff suppressed because one or more lines are too long

@ -64,6 +64,7 @@ You can join our [Slack Group] for discussion.
- [🛠 Using the embed blocks from `flutter_quill_extensions`](#-using-the-embed-blocks-from-flutter_quill_extensions) - [🛠 Using the embed blocks from `flutter_quill_extensions`](#-using-the-embed-blocks-from-flutter_quill_extensions)
- [🔗 Links](#-links-2) - [🔗 Links](#-links-2)
- [🔄 Conversion to HTML](#-conversion-to-html) - [🔄 Conversion to HTML](#-conversion-to-html)
- [📝 Spelling checker](#-Spelling-checker)
- [🌐 Translation](#-translation) - [🌐 Translation](#-translation)
- [🧪 Testing](#-testing) - [🧪 Testing](#-testing)
- [👥 Contributors](#-contributors) - [👥 Contributors](#-contributors)
@ -136,7 +137,7 @@ and attach the `QuillController` to them:
```dart ```dart
QuillSimpleToolbar( QuillSimpleToolbar(
controller: _controller, controller: _controller,
configurations: QuillSimpleToolbarConfigurations(), configurations: const QuillSimpleToolbarConfigurations(),
), ),
Expanded( Expanded(
child: QuillEditor.basic( child: QuillEditor.basic(
@ -282,6 +283,66 @@ The following packages can be used:
3. [`flutter_quill_to_pdf`](https://pub.dev/packages/flutter_quill_to_pdf): To convert **Delta** To **PDF**. 3. [`flutter_quill_to_pdf`](https://pub.dev/packages/flutter_quill_to_pdf): To convert **Delta** To **PDF**.
4. [`markdown_quill`](https://pub.dev/packages/markdown_quill): To convert **Markdown** To **Delta** and vice versa. 4. [`markdown_quill`](https://pub.dev/packages/markdown_quill): To convert **Markdown** To **Delta** and vice versa.
## 📝 Spelling checker
A spell checker is a software tool or feature integrated into various text processing applications that automatically identifies and corrects spelling errors in a written document. It works by comparing the words in the text against a built-in dictionary. If a word isn't found in the dictionary or doesn't match any known word patterns, the spell checker highlights it as a potential error.
#### Benefits of a spell checker include:
* Improved Accuracy: It helps writers avoid common spelling mistakes, ensuring that the text is free of errors.
* Time-Saving: Automatically detecting errors reduces the time needed for manual proofreading.
* Enhanced Professionalism: Correctly spelled words contribute to the overall professionalism of documents, which is crucial in academic, business, and formal writing.
* Multilingual Support: Many spell checkers support multiple languages, making it easier for users to write accurately in different languages.
> [!IMPORTANT]
> The spell checker usually does not work as expected in most cases. **Many translations are not supported** such as: `Chinese`, `Japanese`, `Korean`, `Hebrew`, `Arabic`, `Russian`, etc. For now it is a purely **experimental** feature that may have **code that will be modified** in future versions.
#### The translations supported so far are:
* **German** - `de` (may contain errors or missing words)
* **English** - `en` (currently adding missing translations)
* **Spanish** - `es` (currently adding missing translations)
* **French** - `fr` (may contain errors or missing words)
* **Italian** - `it` (currently adding missing translations)
* **Norwegian** - `no` (may contain errors or missing words)
* **Portuguese** - `pt` (may contain errors or missing words)
* **Swedish** - `sv` (may contain errors or missing words)
_**Note**: If you have knowledge about any of these available languages or the unsupported ones, you can make a pull request to add support or add words that are not currently in [simple_spell_checker](https://github.com/CatHood0/simple_spell_checker)_.
In order to activate this functionality you can use the following code:
```dart
// you can use the language of your preference or directly select the language of the operating system
final language = 'en'; // or Localizations.localeOf(context).languageCode
FlutterQuillExtensions.useSpellCheckerService(language);
```
When you no longer need to have the Spell checker activated you can simply use `dispose()` of the `SpellCheckerServiceProvider` class:
```dart
// dispose all service and it cannot be used after this
SpellCheckerServiceProvider.dispose();
```
If what we want is to **close the StreamControllers** without deleting the values that are already stored in it, we can set `onlyPartial` to `true`.
```dart
// it can be still used by the editor
SpellCheckerServiceProvider.dispose(onlyPartial: true);
```
One use of this would be having the opportunity to **activate and deactivate** the service when we want, we can see this in the example that we have in this package, in which you can see that on each screen, we have a button that dynamically activates and deactivates the service. To do this is pretty simple:
```dart
SpellCheckerServiceProvider.toggleState();
// use isServiceActive to get the state of the service
SpellCheckerServiceProvider.isServiceActive();
setState(() {});
```
Open this [page](https://pub.dev/packages/simple_spell_checker) for more information.
## 🌐 Translation ## 🌐 Translation
The package offers translations for the quill toolbar and editor, it will follow the system locale unless you set your The package offers translations for the quill toolbar and editor, it will follow the system locale unless you set your

@ -4,6 +4,61 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## 10.4.1
* Chore: improve Spell checker API to the example by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2133
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.4.0...v10.4.1
## 10.4.0
* Copy TapAndPanGestureRecognizer from TextField by @demoYang in https://github.com/singerdmx/flutter-quill/pull/2128
* enhance stringToColor with a custom defined palette from `DefaultStyles` by @vishna in https://github.com/singerdmx/flutter-quill/pull/2095
* Feat: include spell checker for example app by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2127
## New Contributors
* @vishna made their first contribution in https://github.com/singerdmx/flutter-quill/pull/2095
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.3.3...v10.4.0
## 10.3.2
* Fix: Loss of style when backspace by @AtlasAutocode in https://github.com/singerdmx/flutter-quill/pull/2125
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.3.1...v10.3.2
## 10.3.1
* Chore: Move spellchecker service to extensions by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2120
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.3.0...v10.3.1
## 10.3.0
* Feat: Spellchecker for Flutter Quill by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2118
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.2.1...v10.3.0
## 10.2.1
* Fix: context menu is visible even when selection is collapsed by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2116
* Fix: unsafe operation while getting overlayEntry in text_selection by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2117
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.2.0...v10.2.1
## 10.2.0
* refactor!: restructure project into modular architecture for flutter_quill_extensions by @EchoEllet in https://github.com/singerdmx/flutter-quill/pull/2106
* Fix: Link selection and editing by @AtlasAutocode in https://github.com/singerdmx/flutter-quill/pull/2114
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.1.10...v10.2.0
## 10.1.10 ## 10.1.10
* Fix(example): image_cropper outdated version by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2100 * Fix(example): image_cropper outdated version by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2100

@ -1,6 +1,6 @@
name: dart_quill_delta name: dart_quill_delta
description: A port of quill-js-delta from typescript to dart description: A port of quill-js-delta from typescript to dart
version: 10.1.10 version: 10.4.1
homepage: https://github.com/singerdmx/flutter-quill/tree/master/dart_quill_delta/ homepage: https://github.com/singerdmx/flutter-quill/tree/master/dart_quill_delta/
repository: https://github.com/singerdmx/flutter-quill/tree/master/dart_quill_delta/ repository: https://github.com/singerdmx/flutter-quill/tree/master/dart_quill_delta/
issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/

@ -15,11 +15,22 @@ import '../../quill/samples/quill_videos_sample.dart';
import '../../settings/widgets/settings_screen.dart'; import '../../settings/widgets/settings_screen.dart';
import 'example_item.dart'; import 'example_item.dart';
class HomeScreen extends StatelessWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
static const routeName = '/home'; static const routeName = '/home';
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
void dispose() {
SpellCheckerServiceProvider.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(

@ -7,10 +7,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart' import 'package:flutter_quill/extensions.dart'
show isAndroid, isDesktop, isIOS, isWeb; show isAndroid, isDesktop, isIOS, isWeb;
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill_extensions/embeds/widgets/image.dart'
show getImageProviderByImageSource, imageFileExtensions;
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
import 'package:flutter_quill_extensions/models/config/video/editor/youtube_video_support_mode.dart'; // ignore: implementation_imports
import 'package:flutter_quill_extensions/src/editor/image/widgets/image.dart'
show getImageProviderByImageSource, imageFileExtensions;
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import '../../extensions/scaffold_messenger.dart'; import '../../extensions/scaffold_messenger.dart';

@ -3,7 +3,10 @@ import 'dart:convert' show jsonEncode;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart' import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'
show FlutterQuillEmbeds, QuillSharedExtensionsConfigurations; show
FlutterQuillEmbeds,
FlutterQuillExtensions,
QuillSharedExtensionsConfigurations;
import 'package:share_plus/share_plus.dart' show Share; import 'package:share_plus/share_plus.dart' show Share;
import '../../extensions/scaffold_messenger.dart'; import '../../extensions/scaffold_messenger.dart';
@ -11,6 +14,8 @@ import '../shared/widgets/home_screen_button.dart';
import 'my_quill_editor.dart'; import 'my_quill_editor.dart';
import 'my_quill_toolbar.dart'; import 'my_quill_toolbar.dart';
var _isSpellcheckerActive = false;
@immutable @immutable
class QuillScreenArgs { class QuillScreenArgs {
const QuillScreenArgs({required this.document}); const QuillScreenArgs({required this.document});
@ -56,10 +61,28 @@ class _QuillScreenState extends State<QuillScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_controller.readOnly = _isReadOnly; _controller.readOnly = _isReadOnly;
if (!_isSpellcheckerActive) {
_isSpellcheckerActive = true;
FlutterQuillExtensions.useSpellCheckerService(
Localizations.localeOf(context).languageCode);
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Flutter Quill'), title: const Text('Flutter Quill'),
actions: [ actions: [
IconButton(
tooltip: 'Spell-checker',
onPressed: () {
SpellCheckerServiceProvider.toggleState();
setState(() {});
},
icon: Icon(
Icons.document_scanner,
color: SpellCheckerServiceProvider.isServiceActive()
? Colors.red.withOpacity(0.5)
: null,
),
),
IconButton( IconButton(
tooltip: 'Share', tooltip: 'Share',
onPressed: () { onPressed: () {

@ -18,8 +18,6 @@ PODS:
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- printing (1.0.0):
- FlutterMacOS
- share_plus (0.0.1): - share_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- sqflite (0.0.3): - sqflite (0.0.3):
@ -42,7 +40,6 @@ DEPENDENCIES:
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
- irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`) - irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- printing (from `Flutter/ephemeral/.symlinks/plugins/printing/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
- super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`)
@ -70,8 +67,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos :path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos
path_provider_foundation: path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
printing:
:path: Flutter/ephemeral/.symlinks/plugins/printing/macos
share_plus: share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
sqflite: sqflite:
@ -86,19 +81,18 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
printing: 1dd6a1fce2209ec240698e2439a4adbb9b427637
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
video_player_avfoundation: 2b4384f3b157206b5e150a0083cdc0c905d260d3 video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
PODFILE CHECKSUM: 7159dd71cf9f57a5669bb2dee7a5030dbcc0483f PODFILE CHECKSUM: 7159dd71cf9f57a5669bb2dee7a5030dbcc0483f

@ -1,7 +1,7 @@
import Cocoa import Cocoa
import FlutterMacOS import FlutterMacOS
@NSApplicationMain @main
class AppDelegate: FlutterAppDelegate { class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true return true

@ -4,6 +4,61 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## 10.4.1
* Chore: improve Spell checker API to the example by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2133
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.4.0...v10.4.1
## 10.4.0
* Copy TapAndPanGestureRecognizer from TextField by @demoYang in https://github.com/singerdmx/flutter-quill/pull/2128
* enhance stringToColor with a custom defined palette from `DefaultStyles` by @vishna in https://github.com/singerdmx/flutter-quill/pull/2095
* Feat: include spell checker for example app by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2127
## New Contributors
* @vishna made their first contribution in https://github.com/singerdmx/flutter-quill/pull/2095
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.3.3...v10.4.0
## 10.3.2
* Fix: Loss of style when backspace by @AtlasAutocode in https://github.com/singerdmx/flutter-quill/pull/2125
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.3.1...v10.3.2
## 10.3.1
* Chore: Move spellchecker service to extensions by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2120
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.3.0...v10.3.1
## 10.3.0
* Feat: Spellchecker for Flutter Quill by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2118
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.2.1...v10.3.0
## 10.2.1
* Fix: context menu is visible even when selection is collapsed by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2116
* Fix: unsafe operation while getting overlayEntry in text_selection by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2117
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.2.0...v10.2.1
## 10.2.0
* refactor!: restructure project into modular architecture for flutter_quill_extensions by @EchoEllet in https://github.com/singerdmx/flutter-quill/pull/2106
* Fix: Link selection and editing by @AtlasAutocode in https://github.com/singerdmx/flutter-quill/pull/2114
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.1.10...v10.2.0
## 10.1.10 ## 10.1.10
* Fix(example): image_cropper outdated version by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2100 * Fix(example): image_cropper outdated version by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2100

@ -1,12 +0,0 @@
import 'package:cross_file/cross_file.dart' show XFile;
typedef MediaFileUrl = String;
typedef MediaFilePicker = Future<XFile?> Function(QuillMediaType mediaType);
typedef MediaPickedCallback = Future<MediaFileUrl> Function(XFile file);
enum QuillMediaType { image, video }
extension QuillMediaTypeX on QuillMediaType {
bool get isImage => this == QuillMediaType.image;
bool get isVideo => this == QuillMediaType.video;
}

@ -1,277 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart' show QuillDialogTheme;
import 'package:flutter_quill/translations.dart';
import '../../utils/patterns.dart';
enum LinkType {
video,
image,
}
class TypeLinkDialog extends StatefulWidget {
const TypeLinkDialog({
required this.linkType,
this.dialogTheme,
this.link,
this.linkRegExp,
super.key,
});
final QuillDialogTheme? dialogTheme;
final String? link;
final RegExp? linkRegExp;
final LinkType linkType;
@override
TypeLinkDialogState createState() => TypeLinkDialogState();
}
class TypeLinkDialogState extends State<TypeLinkDialog> {
late String _link;
late TextEditingController _controller;
RegExp? _linkRegExp;
@override
void initState() {
super.initState();
_link = widget.link ?? '';
_controller = TextEditingController(text: _link);
_linkRegExp = widget.linkRegExp;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
content: TextField(
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
maxLines: null,
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
labelText: context.loc.pasteLink,
hintText: widget.linkType == LinkType.image
? context.loc.pleaseEnterAValidImageURL
: context.loc.pleaseEnterAValidVideoURL,
labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
),
autofocus: true,
onChanged: _linkChanged,
controller: _controller,
onEditingComplete: () {
if (!_canPress()) {
return;
}
_applyLink();
},
),
actions: [
TextButton(
onPressed: _canPress() ? _applyLink : null,
child: Text(
context.loc.ok,
style: widget.dialogTheme?.labelTextStyle,
),
),
],
);
}
void _linkChanged(String value) {
setState(() {
_link = value;
});
}
void _applyLink() {
Navigator.pop(context, _link.trim());
}
RegExp get linkRegExp {
final customRegExp = _linkRegExp;
if (customRegExp != null) {
return customRegExp;
}
switch (widget.linkType) {
case LinkType.video:
if (youtubeRegExp.hasMatch(_link)) {
return youtubeRegExp;
}
return videoRegExp;
case LinkType.image:
return imageRegExp;
}
}
bool _canPress() {
if (_link.isEmpty) {
return false;
}
if (widget.linkType == LinkType.image) {}
return _link.isNotEmpty && linkRegExp.hasMatch(_link);
}
}
// @immutable
// class ImageVideoUtils {
// const ImageVideoUtils._();
// static Future<MediaPickSetting?> selectMediaPickSetting(
// BuildContext context,
// ) =>
// showDialog<MediaPickSetting>(
// context: context,
// builder: (ctx) => AlertDialog(
// contentPadding: EdgeInsets.zero,
// content: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// TextButton.icon(
// icon: const Icon(
// Icons.collections,
// color: Colors.orangeAccent,
// ),
// label: Text('Gallery'.i18n),
// onPressed: () => Navigator.pop(ctx,
// MediaPickSetting.gallery),
// ),
// TextButton.icon(
// icon: const Icon(
// Icons.link,
// color: Colors.cyanAccent,
// ),
// label: Text('Link'.i18n),
// onPressed: () => Navigator.pop(ctx, MediaPickSetting.link),
// )
// ],
// ),
// ),
// );
// /// For image picking logic
// static Future<void> handleImageButtonTap(
// BuildContext context,
// QuillController controller,
// ImageSource imageSource,
// OnImagePickCallback onImagePickCallback, {
// FilePickImpl? filePickImpl,
// WebImagePickImpl? webImagePickImpl,
// }) async {
// String? imageUrl;
// if (kIsWeb) {
// if (webImagePickImpl != null) {
// imageUrl = await webImagePickImpl(onImagePickCallback);
// return;
// }
// final file = await ImagePicker()
//.pickImage(source: ImageSource.gallery);
// imageUrl = file?.path;
// if (imageUrl == null) {
// return;
// }
// } else if (isMobile()) {
// imageUrl = await _pickImage(imageSource, onImagePickCallback);
// } else {
// assert(filePickImpl != null, 'Desktop must provide filePickImpl');
// imageUrl =
// await _pickImageDesktop
//(context, filePickImpl!, onImagePickCallback);
// }
// if (imageUrl == null) {
// return;
// }
// controller.insertImageBlock(
// imageUrl: imageUrl,
// );
// }
// static Future<String?> _pickImage(
// ImageSource source,
// OnImagePickCallback onImagePickCallback,
// ) async {
// final pickedFile = await ImagePicker().pickImage(source: source);
// if (pickedFile == null) {
// return null;
// }
// return onImagePickCallback(File(pickedFile.path));
// }
// static Future<String?> _pickImageDesktop(
// BuildContext context,
// FilePickImpl filePickImpl,
// OnImagePickCallback onImagePickCallback,
// ) async {
// final filePath = await filePickImpl(context);
// if (filePath == null || filePath.isEmpty) return null;
// final file = File(filePath);
// return onImagePickCallback(file);
// }
// /// For video picking logic
// static Future<void> handleVideoButtonTap(
// BuildContext context,
// QuillController controller,
// ImageSource videoSource,
// OnVideoPickCallback onVideoPickCallback, {
// FilePickImpl? filePickImpl,
// WebVideoPickImpl? webVideoPickImpl,
// }) async {
// final index = controller.selection.baseOffset;
// final length = controller.selection.extentOffset - index;
// String? videoUrl;
// if (kIsWeb) {
// assert(
// webVideoPickImpl != null,
// 'Please provide webVideoPickImpl for Web '
// 'in the options of this button',
// );
// videoUrl = await webVideoPickImpl!(onVideoPickCallback);
// } else if (isMobile()) {
// videoUrl = await _pickVideo(videoSource, onVideoPickCallback);
// } else {
// assert(filePickImpl != null, 'Desktop must provide filePickImpl');
// videoUrl =
// await _pickVideoDesktop(context, filePickImpl!,
// onVideoPickCallback);
// }
// if (videoUrl != null) {
// controller.replaceText(index, length, BlockEmbed.video(videoUrl),
// null);
// }
// }
// static Future<String?> _pickVideo(
// ImageSource source, OnVideoPickCallback onVideoPickCallback) async {
// final pickedFile = await ImagePicker().pickVideo(source: source);
// if (pickedFile == null) {
// return null;
// }
// return onVideoPickCallback(File(pickedFile.path));
// }
// static Future<String?> _pickVideoDesktop(
// BuildContext context,
// FilePickImpl filePickImpl,
// OnVideoPickCallback onVideoPickCallback) async {
// final filePath = await filePickImpl(context);
// if (filePath == null || filePath.isEmpty) return null;
// final file = File(filePath);
// return onVideoPickCallback(file);
// }
// }

@ -1,537 +0,0 @@
// // ignore_for_file: use_build_context_synchronously
// import 'dart:math' as math;
// import 'dart:ui';
// import 'package:flutter/foundation.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_quill/extensions.dart';
// import 'package:flutter_quill/flutter_quill.dart';
// import 'package:flutter_quill/translations.dart';
// import 'package:image_picker/image_picker.dart';
// import '../../../models/config/toolbar/buttons/media_button.dart';
// import '../../embed_types.dart';
// import '../utils/image_video_utils.dart';
// /// Widget which combines [ImageButton] and [VideButton] widgets. This widget
// /// has more customization and uses dialog similar to one which is used
// /// on [http://quilljs.com].
// class QuillToolbarMediaButton extends StatelessWidget {
// QuillToolbarMediaButton({
// required this.controller,
// this.options,
// super.key,
// }) : assert(options.type == QuillMediaType.image,
// 'Video selection is not supported yet');
// final QuillController controller;
// final QuillToolbarMediaButtonOptions options;
// double _iconSize(BuildContext context) {
// final baseFontSize = baseButtonExtraOptions(context).globalIconSize;
// final iconSize = options.iconSize;
// return iconSize ?? baseFontSize;
// }
// VoidCallback? _afterButtonPressed(BuildContext context) {
// return options.afterButtonPressed ??
// baseButtonExtraOptions(context).afterButtonPressed;
// }
// QuillIconTheme? _iconTheme(BuildContext context) {
// return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme;
// }
// QuillToolbarBaseButtonOptions baseButtonExtraOptions(
//BuildContext context) {
// return context.requireQuillToolbarBaseButtonOptions;
// }
// (IconData, String) get _defaultData {
// switch (options.type) {
// case QuillMediaType.image:
// return (Icons.perm_media, 'Photo media button');
// case QuillMediaType.video:
// throw UnsupportedError('The video is not supported yet.');
// }
// }
// IconData _iconData(BuildContext context) {
// return options.iconData ??
// baseButtonExtraOptions(context).iconData ??
// _defaultData.$1;
// }
// String _tooltip(BuildContext context) {
// return options.tooltip ??
// baseButtonExtraOptions(context).tooltip ??
// _defaultData.$2;
// // ('Camera'.i18n);
// }
// void _sharedOnPressed(BuildContext context) {
// _onPressedHandler(context);
// _afterButtonPressed(context);
// }
// @override
// Widget build(BuildContext context) {
// final tooltip = _tooltip(context);
// final iconSize = _iconSize(context);
// final iconData = _iconData(context);
// final childBuilder =
// options.childBuilder ?? baseButtonExtraOptions(context).childBuilder;
// final iconTheme = _iconTheme(context);
// if (childBuilder != null) {
// return childBuilder(
// QuillToolbarMediaButtonOptions(
// type: options.type,
// onMediaPickedCallback: options.onMediaPickedCallback,
// onImagePickCallback: options.onImagePickCallback,
// onVideoPickCallback: options.onVideoPickCallback,
// iconData: iconData,
// afterButtonPressed: _afterButtonPressed(context),
// autovalidateMode: options.autovalidateMode,
// childrenSpacing: options.childrenSpacing,
// dialogBarrierColor: options.dialogBarrierColor,
// dialogTheme: options.dialogTheme,
// filePickImpl: options.filePickImpl,
// fillColor: options.fillColor,
// galleryButtonText: options.galleryButtonText,
// iconTheme: iconTheme,
// iconSize: iconSize,
// iconButtonFactor: iconButtonFactor,
// hintText: options.hintText,
// labelText: options.labelText,
// submitButtonSize: options.submitButtonSize,
// linkButtonText: options.linkButtonText,
// mediaFilePicker: options.mediaFilePicker,
// submitButtonText: options.submitButtonText,
// validationMessage: options.validationMessage,
// webImagePickImpl: options.webImagePickImpl,
// webVideoPickImpl: options.webVideoPickImpl,
// tooltip: options.tooltip,
// ),
// QuillToolbarMediaButtonExtraOptions(
// context: context,
// controller: controller,
// onPressed: () => _sharedOnPressed(context),
// ),
// );
// }
// final theme = Theme.of(context);
// final iconColor =
// options.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color;
// final iconFillColor = options.iconTheme?.iconUnselectedFillColor ??
// options.fillColor ??
// theme.canvasColor;
// return QuillToolbarIconButton(
// icon: Icon(iconData, size: iconSize, color: iconColor),
// tooltip: tooltip,
// highlightElevation: 0,
// hoverElevation: 0,
// size: iconSize * iconButtonFactor,
// fillColor: iconFillColor,
// borderRadius: iconTheme?.borderRadius ?? 2,
// onPressed: () => _sharedOnPressed(context),
// );
// }
// Future<void> _onPressedHandler(BuildContext context) async {
// if (options.onMediaPickedCallback == null) {
// _inputLink(context);
// return;
// }
// final mediaSource = await showDialog<MediaPickSetting>(
// context: context,
// builder: (_) => MediaSourceSelectorDialog(
// dialogTheme: options.dialogTheme,
// galleryButtonText: options.galleryButtonText,
// linkButtonText: options.linkButtonText,
// ),
// );
// if (mediaSource == null) {
// return;
// }
// switch (mediaSource) {
// case MediaPickSetting.gallery:
// await _pickImage();
// break;
// case MediaPickSetting.link:
// _inputLink(context);
// break;
// case MediaPickSetting.camera:
// await ImageVideoUtils.handleImageButtonTap(
// context,
// controller,
// ImageSource.camera,
// options.onImagePickCallback,
// filePickImpl: options.filePickImpl,
// webImagePickImpl: options.webImagePickImpl,
// );
// break;
// case MediaPickSetting.video:
// await ImageVideoUtils.handleVideoButtonTap(
// context,
// controller,
// ImageSource.camera,
// options.onVideoPickCallback,
// filePickImpl: options.filePickImpl,
// webVideoPickImpl: options.webVideoPickImpl,
// );
// break;
// }
// }
// Future<void> _pickImage() async {
// if (!(kIsWeb || isMobile() || isDesktop())) {
// throw UnsupportedError(
// 'Unsupported target platform: ${defaultTargetPlatform.name}',
// );
// }
// final mediaFileUrl = await _pickMediaFileUrl();
// if (mediaFileUrl != null) {
// final index = controller.selection.baseOffset;
// final length = controller.selection.extentOffset - index;
// controller.replaceText(
// index,
// length,
// BlockEmbed.image(mediaFileUrl),
// null,
// );
// }
// }
// Future<MediaFileUrl?> _pickMediaFileUrl() async {
// final mediaFile = await options.mediaFilePicker?.call(options.type);
// return mediaFile != null
// ? options.onMediaPickedCallback?.call(mediaFile)
// : null;
// }
// void _inputLink(BuildContext context) {
// showDialog<String>(
// context: context,
// builder: (_) => MediaLinkDialog(
// dialogTheme: options.dialogTheme,
// labelText: options.labelText,
// hintText: options.hintText,
// buttonText: options.submitButtonText,
// buttonSize: options.submitButtonSize,
// childrenSpacing: options.childrenSpacing,
// autovalidateMode: options.autovalidateMode,
// validationMessage: options.validationMessage,
// ),
// ).then(_linkSubmitted);
// }
// void _linkSubmitted(String? value) {
// if (value != null && value.isNotEmpty) {
// final index = controller.selection.baseOffset;
// final length = controller.selection.extentOffset - index;
// final data = options.type.isImage
// ? BlockEmbed.image(value)
// : BlockEmbed.video(value);
// controller.replaceText(index, length, data, null);
// }
// }
// }
// /// Provides a dialog for input link to media resource.
// class MediaLinkDialog extends StatefulWidget {
// const MediaLinkDialog({
// super.key,
// this.link,
// this.dialogTheme,
// this.childrenSpacing = 16.0,
// this.labelText,
// this.hintText,
// this.buttonText,
// this.buttonSize,
// this.autovalidateMode = AutovalidateMode.disabled,
// this.validationMessage,
// }) : assert(childrenSpacing > 0);
// final String? link;
// final QuillDialogTheme? dialogTheme;
// /// The margin between child widgets in the dialog.
// final double childrenSpacing;
// /// The text of label in link add mode.
// final String? labelText;
// /// The hint text for link [TextField].
// final String? hintText;
// /// The text of the submit button.
// final String? buttonText;
// /// The size of dialog buttons.
// final Size? buttonSize;
// final AutovalidateMode autovalidateMode;
// final String? validationMessage;
// @override
// State<MediaLinkDialog> createState() => _MediaLinkDialogState();
// }
// class _MediaLinkDialogState extends State<MediaLinkDialog> {
// final _linkFocus = FocusNode();
// final _linkController = TextEditingController();
// @override
// void dispose() {
// _linkFocus.dispose();
// _linkController.dispose();
// super.dispose();
// }
// @override
// Widget build(BuildContext context) {
// final constraints = widget.dialogTheme?.linkDialogConstraints ??
// () {
// final size = MediaQuery.sizeOf(context);
// final maxWidth = kIsWeb ? size.width / 4 : size.width - 80;
// return BoxConstraints(maxWidth: maxWidth, maxHeight: 80);
// }();
// final buttonStyle = widget.buttonSize != null
// ? Theme.of(context)
// .elevatedButtonTheme
// .style
// ?.copyWith(
//fixedSize: MaterialStatePropertyAll(widget.buttonSize))
// : widget.dialogTheme?.buttonStyle;
// final isWrappable = widget.dialogTheme?.isWrappable ?? false;
// final children = [
// Text(widget.labelText ?? 'Enter media'.i18n),
// UtilityWidgets.maybeWidget(
// enabled: !isWrappable,
// wrapper: (child) => Expanded(
// child: child,
// ),
// child: Padding(
// padding: EdgeInsets.symmetric(horizontal: widget.childrenSpacing),
// child: TextFormField(
// controller: _linkController,
// focusNode: _linkFocus,
// style: widget.dialogTheme?.inputTextStyle,
// keyboardType: TextInputType.url,
// textInputAction: TextInputAction.done,
// decoration: InputDecoration(
// labelStyle: widget.dialogTheme?.labelTextStyle,
// hintText: widget.hintText,
// ),
// autofocus: true,
// autovalidateMode: widget.autovalidateMode,
// validator: _validateLink,
// onChanged: _linkChanged,
// ),
// ),
// ),
// ElevatedButton(
// onPressed: _canPress() ? _submitLink : null,
// style: buttonStyle,
// child: Text(widget.buttonText ?? 'Ok'.i18n),
// ),
// ];
// return Dialog(
// backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
// shape: widget.dialogTheme?.shape ??
// DialogTheme.of(context).shape ??
// RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
// child: ConstrainedBox(
// constraints: constraints,
// child: Padding(
// padding:
// widget.dialogTheme?.linkDialogPadding ?? const
// EdgeInsets.all(16),
// child: Form(
// child: isWrappable
// ? Wrap(
// alignment: WrapAlignment.center,
// crossAxisAlignment: WrapCrossAlignment.center,
// runSpacing: widget.dialogTheme?.runSpacing ?? 0.0,
// children: children,
// )
// : Row(
// children: children,
// ),
// ),
// ),
// ),
// );
// }
// bool _canPress() => _validateLink(_linkController.text) == null;
// void _linkChanged(String value) {
// setState(() {
// _linkController.text = value;
// });
// }
// void _submitLink() => Navigator.pop(context, _linkController.text);
// String? _validateLink(String? value) {
// if ((value?.isEmpty ?? false) ||
// !AutoFormatMultipleLinksRule.oneLineLinkRegExp.hasMatch(value!)) {
// return widget.validationMessage ?? 'That is not a valid URL';
// }
// return null;
// }
// }
// /// Media souce selector.
// class MediaSourceSelectorDialog extends StatelessWidget {
// const MediaSourceSelectorDialog({
// super.key,
// this.dialogTheme,
// this.galleryButtonText,
// this.linkButtonText,
// });
// final QuillDialogTheme? dialogTheme;
// /// The text of the gallery button [MediaSourceSelectorDialog].
// final String? galleryButtonText;
// /// The text of the link button [MediaSourceSelectorDialog].
// final String? linkButtonText;
// @override
// Widget build(BuildContext context) {
// final constraints = dialogTheme?.mediaSelectorDialogConstraints ??
// () {
// final size = MediaQuery.sizeOf(context);
// double maxWidth, maxHeight;
// if (kIsWeb) {
// maxWidth = size.width / 7;
// maxHeight = size.height / 7;
// } else {
// maxWidth = size.width - 80;
// maxHeight = maxWidth / 2;
// }
// return BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight);
// }();
// final shape = dialogTheme?.shape ??
// DialogTheme.of(context).shape ??
// RoundedRectangleBorder(borderRadius: BorderRadius.circular(4));
// return Dialog(
// backgroundColor: dialogTheme?.dialogBackgroundColor,
// shape: shape,
// child: ConstrainedBox(
// constraints: constraints,
// child: Padding(
// padding: dialogTheme?.mediaSelectorDialogPadding ??
// const EdgeInsets.all(16),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Expanded(
// child: TextButtonWithIcon(
// icon: Icons.collections,
// label: galleryButtonText ?? 'Gallery'.i18n,
// onPressed: () =>
// Navigator.pop(context, MediaPickSetting.gallery),
// ),
// ),
// const SizedBox(width: 10),
// Expanded(
// child: TextButtonWithIcon(
// icon: Icons.link,
// label: linkButtonText ?? 'Link'.i18n,
// onPressed: () =>
// Navigator.pop(context, MediaPickSetting.link),
// ),
// )
// ],
// ),
// ),
// ),
// );
// }
// }
// class TextButtonWithIcon extends StatelessWidget {
// const TextButtonWithIcon({
// required this.label,
// required this.icon,
// required this.onPressed,
// this.textStyle,
// super.key,
// });
// final String label;
// final IconData icon;
// final VoidCallback onPressed;
// final TextStyle? textStyle;
// @override
// Widget build(BuildContext context) {
// final theme = Theme.of(context);
// final scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
// final gap = scale <= 1 ? 8.0 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
// final buttonStyle = TextButtonTheme.of(context).style;
// final shape = buttonStyle?.shape?.resolve({}) ??
// const RoundedRectangleBorder(
// borderRadius: BorderRadius.all(
// Radius.circular(4),
// ),
// );
// return Material(
// shape: shape,
// textStyle: textStyle ??
// theme.textButtonTheme.style?.textStyle?.resolve({}) ??
// theme.textTheme.labelLarge,
// elevation: buttonStyle?.elevation?.resolve({}) ?? 0,
// child: InkWell(
// customBorder: shape,
// onTap: onPressed,
// child: Padding(
// padding: const EdgeInsets.all(16),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: <Widget>[
// Icon(icon),
// SizedBox(height: gap),
// Flexible(child: Text(label)),
// ],
// ),
// ),
// ),
// );
// }
// }
// /// Default file picker.
// // Future<QuillFile?> _defaultMediaPicker(QuillMediaType mediaType) async {
// // final pickedFile = mediaType.isImage
// // ? await ImagePicker().pickImage(source: ImageSource.gallery)
// // : await ImagePicker().pickVideo(source: ImageSource.gallery);
// // if (pickedFile != null) {
// // return QuillFile(
// // name: pickedFile.name,
// // path: pickedFile.path,
// // bytes: await pickedFile.readAsBytes(),
// // );
// // }
// // return null;
// // }

@ -1,19 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_quill/flutter_quill.dart';
class QuillEditorUnknownEmbedBuilder extends EmbedBuilder {
@override
Widget build(
BuildContext context,
QuillController controller,
Embed node,
bool readOnly,
bool inline,
TextStyle textStyle,
) {
return const Text('Unknown embed builder');
}
@override
String get key => 'unknown';
}

@ -1,50 +1,67 @@
library flutter_quill_extensions; library flutter_quill_extensions;
// ignore: implementation_imports
import 'package:flutter_quill/src/editor/spellchecker/spellchecker_service_provider.dart';
// ignore: implementation_imports // ignore: implementation_imports
import 'package:flutter_quill/src/editor_toolbar_controller_shared/clipboard/clipboard_service_provider.dart'; import 'package:flutter_quill/src/editor_toolbar_controller_shared/clipboard/clipboard_service_provider.dart';
import 'package:meta/meta.dart' show immutable; import 'package:meta/meta.dart' show immutable;
import 'services/clipboard/super_clipboard_service.dart'; import 'src/editor/spell_checker/simple_spell_checker_service.dart';
import 'src/editor_toolbar_controller_shared/clipboard/super_clipboard_service.dart';
export 'embeds/embed_types.dart';
export 'embeds/formula/toolbar/formula_button.dart';
export 'embeds/image/editor/image_embed.dart';
export 'embeds/image/editor/image_embed_types.dart';
export 'embeds/image/editor/image_web_embed.dart';
export 'embeds/image/toolbar/image_button.dart';
export 'embeds/others/camera_button/camera_button.dart';
export 'embeds/others/media_button/media_button.dart';
export 'embeds/table/editor/table_cell_embed.dart';
export 'embeds/table/editor/table_embed.dart';
export 'embeds/table/editor/table_models.dart';
export 'embeds/table/toolbar/table_button.dart';
export 'embeds/unknown/editor/unknown_embed.dart';
export 'embeds/video/editor/video_embed.dart';
export 'embeds/video/editor/video_web_embed.dart';
export 'embeds/video/toolbar/video_button.dart';
export 'embeds/video/video.dart';
export 'extensions/controller_ext.dart';
export 'flutter_quill_embeds.dart';
export 'models/config/camera/camera_configurations.dart';
export 'models/config/formula/formula_configurations.dart';
export 'models/config/image/editor/image_configurations.dart';
export 'models/config/image/editor/image_web_configurations.dart';
export 'models/config/image/toolbar/image_configurations.dart';
export 'models/config/media/media_button_configurations.dart';
export 'models/config/shared_configurations.dart';
export 'models/config/table/table_configurations.dart';
export 'models/config/video/editor/video_configurations.dart';
export 'models/config/video/editor/video_web_configurations.dart';
export 'models/config/video/toolbar/video_configurations.dart';
export 'utils/utils.dart';
// TODO: Refactor flutter_quill_extensions to match the structure of flutter_quill export 'src/common/extensions/controller_ext.dart';
// Also avoid exposing all APIs as public. Use `src` as directory name export 'src/common/utils/utils.dart';
export 'src/editor/image/image_embed.dart';
export 'src/editor/image/image_embed_types.dart';
export 'src/editor/image/image_web_embed.dart';
export 'src/editor/image/models/image_configurations.dart';
export 'src/editor/image/models/image_web_configurations.dart';
export 'src/editor/spell_checker/simple_spell_checker_service.dart';
export 'src/editor/table/table_cell_embed.dart';
export 'src/editor/table/table_embed.dart';
export 'src/editor/table/table_models.dart';
export 'src/editor/video/models/video_configurations.dart';
export 'src/editor/video/models/video_web_configurations.dart';
export 'src/editor/video/models/youtube_video_support_mode.dart';
export 'src/editor/video/video_embed.dart';
export 'src/editor/video/video_web_embed.dart';
export 'src/editor_toolbar_shared/shared_configurations.dart';
export 'src/flutter_quill_embeds.dart';
export 'src/toolbar/camera/camera_button.dart';
export 'src/toolbar/camera/models/camera_configurations.dart';
export 'src/toolbar/formula/formula_button.dart';
export 'src/toolbar/formula/models/formula_configurations.dart';
export 'src/toolbar/image/image_button.dart';
export 'src/toolbar/image/models/image_configurations.dart';
export 'src/toolbar/table/models/table_configurations.dart';
export 'src/toolbar/table/table_button.dart';
export 'src/toolbar/video/models/video.dart';
export 'src/toolbar/video/models/video_configurations.dart';
export 'src/toolbar/video/video_button.dart';
@immutable @immutable
class FlutterQuillExtensions { class FlutterQuillExtensions {
const FlutterQuillExtensions._(); const FlutterQuillExtensions._();
/// override the default implementation of [SpellCheckerServiceProvider]
/// to allow a `flutter quill` support a better check spelling
///
/// # !WARNING
/// To avoid memory leaks, ensure to use [dispose()] method to
/// close stream controllers that used by this custom implementation
/// when them no longer needed
///
/// Example:
///
///```dart
///// set partial true if you only need to close the controllers
///SpellCheckerServiceProvider.dispose(onlyPartial: false);
///```
static void useSpellCheckerService(String language) {
SpellCheckerServiceProvider.setNewCheckerService(
SimpleSpellCheckerService(language: language));
}
/// Override default implementation of [ClipboardServiceProvider.instance] /// Override default implementation of [ClipboardServiceProvider.instance]
/// to allow `flutter_quill` package to use `super_clipboard` plugin /// to allow `flutter_quill` package to use `super_clipboard` plugin
/// to support rich text features, gif and images. /// to support rich text features, gif and images.

@ -1,71 +0,0 @@
import 'package:flutter/widgets.dart' show AutovalidateMode;
import 'package:flutter/widgets.dart' show Color, Size;
import 'package:flutter_quill/flutter_quill.dart';
import '../../../embeds/embed_types.dart';
class QuillToolbarMediaButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions {
const QuillToolbarMediaButtonExtraOptions({
required super.controller,
required super.context,
required super.onPressed,
});
}
class QuillToolbarMediaButtonOptions extends QuillToolbarBaseButtonOptions<
QuillToolbarMediaButtonOptions, QuillToolbarMediaButtonExtraOptions> {
const QuillToolbarMediaButtonOptions({
required this.type,
required this.onMediaPickedCallback,
// required this.onVideoPickCallback,
this.dialogBarrierColor,
this.mediaFilePicker,
this.childrenSpacing = 16.0,
this.autovalidateMode = AutovalidateMode.disabled,
super.iconSize,
this.dialogTheme,
this.labelText,
this.hintText,
this.submitButtonText,
this.submitButtonSize,
this.galleryButtonText,
this.linkButtonText,
this.validationMessage,
super.iconData,
super.afterButtonPressed,
super.tooltip,
super.iconTheme,
super.childBuilder,
});
final QuillMediaType type;
final QuillDialogTheme? dialogTheme;
final MediaFilePicker? mediaFilePicker;
final MediaPickedCallback? onMediaPickedCallback;
final Color? dialogBarrierColor;
/// The margin between child widgets in the dialog.
final double childrenSpacing;
/// The text of label in link add mode.
final String? labelText;
/// The hint text for link [TextField].
final String? hintText;
/// The text of the submit button.
final String? submitButtonText;
/// The size of dialog buttons.
final Size? submitButtonSize;
/// The text of the gallery button [MediaSourceSelectorDialog].
final String? galleryButtonText;
/// The text of the link button [MediaSourceSelectorDialog].
final String? linkButtonText;
final AutovalidateMode autovalidateMode;
final String? validationMessage;
}

@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart' show QuillDialogTheme;
import 'package:flutter_quill/translations.dart';
import 'utils/patterns.dart';
enum LinkType {
video,
image,
}
class TypeLinkDialog extends StatefulWidget {
const TypeLinkDialog({
required this.linkType,
this.dialogTheme,
this.link,
this.linkRegExp,
super.key,
});
final QuillDialogTheme? dialogTheme;
final String? link;
final RegExp? linkRegExp;
final LinkType linkType;
@override
TypeLinkDialogState createState() => TypeLinkDialogState();
}
class TypeLinkDialogState extends State<TypeLinkDialog> {
late String _link;
late TextEditingController _controller;
RegExp? _linkRegExp;
@override
void initState() {
super.initState();
_link = widget.link ?? '';
_controller = TextEditingController(text: _link);
_linkRegExp = widget.linkRegExp;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
content: TextField(
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
maxLines: null,
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
labelText: context.loc.pasteLink,
hintText: widget.linkType == LinkType.image
? context.loc.pleaseEnterAValidImageURL
: context.loc.pleaseEnterAValidVideoURL,
labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
),
autofocus: true,
onChanged: _linkChanged,
controller: _controller,
onEditingComplete: () {
if (!_canPress()) {
return;
}
_applyLink();
},
),
actions: [
TextButton(
onPressed: _canPress() ? _applyLink : null,
child: Text(
context.loc.ok,
style: widget.dialogTheme?.labelTextStyle,
),
),
],
);
}
void _linkChanged(String value) {
setState(() {
_link = value;
});
}
void _applyLink() {
Navigator.pop(context, _link.trim());
}
RegExp get linkRegExp {
final customRegExp = _linkRegExp;
if (customRegExp != null) {
return customRegExp;
}
switch (widget.linkType) {
case LinkType.video:
if (youtubeRegExp.hasMatch(_link)) {
return youtubeRegExp;
}
return videoRegExp;
case LinkType.image:
return imageRegExp;
}
}
bool _canPress() {
if (_link.isEmpty) {
return false;
}
if (widget.linkType == LinkType.image) {}
return _link.isNotEmpty && linkRegExp.hasMatch(_link);
}
}

@ -4,8 +4,8 @@ import 'package:cross_file/cross_file.dart';
import 'package:flutter/foundation.dart' show Uint8List, immutable; import 'package:flutter/foundation.dart' show Uint8List, immutable;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../embeds/widgets/image.dart'; import '../../editor/image/widgets/image.dart';
import '../services/image_saver/s_image_saver.dart'; import '../../editor_toolbar_shared/image_saver/s_image_saver.dart';
import 'patterns.dart'; import 'patterns.dart';
bool isBase64(String str) { bool isBase64(String str) {

@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart' hide OptionalSize; import 'package:flutter_quill/flutter_quill.dart' hide OptionalSize;
import 'package:flutter_quill/translations.dart'; import 'package:flutter_quill/translations.dart';
import '../../../models/config/image/editor/image_configurations.dart'; import '../../common/utils/element_utils/element_utils.dart';
import '../../../models/config/shared_configurations.dart'; import '../../editor_toolbar_shared/shared_configurations.dart';
import '../../../utils/element_utils/element_utils.dart';
import '../../widgets/image.dart';
import 'image_menu.dart'; import 'image_menu.dart';
import 'models/image_configurations.dart';
import 'widgets/image.dart';
class QuillEditorImageEmbedBuilder extends EmbedBuilder { class QuillEditorImageEmbedBuilder extends EmbedBuilder {
QuillEditorImageEmbedBuilder({ QuillEditorImageEmbedBuilder({

@ -4,8 +4,8 @@ import 'package:flutter/widgets.dart' show BuildContext;
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable; import 'package:meta/meta.dart' show immutable;
import '../../../extensions/controller_ext.dart'; import '../../common/extensions/controller_ext.dart';
import '../../../services/image_picker/s_image_picker.dart'; import '../../editor_toolbar_shared/image_picker/s_image_picker.dart';
/// When request picking an image, for example when the image button toolbar /// When request picking an image, for example when the image button toolbar
/// clicked, it should be null in case the user didn't choose any image or /// clicked, it should be null in case the user didn't choose any image or

@ -7,14 +7,14 @@ import 'package:flutter_quill/flutter_quill.dart'
import 'package:flutter_quill/translations.dart'; import 'package:flutter_quill/translations.dart';
import 'package:super_clipboard/super_clipboard.dart'; import 'package:super_clipboard/super_clipboard.dart';
import '../../../models/config/image/editor/image_configurations.dart'; import '../../common/utils/element_utils/element_utils.dart';
import '../../../models/config/shared_configurations.dart'; import '../../common/utils/string.dart';
import '../../../services/image_saver/s_image_saver.dart'; import '../../common/utils/utils.dart';
import '../../../utils/element_utils/element_utils.dart'; import '../../editor_toolbar_shared/image_saver/s_image_saver.dart';
import '../../../utils/string.dart'; import '../../editor_toolbar_shared/shared_configurations.dart';
import '../../../utils/utils.dart'; import 'models/image_configurations.dart';
import '../../widgets/image.dart' show ImageTapWrapper, getImageStyleString; import 'widgets/image.dart' show ImageTapWrapper, getImageStyleString;
import '../../widgets/image_resizer.dart' show ImageResizer; import 'widgets/image_resizer.dart' show ImageResizer;
class ImageOptionsMenu extends StatelessWidget { class ImageOptionsMenu extends StatelessWidget {
const ImageOptionsMenu({ const ImageOptionsMenu({

@ -3,12 +3,12 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:universal_html/html.dart' as html; import 'package:universal_html/html.dart' as html;
import '../../../models/config/image/editor/image_web_configurations.dart'; import '../../common/utils/dart_ui/dart_ui_fake.dart'
import '../../../utils/dart_ui/dart_ui_fake.dart' if (dart.library.js_interop) '../../common/utils/dart_ui/dart_ui_real.dart'
if (dart.library.js_interop) '../../../utils/dart_ui/dart_ui_real.dart'
as ui; as ui;
import '../../../utils/element_utils/element_web_utils.dart'; import '../../common/utils/element_utils/element_web_utils.dart';
import '../../../utils/utils.dart'; import '../../common/utils/utils.dart';
import 'models/image_web_configurations.dart';
class QuillEditorWebImageEmbedBuilder extends EmbedBuilder { class QuillEditorWebImageEmbedBuilder extends EmbedBuilder {
const QuillEditorWebImageEmbedBuilder({ const QuillEditorWebImageEmbedBuilder({

@ -3,7 +3,7 @@ import 'dart:io' show File;
import 'package:flutter_quill/extensions.dart'; import 'package:flutter_quill/extensions.dart';
import 'package:meta/meta.dart' show immutable; import 'package:meta/meta.dart' show immutable;
import '../../../../embeds/image/editor/image_embed_types.dart'; import '../image_embed_types.dart';
/// [QuillEditorImageEmbedConfigurations] for desktop, mobile and /// [QuillEditorImageEmbedConfigurations] for desktop, mobile and
/// other platforms /// other platforms

@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import '../../models/config/image/editor/image_configurations.dart'; import '../../../common/utils/utils.dart';
import '../../utils/utils.dart'; import '../image_embed_types.dart';
import '../image/editor/image_embed_types.dart'; import '../models/image_configurations.dart';
const List<String> imageFileExtensions = [ const List<String> imageFileExtensions = [
'.jpeg', '.jpeg',

@ -0,0 +1,65 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:simple_spell_checker/simple_spell_checker.dart';
/// SimpleSpellChecker is a simple spell checker for get
/// all words divide on different objects if them are wrong or not
class SimpleSpellCheckerService
extends SpellCheckerService<LanguageIdentifier> {
SimpleSpellCheckerService({required super.language})
: checker = SimpleSpellChecker(
language: language,
safeDictionaryLoad: true,
);
/// [SimpleSpellChecker] comes from the package [simple_spell_checker]
/// that give us all necessary methods for get our spans with highlighting
/// where needed
final SimpleSpellChecker checker;
@override
List<TextSpan>? checkSpelling(
String text, {
LongPressGestureRecognizer Function(String word)?
customLongPressRecognizerOnWrongSpan,
}) {
return checker.check(
text,
customLongPressRecognizerOnWrongSpan:
customLongPressRecognizerOnWrongSpan,
);
}
@override
void toggleChecker() => checker.toggleChecker();
@override
bool isServiceActive() => checker.isCheckerActive();
@override
void dispose({bool onlyPartial = false}) {
if (onlyPartial) {
checker.disposeControllers();
return;
}
checker.dispose();
}
@override
void addCustomLanguage({required languageIdentifier}) {
checker
..registerLanguage(languageIdentifier.language)
..addCustomLanguage(languageIdentifier);
}
@override
void setNewLanguageState({required String language}) {
checker.setNewLanguageToState(language);
}
@override
void updateCustomLanguageIfExist({required languageIdentifier}) {
checker.updateCustomLanguageIfExist(languageIdentifier);
}
}

@ -3,7 +3,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/quill_delta.dart'; import 'package:flutter_quill/quill_delta.dart';
import '../../../utils/quill_table_utils.dart'; import '../../common/utils/quill_table_utils.dart';
import 'table_cell_embed.dart'; import 'table_cell_embed.dart';
import 'table_models.dart'; import 'table_models.dart';

@ -2,11 +2,11 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import '../../../models/config/video/editor/video_configurations.dart'; import '../../common/utils/element_utils/element_utils.dart';
import '../../../utils/element_utils/element_utils.dart'; import '../../common/utils/utils.dart';
import '../../../utils/utils.dart'; import 'models/video_configurations.dart';
import '../../widgets/video_app.dart'; import 'widgets/video_app.dart';
import '../../widgets/youtube_video_app.dart'; import 'widgets/youtube_video_app.dart';
class QuillEditorVideoEmbedBuilder extends EmbedBuilder { class QuillEditorVideoEmbedBuilder extends EmbedBuilder {
const QuillEditorVideoEmbedBuilder({ const QuillEditorVideoEmbedBuilder({

@ -4,12 +4,12 @@ import 'package:universal_html/html.dart' as html;
import 'package:youtube_player_flutter/youtube_player_flutter.dart' import 'package:youtube_player_flutter/youtube_player_flutter.dart'
show YoutubePlayer; show YoutubePlayer;
import '../../../models/config/video/editor/video_web_configurations.dart'; import '../../common/utils/dart_ui/dart_ui_fake.dart'
import '../../../utils/dart_ui/dart_ui_fake.dart' if (dart.library.js_interop) '../../common/utils/dart_ui/dart_ui_real.dart'
if (dart.library.js_interop) '../../../utils/dart_ui/dart_ui_real.dart'
as ui; as ui;
import '../../../utils/element_utils/element_web_utils.dart'; import '../../common/utils/element_utils/element_web_utils.dart';
import '../../../utils/utils.dart'; import '../../common/utils/utils.dart';
import 'models/video_web_configurations.dart';
class QuillEditorWebVideoEmbedBuilder extends EmbedBuilder { class QuillEditorWebVideoEmbedBuilder extends EmbedBuilder {
const QuillEditorWebVideoEmbedBuilder({ const QuillEditorWebVideoEmbedBuilder({

@ -6,7 +6,7 @@ import 'package:flutter_quill/flutter_quill.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import '../../flutter_quill_extensions.dart'; import '../../../../flutter_quill_extensions.dart';
/// Widget for playing back video /// Widget for playing back video
/// Refer to https://github.com/flutter/plugins/tree/master/packages/video_player/video_player /// Refer to https://github.com/flutter/plugins/tree/master/packages/video_player/video_player

@ -5,7 +5,7 @@ import 'package:url_launcher/url_launcher_string.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart'; import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import '../../models/config/video/editor/youtube_video_support_mode.dart'; import '../models/youtube_video_support_mode.dart';
import 'video_app.dart'; import 'video_app.dart';
class YoutubeVideoApp extends StatefulWidget { class YoutubeVideoApp extends StatefulWidget {

@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart' show BuildContext;
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable; import 'package:meta/meta.dart' show immutable;
import '../../services/image_picker/s_image_picker.dart'; import 'image_picker/s_image_picker.dart';
import '../../services/image_saver/s_image_saver.dart'; import 'image_saver/s_image_saver.dart';
/// Configurations for Flutter Editor Extensions /// Configurations for Flutter Editor Extensions
/// shared between toolbar and editor /// shared between toolbar and editor

@ -2,23 +2,21 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_quill/flutter_quill.dart' as fq; import 'package:flutter_quill/flutter_quill.dart' as fq;
import 'package:meta/meta.dart' show immutable; import 'package:meta/meta.dart' show immutable;
import 'embeds/image/editor/image_embed.dart'; import 'editor/image/image_embed.dart';
import 'embeds/image/editor/image_web_embed.dart'; import 'editor/image/models/image_configurations.dart';
import 'embeds/image/toolbar/image_button.dart'; import 'editor/table/table_embed.dart';
import 'embeds/others/camera_button/camera_button.dart'; import 'editor/video/models/video_configurations.dart';
import 'embeds/table/editor/table_embed.dart'; import 'editor/video/models/video_web_configurations.dart';
import 'embeds/table/toolbar/table_button.dart'; import 'editor/video/video_embed.dart';
import 'embeds/video/editor/video_embed.dart'; import 'editor/video/video_web_embed.dart';
import 'embeds/video/editor/video_web_embed.dart'; import 'toolbar/camera/camera_button.dart';
import 'embeds/video/toolbar/video_button.dart'; import 'toolbar/camera/models/camera_configurations.dart';
import 'models/config/camera/camera_configurations.dart'; import 'toolbar/image/image_button.dart';
import 'models/config/image/editor/image_configurations.dart'; import 'toolbar/image/models/image_configurations.dart';
import 'models/config/image/toolbar/image_configurations.dart'; import 'toolbar/table/models/table_configurations.dart';
import 'models/config/media/media_button_configurations.dart'; import 'toolbar/table/table_button.dart';
import 'models/config/table/table_configurations.dart'; import 'toolbar/video/models/video_configurations.dart';
import 'models/config/video/editor/video_configurations.dart'; import 'toolbar/video/video_button.dart';
import 'models/config/video/editor/video_web_configurations.dart';
import 'models/config/video/toolbar/video_configurations.dart';
@immutable @immutable
class FlutterQuillEmbeds { class FlutterQuillEmbeds {
@ -122,10 +120,6 @@ class FlutterQuillEmbeds {
const QuillToolbarVideoButtonOptions(), const QuillToolbarVideoButtonOptions(),
QuillToolbarCameraButtonOptions? cameraButtonOptions, QuillToolbarCameraButtonOptions? cameraButtonOptions,
QuillToolbarTableButtonOptions? tableButtonOptions, QuillToolbarTableButtonOptions? tableButtonOptions,
@Deprecated(
'Media button has been removed, the value of this parameter will be ignored',
)
QuillToolbarMediaButtonOptions? mediaButtonOptions,
}) => }) =>
[ [
if (imageButtonOptions != null) if (imageButtonOptions != null)

@ -10,10 +10,10 @@ import 'package:flutter_quill/flutter_quill.dart'
kDefaultIconButtonFactor; kDefaultIconButtonFactor;
import 'package:flutter_quill/translations.dart'; import 'package:flutter_quill/translations.dart';
import '../../../models/config/camera/camera_configurations.dart'; import '../../editor_toolbar_shared/image_picker/image_options.dart';
import '../../../models/config/shared_configurations.dart'; import '../../editor_toolbar_shared/shared_configurations.dart';
import '../../../services/image_picker/image_options.dart';
import 'camera_types.dart'; import 'camera_types.dart';
import 'models/camera_configurations.dart';
import 'select_camera_action.dart'; import 'select_camera_action.dart';
class QuillToolbarCameraButton extends StatelessWidget { class QuillToolbarCameraButton extends StatelessWidget {

@ -1,8 +1,8 @@
import 'package:flutter/widgets.dart' show BuildContext; import 'package:flutter/widgets.dart' show BuildContext;
import 'package:meta/meta.dart' show immutable; import 'package:meta/meta.dart' show immutable;
import '../../image/editor/image_embed_types.dart'; import '../../editor/image/image_embed_types.dart';
import '../../video/video.dart'; import '../video/models/video.dart';
enum CameraAction { enum CameraAction {
video, video,

@ -1,6 +1,6 @@
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import '../../../embeds/others/camera_button/camera_types.dart'; import '../camera_types.dart';
class QuillToolbarCameraButtonExtraOptions class QuillToolbarCameraButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions { extends QuillToolbarBaseButtonExtraOptions {

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import '../../../models/config/formula/formula_configurations.dart'; import 'models/formula_configurations.dart';
class QuillToolbarFormulaButton extends StatelessWidget { class QuillToolbarFormulaButton extends StatelessWidget {
const QuillToolbarFormulaButton({ const QuillToolbarFormulaButton({

@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/translations.dart'; import 'package:flutter_quill/translations.dart';
import '../../../models/config/image/toolbar/image_configurations.dart'; import '../../common/image_video_utils.dart';
import '../../../models/config/shared_configurations.dart'; import '../../editor/image/image_embed_types.dart';
import '../../../services/image_picker/image_picker.dart'; import '../../editor_toolbar_shared/image_picker/image_picker.dart';
import '../../others/image_video_utils.dart'; import '../../editor_toolbar_shared/shared_configurations.dart';
import '../editor/image_embed_types.dart'; import 'models/image_configurations.dart';
import 'select_image_source.dart'; import 'select_image_source.dart';
class QuillToolbarImageButton extends StatelessWidget { class QuillToolbarImageButton extends StatelessWidget {

@ -1,7 +1,7 @@
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable; import 'package:meta/meta.dart' show immutable;
import '../../../../embeds/image/editor/image_embed_types.dart'; import '../../../editor/image/image_embed_types.dart';
class QuillToolbarImageButtonExtraOptions class QuillToolbarImageButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions { extends QuillToolbarBaseButtonExtraOptions {

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart' show isDesktop; import 'package:flutter_quill/extensions.dart' show isDesktop;
import 'package:flutter_quill/translations.dart'; import 'package:flutter_quill/translations.dart';
import '../editor/image_embed_types.dart'; import '../../editor/image/image_embed_types.dart';
class SelectImageSourceDialog extends StatelessWidget { class SelectImageSourceDialog extends StatelessWidget {
const SelectImageSourceDialog({super.key}); const SelectImageSourceDialog({super.key});

@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/translations.dart'; import 'package:flutter_quill/translations.dart';
import '../../../models/config/table/table_configurations.dart';
import '../../../utils/quill_table_utils.dart'; import '../../common/utils/quill_table_utils.dart';
import 'models/table_configurations.dart';
class QuillToolbarTableButton extends StatelessWidget { class QuillToolbarTableButton extends StatelessWidget {
const QuillToolbarTableButton({ const QuillToolbarTableButton({

@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart' show BuildContext;
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable; import 'package:meta/meta.dart' show immutable;
import '../../extensions/controller_ext.dart'; import '../../../common/extensions/controller_ext.dart';
import '../../services/image_picker/s_image_picker.dart'; import '../../../editor_toolbar_shared/image_picker/s_image_picker.dart';
/// When request picking an video, for example when the video button toolbar /// When request picking an video, for example when the video button toolbar
/// clicked, it should be null in case the user didn't choose any video or /// clicked, it should be null in case the user didn't choose any video or

@ -1,6 +1,6 @@
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import '../../../../embeds/video/video.dart'; import 'video.dart';
class QuillToolbarVideoButtonExtraOptions class QuillToolbarVideoButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions { extends QuillToolbarBaseButtonExtraOptions {

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart' show isDesktop; import 'package:flutter_quill/extensions.dart' show isDesktop;
import 'package:flutter_quill/translations.dart'; import 'package:flutter_quill/translations.dart';
import '../video.dart'; import 'models/video.dart';
class SelectVideoSourceDialog extends StatelessWidget { class SelectVideoSourceDialog extends StatelessWidget {
const SelectVideoSourceDialog({super.key}); const SelectVideoSourceDialog({super.key});

@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/translations.dart'; import 'package:flutter_quill/translations.dart';
import '../../../models/config/shared_configurations.dart'; import '../../common/image_video_utils.dart';
import '../../../models/config/video/toolbar/video_configurations.dart'; import '../../editor_toolbar_shared/image_picker/image_options.dart';
import '../../../services/image_picker/image_options.dart'; import '../../editor_toolbar_shared/shared_configurations.dart';
import '../../others/image_video_utils.dart'; import 'models/video.dart';
import '../video.dart'; import 'models/video_configurations.dart';
import 'select_video_source.dart'; import 'select_video_source.dart';
// TODO: Add custom callback to validate the video link input // TODO: Add custom callback to validate the video link input

@ -1,6 +1,6 @@
name: flutter_quill_extensions name: flutter_quill_extensions
description: Embed extensions for flutter_quill including image, video, formula and etc. description: Embed extensions for flutter_quill including image, video, formula and etc.
version: 10.1.10 version: 10.4.1
homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/ homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/
repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/ repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/
issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/
@ -35,12 +35,13 @@ dependencies:
universal_html: ^2.2.4 universal_html: ^2.2.4
cross_file: ^0.3.3+6 cross_file: ^0.3.3+6
flutter_quill: ^10.0.0 flutter_quill: ^10.3.0
photo_view: ^0.15.0 photo_view: ^0.15.0
youtube_explode_dart: ^2.2.1 youtube_explode_dart: ^2.2.1
# Plugins # Plugins
video_player: ^2.8.1 video_player: ^2.8.1
simple_spell_checker: ^1.1.6
youtube_player_flutter: ^9.0.1 youtube_player_flutter: ^9.0.1
url_launcher: ^6.2.1 url_launcher: ^6.2.1
super_clipboard: ^0.8.15 super_clipboard: ^0.8.15

@ -4,6 +4,61 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## 10.4.1
* Chore: improve Spell checker API to the example by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2133
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.4.0...v10.4.1
## 10.4.0
* Copy TapAndPanGestureRecognizer from TextField by @demoYang in https://github.com/singerdmx/flutter-quill/pull/2128
* enhance stringToColor with a custom defined palette from `DefaultStyles` by @vishna in https://github.com/singerdmx/flutter-quill/pull/2095
* Feat: include spell checker for example app by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2127
## New Contributors
* @vishna made their first contribution in https://github.com/singerdmx/flutter-quill/pull/2095
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.3.3...v10.4.0
## 10.3.2
* Fix: Loss of style when backspace by @AtlasAutocode in https://github.com/singerdmx/flutter-quill/pull/2125
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.3.1...v10.3.2
## 10.3.1
* Chore: Move spellchecker service to extensions by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2120
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.3.0...v10.3.1
## 10.3.0
* Feat: Spellchecker for Flutter Quill by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2118
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.2.1...v10.3.0
## 10.2.1
* Fix: context menu is visible even when selection is collapsed by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2116
* Fix: unsafe operation while getting overlayEntry in text_selection by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2117
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.2.0...v10.2.1
## 10.2.0
* refactor!: restructure project into modular architecture for flutter_quill_extensions by @EchoEllet in https://github.com/singerdmx/flutter-quill/pull/2106
* Fix: Link selection and editing by @AtlasAutocode in https://github.com/singerdmx/flutter-quill/pull/2114
**Full Changelog**: https://github.com/singerdmx/flutter-quill/compare/v10.1.10...v10.2.0
## 10.1.10 ## 10.1.10
* Fix(example): image_cropper outdated version by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2100 * Fix(example): image_cropper outdated version by @CatHood0 in https://github.com/singerdmx/flutter-quill/pull/2100

@ -1,6 +1,6 @@
name: flutter_quill_test name: flutter_quill_test
description: Test utilities for flutter_quill which includes methods to simplify interacting with the editor in test cases. description: Test utilities for flutter_quill which includes methods to simplify interacting with the editor in test cases.
version: 10.1.10 version: 10.4.1
homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_test/ homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_test/
repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_test/ repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_test/
issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/

@ -24,6 +24,8 @@ export 'src/editor/raw_editor/config/raw_editor_configurations.dart';
export 'src/editor/raw_editor/quill_single_child_scroll_view.dart'; export 'src/editor/raw_editor/quill_single_child_scroll_view.dart';
export 'src/editor/raw_editor/raw_editor.dart'; export 'src/editor/raw_editor/raw_editor.dart';
export 'src/editor/raw_editor/raw_editor_state.dart'; export 'src/editor/raw_editor/raw_editor_state.dart';
export 'src/editor/spellchecker/spellchecker_service.dart';
export 'src/editor/spellchecker/spellchecker_service_provider.dart';
export 'src/editor/style_widgets/style_widgets.dart'; export 'src/editor/style_widgets/style_widgets.dart';
export 'src/editor/widgets/cursor.dart'; export 'src/editor/widgets/cursor.dart';
export 'src/editor/widgets/default_styles.dart'; export 'src/editor/widgets/default_styles.dart';

@ -1,6 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
Color stringToColor(String? s, [Color? originalColor]) { import '../../editor/widgets/default_styles.dart';
Color stringToColor(String? s,
[Color? originalColor, DefaultStyles? defaultStyles]) {
final palette = defaultStyles?.palette;
if (s != null && palette != null) {
final maybeColor = palette[s];
if (maybeColor != null) {
return maybeColor;
}
}
switch (s) { switch (s) {
case 'transparent': case 'transparent':
return Colors.transparent; return Colors.transparent;

@ -124,6 +124,12 @@ class QuillController extends ChangeNotifier {
/// It gets reset after each format action within the [document]. /// It gets reset after each format action within the [document].
Style toggledStyle = const Style(); Style toggledStyle = const Style();
/// [raw_editor_actions] handling of backspace event may need to force the style displayed in the toolbar
void forceToggledStyle(Style style) {
toggledStyle = style;
notifyListeners();
}
bool ignoreFocusOnTextChange = false; bool ignoreFocusOnTextChange = false;
/// Skip requestKeyboard being called in /// Skip requestKeyboard being called in

@ -192,11 +192,12 @@ class Document {
while ((res.node as Line).length == 1 && index > 0) { while ((res.node as Line).length == 1 && index > 0) {
res = queryChild(--index); res = queryChild(--index);
} }
// Get inline attributes from previous line // Get inline attributes from previous line (link does not cross line breaks)
final prev = (res.node as Line).collectStyle(res.offset, 0); final prev = (res.node as Line).collectStyle(res.offset, 0);
final attributes = <String, Attribute>{}; final attributes = <String, Attribute>{};
for (final attr in prev.attributes.values) { for (final attr in prev.attributes.values) {
if (attr.scope == AttributeScope.inline) { if (attr.scope == AttributeScope.inline &&
attr.key != Attribute.link.key) {
attributes[attr.key] = attr; attributes[attr.key] = attr;
} }
} }
@ -211,13 +212,15 @@ class Document {
// //
final style = (res.node as Line).collectStyle(res.offset - 1, 0); final style = (res.node as Line).collectStyle(res.offset - 1, 0);
final linkAttribute = style.attributes[Attribute.link.key]; final linkAttribute = style.attributes[Attribute.link.key];
if ((linkAttribute != null) && if (linkAttribute != null) {
(linkAttribute.value != if ((res.node!.length - 1 == res.offset) ||
(res.node as Line) (linkAttribute.value !=
.collectStyle(res.offset, len) (res.node as Line)
.attributes[Attribute.link.key] .collectStyle(res.offset, len)
?.value)) { .attributes[Attribute.link.key]
return style.removeAll({linkAttribute}); ?.value)) {
return style.removeAll({linkAttribute});
}
} }
return style; return style;
} }

@ -383,15 +383,34 @@ base class Line extends QuillContainer<Leaf?> {
pos += node.length; pos += node.length;
} }
} }
result = result.mergeAll(style);
/// Blank lines do not have style and must get the active style from prior line
if (isEmpty) {
var prevLine = previous;
while (prevLine is Block && prevLine.isNotEmpty) {
prevLine = prevLine.children.last;
}
if (prevLine is Line) {
result = result.mergeAll(prevLine.collectStyle(prevLine.length - 1, 1));
}
} else {
result = result.mergeAll(style);
}
if (parent is Block) { if (parent is Block) {
final block = parent as Block; final block = parent as Block;
result = result.mergeAll(block.style); result = result.mergeAll(block.style);
} }
final remaining = len - local; var remaining = len - local;
if (remaining > 0 && nextLine != null) { var nxt = nextLine;
final rest = nextLine!.collectStyle(0, remaining);
/// Skip over empty lines that have no attributes
while (remaining > 0 && nxt != null && nxt.isEmpty) {
remaining--;
nxt = nxt.nextLine;
}
if (remaining > 0 && nxt != null) {
final rest = nxt.collectStyle(0, remaining);
handle(rest); handle(rest);
} }

@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart' show Brightness, Uint8List, immutable; import 'package:flutter/foundation.dart' show Brightness, Uint8List, immutable;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' import 'package:flutter/material.dart'
show TextCapitalization, TextInputAction, TextSelectionThemeData; show TextCapitalization, TextInputAction, TextSelectionThemeData;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -258,11 +259,12 @@ class QuillEditorConfigurations extends Equatable {
// Returns whether gesture is handled // Returns whether gesture is handled
final bool Function( final bool Function(
TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; TapDragDownDetails details, TextPosition Function(Offset offset))?
onTapDown;
// Returns whether gesture is handled // Returns whether gesture is handled
final bool Function( final bool Function(
TapUpDetails details, TextPosition Function(Offset offset))? onTapUp; TapDragUpDetails details, TextPosition Function(Offset offset))? onTapUp;
// Returns whether gesture is handled // Returns whether gesture is handled
final bool Function( final bool Function(

@ -4,7 +4,13 @@ import 'package:flutter/cupertino.dart'
show CupertinoTheme, cupertinoTextSelectionControls; show CupertinoTheme, cupertinoTextSelectionControls;
import 'package:flutter/foundation.dart' import 'package:flutter/foundation.dart'
show ValueListenable, defaultTargetPlatform; show ValueListenable, defaultTargetPlatform;
import 'package:flutter/gestures.dart' show PointerDeviceKind; import 'package:flutter/gestures.dart'
show
PointerDeviceKind,
TapDragDownDetails,
TapDragEndDetails,
TapDragStartDetails,
TapDragUpDetails;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -488,7 +494,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
editor?.updateMagnifier(details.globalPosition); editor?.updateMagnifier(details.globalPosition);
} }
bool _isPositionSelected(TapUpDetails details) { bool _isPositionSelected(TapDragUpDetails details) {
if (_state.controller.document.isEmpty()) { if (_state.controller.document.isEmpty()) {
return false; return false;
} }
@ -511,7 +517,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
} }
@override @override
void onTapDown(TapDownDetails details) { void onTapDown(TapDragDownDetails details) {
if (_state.configurations.onTapDown != null) { if (_state.configurations.onTapDown != null) {
if (renderEditor != null && if (renderEditor != null &&
_state.configurations.onTapDown!( _state.configurations.onTapDown!(
@ -532,7 +538,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
} }
@override @override
void onSingleTapUp(TapUpDetails details) { void onSingleTapUp(TapDragUpDetails details) {
if (_state.configurations.onTapUp != null && if (_state.configurations.onTapUp != null &&
renderEditor != null && renderEditor != null &&
_state.configurations.onTapUp!( _state.configurations.onTapUp!(
@ -738,6 +744,7 @@ class RenderEditor extends RenderEditableContainerBox
Document document; Document document;
TextSelection selection; TextSelection selection;
bool _hasFocus = false; bool _hasFocus = false;
bool get hasFocus => _hasFocus;
LayerLink _startHandleLayerLink; LayerLink _startHandleLayerLink;
LayerLink _endHandleLayerLink; LayerLink _endHandleLayerLink;
@ -911,11 +918,22 @@ class RenderEditor extends RenderEditableContainerBox
final extentNode = _container.queryChild(textSelection.end, false).node; final extentNode = _container.queryChild(textSelection.end, false).node;
RenderEditableBox? extentChild = baseChild; RenderEditableBox? extentChild = baseChild;
while (extentChild != null) {
if (extentChild.container == extentNode) { /// Trap shortening the text of a link which can cause selection to extend off end of line
break; if (extentNode == null) {
while (true) {
final next = childAfter(extentChild);
if (next == null) {
break;
}
}
} else {
while (extentChild != null) {
if (extentChild.container == extentNode) {
break;
}
extentChild = childAfter(extentChild);
} }
extentChild = childAfter(extentChild);
} }
assert(extentChild != null); assert(extentChild != null);
@ -933,12 +951,20 @@ class RenderEditor extends RenderEditableContainerBox
} }
Offset? _lastTapDownPosition; Offset? _lastTapDownPosition;
Offset? _lastSecondaryTapDownPosition;
Offset? get lastSecondaryTapDownPosition => _lastSecondaryTapDownPosition;
// Used on Desktop (mouse and keyboard enabled platforms) as base offset // Used on Desktop (mouse and keyboard enabled platforms) as base offset
// for extending selection, either with combination of `Shift` + Click or // for extending selection, either with combination of `Shift` + Click or
// by dragging // by dragging
TextSelection? _extendSelectionOrigin; TextSelection? _extendSelectionOrigin;
void handleSecondaryTapDown(TapDownDetails details) {
_lastTapDownPosition = details.globalPosition;
_lastSecondaryTapDownPosition = details.globalPosition;
}
@override @override
void handleTapDown(TapDownDetails details) { void handleTapDown(TapDownDetails details) {
_lastTapDownPosition = details.globalPosition; _lastTapDownPosition = details.globalPosition;
@ -946,7 +972,7 @@ class RenderEditor extends RenderEditableContainerBox
bool _isDragging = false; bool _isDragging = false;
void handleDragStart(DragStartDetails details) { void handleDragStart(TapDragStartDetails details) {
_isDragging = true; _isDragging = true;
final newSelection = selectPositionAt( final newSelection = selectPositionAt(
@ -959,7 +985,7 @@ class RenderEditor extends RenderEditableContainerBox
_extendSelectionOrigin = newSelection; _extendSelectionOrigin = newSelection;
} }
void handleDragEnd(DragEndDetails details) { void handleDragEnd(TapDragEndDetails details) {
_isDragging = false; _isDragging = false;
onSelectionCompleted(); onSelectionCompleted();
} }

@ -100,4 +100,6 @@ abstract class EditorState extends State<QuillRawEditor>
void updateMagnifier(Offset positionToShow); void updateMagnifier(Offset positionToShow);
void hideMagnifier(); void hideMagnifier();
void toggleToolbar([bool hideHandles = true]);
} }

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../../../translations.dart'; import '../../../translations.dart';
import '../../document/attribute.dart'; import '../../document/attribute.dart';
import '../../document/style.dart';
import '../../toolbar/buttons/link_style2_button.dart'; import '../../toolbar/buttons/link_style2_button.dart';
import '../../toolbar/buttons/search/search_dialog.dart'; import '../../toolbar/buttons/search/search_dialog.dart';
import '../editor.dart'; import '../editor.dart';
@ -38,42 +39,72 @@ class QuillEditorDeleteTextAction<T extends DirectionalTextEditingIntent>
final selection = state.textEditingValue.selection; final selection = state.textEditingValue.selection;
assert(selection.isValid); assert(selection.isValid);
if (!selection.isCollapsed) { Object? execute() {
if (!selection.isCollapsed) {
return Actions.invoke(
context!,
ReplaceTextIntent(
state.textEditingValue,
'',
_expandNonCollapsedRange(state.textEditingValue),
SelectionChangedCause.keyboard),
);
}
final textBoundary = getTextBoundariesForIntent(intent);
if (!textBoundary.textEditingValue.selection.isValid) {
return null;
}
if (!textBoundary.textEditingValue.selection.isCollapsed) {
return Actions.invoke(
context!,
ReplaceTextIntent(
state.textEditingValue,
'',
_expandNonCollapsedRange(textBoundary.textEditingValue),
SelectionChangedCause.keyboard),
);
}
return Actions.invoke( return Actions.invoke(
context!, context!,
ReplaceTextIntent( ReplaceTextIntent(
state.textEditingValue, textBoundary.textEditingValue,
'', '',
_expandNonCollapsedRange(state.textEditingValue), textBoundary
SelectionChangedCause.keyboard), .getTextBoundaryAt(textBoundary.textEditingValue.selection.base),
SelectionChangedCause.keyboard,
),
); );
} }
final textBoundary = getTextBoundariesForIntent(intent); /// Backspace event needs to 'remember' the style of the deleted text.
if (!textBoundary.textEditingValue.selection.isValid) { /// Example: enter styled text, backspace to erase and reenter - expects to use the same style and not reset to default.
return null; /// Also must handle situations where text is selected and deleted by backspace.
/// Note: This implementation is the same as that used by word processors.
/// Backspace events are handled differently from selection replacement or using the delete key.
Style? postStyle;
if (!intent.forward) {
final start = selection.start + (selection.isCollapsed ? 0 : 1);
var target = state.controller.document.collectStyle(start, 0);
if (start > 0) {
final style = state.controller.document.collectStyle(start - 1, 0);
for (final key in style.attributes.keys) {
if (Attribute.inlineKeys.contains(key)) {
if (!target.containsKey(key)) {
target = target.put(Attribute(key, AttributeScope.inline, null));
}
}
}
}
postStyle = target;
} }
if (!textBoundary.textEditingValue.selection.isCollapsed) { //
return Actions.invoke( final result = execute();
context!, if (postStyle != null) {
ReplaceTextIntent( state.controller.forceToggledStyle(postStyle);
state.textEditingValue,
'',
_expandNonCollapsedRange(textBoundary.textEditingValue),
SelectionChangedCause.keyboard),
);
} }
return result;
return Actions.invoke(
context!,
ReplaceTextIntent(
textBoundary.textEditingValue,
'',
textBoundary
.getTextBoundaryAt(textBoundary.textEditingValue.selection.base),
SelectionChangedCause.keyboard,
),
);
} }
@override @override

@ -906,7 +906,7 @@ class QuillRawEditorState extends EditorState
_selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles();
_selectionOverlay?.showHandles(); _selectionOverlay?.showHandles();
if (!_keyboardVisible) { if (!_hasFocus) {
// This will show the keyboard for all selection changes on the // This will show the keyboard for all selection changes on the
// editor, not just changes triggered by user gestures. // editor, not just changes triggered by user gestures.
requestKeyboard(); requestKeyboard();
@ -1421,6 +1421,9 @@ class QuillRawEditorState extends EditorState
if (_selectionOverlay != null) { if (_selectionOverlay != null) {
if (_hasFocus) { if (_hasFocus) {
_selectionOverlay!.update(textEditingValue); _selectionOverlay!.update(textEditingValue);
} else {
_selectionOverlay!.dispose();
_selectionOverlay = null;
} }
} else if (_hasFocus) { } else if (_hasFocus) {
_selectionOverlay = _createSelectionOverlay(); _selectionOverlay = _createSelectionOverlay();
@ -1598,6 +1601,16 @@ class QuillRawEditorState extends EditorState
return true; return true;
} }
@override
void toggleToolbar([bool hideHandles = true]) {
final selectionOverlay = _selectionOverlay ??= _createSelectionOverlay();
if (selectionOverlay.handlesVisible) {
hideToolbar(hideHandles);
} else {
showToolbar();
}
}
void _replaceText(ReplaceTextIntent intent) { void _replaceText(ReplaceTextIntent intent) {
userUpdateTextEditingValue( userUpdateTextEditingValue(
intent.currentTextEditingValue intent.currentTextEditingValue
@ -1832,15 +1845,19 @@ class QuillRawEditorState extends EditorState
@override @override
void showMagnifier(ui.Offset positionToShow) { void showMagnifier(ui.Offset positionToShow) {
if (_hasFocus == false) return;
if (_selectionOverlay == null) return; if (_selectionOverlay == null) return;
final position = renderEditor.getPositionForOffset(positionToShow); final position = renderEditor.getPositionForOffset(positionToShow);
_selectionOverlay?.showMagnifier(position, positionToShow, renderEditor); if (_selectionOverlay!.magnifierIsVisible) {
_selectionOverlay!
.updateMagnifier(position, positionToShow, renderEditor);
} else {
_selectionOverlay!.showMagnifier(position, positionToShow, renderEditor);
}
} }
@override @override
void updateMagnifier(ui.Offset positionToShow) { void updateMagnifier(ui.Offset positionToShow) {
_updateOrDisposeSelectionOverlayIfNeeded(); showMagnifier(positionToShow);
final position = renderEditor.getPositionForOffset(positionToShow);
_selectionOverlay?.updateMagnifier(position, positionToShow, renderEditor);
} }
} }

@ -79,6 +79,19 @@ mixin RawEditorStateTextInputClientMixin on EditorState
_updateComposingRectIfNeeded(); _updateComposingRectIfNeeded();
//update IME position for Macos //update IME position for Macos
_updateCaretRectIfNeeded(); _updateCaretRectIfNeeded();
/// Trap selection extends off end of document
if (_lastKnownRemoteTextEditingValue != null) {
if (_lastKnownRemoteTextEditingValue!.selection.end >
_lastKnownRemoteTextEditingValue!.text.length) {
_lastKnownRemoteTextEditingValue = _lastKnownRemoteTextEditingValue!
.copyWith(
selection: _lastKnownRemoteTextEditingValue!.selection
.copyWith(
extentOffset:
_lastKnownRemoteTextEditingValue!.text.length));
}
}
_textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!);
} }
_textInputConnection!.show(); _textInputConnection!.show();

@ -0,0 +1,37 @@
import 'package:flutter/gestures.dart' show LongPressGestureRecognizer;
import 'package:flutter/material.dart' show TextSpan;
import 'spellchecker_service.dart' show SpellCheckerService;
/// A default implementation of the [SpellcheckerService]
/// that always will return null since Spell checking
/// is not a standard feature
class DefaultSpellCheckerService extends SpellCheckerService<Object?> {
DefaultSpellCheckerService() : super(language: 'en');
@override
void dispose({bool onlyPartial = false}) {}
@override
List<TextSpan>? checkSpelling(
String text, {
LongPressGestureRecognizer Function(String p1)?
customLongPressRecognizerOnWrongSpan,
}) {
return null;
}
@override
void addCustomLanguage({languageIdentifier}) {}
@override
void setNewLanguageState({required String language}) {}
@override
void updateCustomLanguageIfExist({languageIdentifier}) {}
@override
bool isServiceActive() => false;
@override
void toggleChecker() {}
}

@ -0,0 +1,39 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
/// A representation a custom SpellCheckService.
abstract class SpellCheckerService<T> {
SpellCheckerService({required this.language});
final String language;
/// Decide if the service should be activate or deactivate
/// without dispose the service
void toggleChecker();
bool isServiceActive();
/// dispose all the resources used for SpellcheckerService
///
/// if [onlyPartial] is true just dispose a part of the SpellcheckerService
/// (this comes from the implementation)
///
/// if [onlyPartial] is false dispose all resources
void dispose({bool onlyPartial = false});
/// set a new language state used for SpellcheckerService
void setNewLanguageState({required String language});
/// set a new language state used for SpellcheckerService
void updateCustomLanguageIfExist({required T languageIdentifier});
/// set a new custom language for SpellcheckerService
void addCustomLanguage({required T languageIdentifier});
/// Facilitates a spell check request.
///
/// Returns a [List<TextSpan>] with all misspelled words divide from the right words.
List<TextSpan>? checkSpelling(String text,
{LongPressGestureRecognizer Function(String)?
customLongPressRecognizerOnWrongSpan});
}

@ -0,0 +1,36 @@
import 'package:flutter/foundation.dart' show immutable;
import 'default_spellchecker_service.dart';
import 'spellchecker_service.dart';
@immutable
class SpellCheckerServiceProvider {
const SpellCheckerServiceProvider._();
static SpellCheckerService _instance = DefaultSpellCheckerService();
static SpellCheckerService get instance => _instance;
static void setNewCheckerService(SpellCheckerService service) {
_instance = service;
}
static void dispose({bool onlyPartial = false}) {
_instance.dispose(onlyPartial: onlyPartial);
}
static void toggleState() {
_instance.toggleChecker();
}
static bool isServiceActive() {
return _instance.isServiceActive();
}
static void setNewLanguageState({required String language}) {
assert(language.isNotEmpty);
_instance.setNewLanguageState(language: language);
}
static void turnOffService() {
_instance = DefaultSpellCheckerService();
}
}

@ -200,6 +200,7 @@ class DefaultStyles {
this.sizeSmall, this.sizeSmall,
this.sizeLarge, this.sizeLarge,
this.sizeHuge, this.sizeHuge,
this.palette,
}); });
final DefaultTextBlockStyle? h1; final DefaultTextBlockStyle? h1;
@ -236,6 +237,9 @@ class DefaultStyles {
final DefaultTextBlockStyle? align; final DefaultTextBlockStyle? align;
final DefaultTextBlockStyle? leading; final DefaultTextBlockStyle? leading;
/// Custom palette of colors
final Map<String, Color>? palette;
static DefaultStyles getInstance(BuildContext context) { static DefaultStyles getInstance(BuildContext context) {
final themeData = Theme.of(context); final themeData = Theme.of(context);
final defaultTextStyle = DefaultTextStyle.of(context); final defaultTextStyle = DefaultTextStyle.of(context);
@ -518,6 +522,7 @@ class DefaultStyles {
sizeSmall: other.sizeSmall ?? sizeSmall, sizeSmall: other.sizeSmall ?? sizeSmall,
sizeLarge: other.sizeLarge ?? sizeLarge, sizeLarge: other.sizeLarge ?? sizeLarge,
sizeHuge: other.sizeHuge ?? sizeHuge, sizeHuge: other.sizeHuge ?? sizeHuge,
palette: other.palette ?? palette,
); );
} }
} }

@ -1,7 +1,9 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import '../../common/utils/platform.dart'; import '../../common/utils/platform.dart';
import '../../document/attribute.dart'; import '../../document/attribute.dart';
@ -107,6 +109,138 @@ class EditorTextSelectionGestureDetectorBuilder {
@protected @protected
RenderEditor? get renderEditor => editor?.renderEditor; RenderEditor? get renderEditor => editor?.renderEditor;
/// Whether the Shift key was pressed when the most recent [PointerDownEvent]
/// was tracked by the [BaseTapAndDragGestureRecognizer].
bool _isShiftPressed = false;
/// The viewport offset pixels of any [Scrollable] containing the
/// [RenderEditable] at the last drag start.
double _dragStartScrollOffset = 0;
/// The viewport offset pixels of the [RenderEditable] at the last drag start.
double _dragStartViewportOffset = 0;
double get _scrollPosition {
final scrollableState = delegate.editableTextKey.currentContext == null
? null
: Scrollable.maybeOf(delegate.editableTextKey.currentContext!);
return scrollableState == null ? 0.0 : scrollableState.position.pixels;
}
// For tap + drag gesture on iOS, whether the position where the drag started
// was on the previous TextSelection. iOS uses this value to determine if
// the cursor should move on drag update.
//
TextSelection? _dragStartSelection;
// If the drag started on the previous selection then the cursor will move on
// drag update. If the drag did not start on the previous selection then the
// cursor will not move on drag update.
bool? _dragBeganOnPreviousSelection;
/// Returns true if lastSecondaryTapDownPosition was on selection.
bool get _lastSecondaryTapWasOnSelection {
assert(renderEditor?.lastSecondaryTapDownPosition != null);
if (renderEditor?.selection == null) {
return false;
}
renderEditor?.lastSecondaryTapDownPosition;
final textPosition = renderEditor?.getPositionForOffset(
renderEditor!.lastSecondaryTapDownPosition!,
);
if (textPosition == null) return false;
return renderEditor!.selection.start <= textPosition.offset &&
renderEditor!.selection.end >= textPosition.offset;
}
/// Returns true if position was on selection.
bool _positionOnSelection(Offset position, TextSelection? targetSelection) {
if (targetSelection == null) return false;
final textPosition = renderEditor?.getPositionForOffset(position);
if (textPosition == null) return false;
return targetSelection.start <= textPosition.offset &&
targetSelection.end >= textPosition.offset;
}
// Expand the selection to the given global position.
//
// Either base or extent will be moved to the last tapped position, whichever
// is closest. The selection will never shrink or pivot, only grow.
//
// If fromSelection is given, will expand from that selection instead of the
// current selection in renderEditable.
//
// See also:
//
// * [_extendSelection], which is similar but pivots the selection around
// the base.
void _expandSelection(Offset offset, SelectionChangedCause cause,
[TextSelection? fromSelection]) {
final tappedPosition = renderEditor!.getPositionForOffset(offset);
final selection = fromSelection ?? renderEditor!.selection;
final baseIsCloser = (tappedPosition.offset - selection.baseOffset).abs() <
(tappedPosition.offset - selection.extentOffset).abs();
final nextSelection = selection.copyWith(
baseOffset: baseIsCloser ? selection.extentOffset : selection.baseOffset,
extentOffset: tappedPosition.offset,
);
editor?.userUpdateTextEditingValue(
editor!.textEditingValue.copyWith(selection: nextSelection), cause);
}
// Extend the selection to the given global position.
//
// Holds the base in place and moves the extent.
//
// See also:
//
// * [_expandSelection], which is similar but always increases the size of
// the selection.
void _extendSelection(Offset offset, SelectionChangedCause cause) {
assert(renderEditor?.selection.baseOffset != null);
final tappedPosition = renderEditor!.getPositionForOffset(offset);
final selection = renderEditor!.selection;
final nextSelection = selection.copyWith(
extentOffset: tappedPosition.offset,
);
editor?.userUpdateTextEditingValue(
editor!.textEditingValue.copyWith(selection: nextSelection), cause);
}
/// Handler for [TextSelectionGestureDetector.onTapTrackStart].
///
/// See also:
///
/// * [TextSelectionGestureDetector.onTapTrackStart], which triggers this
/// callback.
@protected
void onTapTrackStart() {
_isShiftPressed = HardwareKeyboard.instance.logicalKeysPressed
.intersection(<LogicalKeyboardKey>{
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight
}).isNotEmpty;
}
/// Handler for [TextSelectionGestureDetector.onTapTrackReset].
///
/// See also:
///
/// * [TextSelectionGestureDetector.onTapTrackReset], which triggers this
/// callback.
@protected
void onTapTrackReset() {
_isShiftPressed = false;
}
/// Handler for [EditorTextSelectionGestureDetector.onTapDown]. /// Handler for [EditorTextSelectionGestureDetector.onTapDown].
/// ///
/// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets /// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets
@ -118,19 +252,45 @@ class EditorTextSelectionGestureDetectorBuilder {
/// * [EditorTextSelectionGestureDetector.onTapDown], /// * [EditorTextSelectionGestureDetector.onTapDown],
/// which triggers this callback. /// which triggers this callback.
@protected @protected
void onTapDown(TapDownDetails details) { void onTapDown(TapDragDownDetails details) {
renderEditor!.handleTapDown(details); if (!delegate.selectionEnabled) return;
// The selection overlay should only be shown when the user is interacting renderEditor!
// through a touch screen (via either a finger or a stylus). .handleTapDown(TapDownDetails(globalPosition: details.globalPosition));
// A mouse shouldn't trigger the selection overlay. final kind = details.kind;
// For backwards-compatibility, we treat a null kind the same as touch.
kind = details.kind;
shouldShowSelectionToolbar = kind == null || shouldShowSelectionToolbar = kind == null ||
kind ==
PointerDeviceKind
.mouse || // Enable word selection by mouse double tap
kind == PointerDeviceKind.touch || kind == PointerDeviceKind.touch ||
kind == PointerDeviceKind.stylus; kind == PointerDeviceKind.stylus;
final isShiftPressedValid =
_isShiftPressed && renderEditor?.selection.baseOffset != null;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
editor?.hideToolbar(false);
case TargetPlatform.iOS:
// On mobile platforms the selection is set on tap up.
break;
case TargetPlatform.macOS:
editor?.hideToolbar();
// On macOS, a shift-tapped unfocused field expands from 0, not from the
// previous selection.
if (isShiftPressedValid) {
final fromSelection = renderEditor?.hasFocus == true
? null
: const TextSelection.collapsed(offset: 0);
_expandSelection(
details.globalPosition, SelectionChangedCause.tap, fromSelection);
return;
}
renderEditor?.selectPosition(cause: SelectionChangedCause.tap);
case TargetPlatform.linux:
case TargetPlatform.windows:
editor?.hideToolbar();
if (isShiftPressedValid) {
_extendSelection(details.globalPosition, SelectionChangedCause.tap);
return;
}
renderEditor?.selectPosition(cause: SelectionChangedCause.tap);
}
} }
/// Handler for [EditorTextSelectionGestureDetector.onForcePressStart]. /// Handler for [EditorTextSelectionGestureDetector.onForcePressStart].
@ -181,6 +341,27 @@ class EditorTextSelectionGestureDetectorBuilder {
} }
} }
/// Whether the provided [onUserTap] callback should be dispatched on every
/// tap or only non-consecutive taps.
///
/// Defaults to false.
@protected
bool get onUserTapAlwaysCalled => false;
/// Handler for [TextSelectionGestureDetector.onUserTap].
///
/// By default, it serves as placeholder to enable subclass override.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onUserTap], which triggers this
/// callback.
/// * [TextSelectionGestureDetector.onUserTapAlwaysCalled], which controls
/// whether this callback is called only on the first tap in a series
/// of taps.
@protected
void onUserTap() {/* Subclass should override this method if needed. */}
/// Handler for [EditorTextSelectionGestureDetector.onSingleTapUp]. /// Handler for [EditorTextSelectionGestureDetector.onSingleTapUp].
/// ///
/// By default, it selects word edge if selection is enabled. /// By default, it selects word edge if selection is enabled.
@ -190,7 +371,7 @@ class EditorTextSelectionGestureDetectorBuilder {
/// * [EditorTextSelectionGestureDetector.onSingleTapUp], which triggers /// * [EditorTextSelectionGestureDetector.onSingleTapUp], which triggers
/// this callback. /// this callback.
@protected @protected
void onSingleTapUp(TapUpDetails details) { void onSingleTapUp(TapDragUpDetails details) {
if (delegate.selectionEnabled) { if (delegate.selectionEnabled) {
renderEditor!.selectWordEdge(SelectionChangedCause.tap); renderEditor!.selectWordEdge(SelectionChangedCause.tap);
} }
@ -269,6 +450,64 @@ class EditorTextSelectionGestureDetectorBuilder {
if (checkSelectionToolbarShouldShow(isAdditionalAction: false)) { if (checkSelectionToolbarShouldShow(isAdditionalAction: false)) {
editor!.showToolbar(); editor!.showToolbar();
} }
// Q: why ?
// A: cannot access QuillRawEditorState.updateFloatingCursor
//
// if (defaultTargetPlatform == TargetPlatform.iOS &&
// delegate.selectionEnabled &&
// editor?.textEditingValue.selection.isCollapsed == true) {
// // Update the floating cursor.
// final cursorPoint =
// RawFloatingCursorPoint(state: FloatingCursorDragState.End);
// // !.updateFloatingCursor(cursorPoint);
// (editor as QuillRawEditorState?)?.updateFloatingCursor(cursorPoint);
// }
}
/// Handler for [TextSelectionGestureDetector.onSecondaryTap].
///
/// By default, selects the word if possible and shows the toolbar.
@protected
void onSecondaryTap() {
if (!delegate.selectionEnabled) {
return;
}
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
if (!_lastSecondaryTapWasOnSelection ||
renderEditor?.hasFocus == false) {
renderEditor?.selectWord(SelectionChangedCause.tap);
}
if (shouldShowSelectionToolbar) {
editor?.hideToolbar();
editor?.showToolbar();
}
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
if (renderEditor?.hasFocus == false) {
renderEditor?.selectPosition(cause: SelectionChangedCause.tap);
}
editor?.toggleToolbar();
}
}
/// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
///
/// By default, it selects a word through [RenderEditable.selectWord] if
/// selectionEnabled and shows toolbar if necessary.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this
/// callback.
@protected
void onSecondaryTapDown(TapDownDetails details) {
renderEditor?.handleSecondaryTapDown(
TapDownDetails(globalPosition: details.globalPosition));
shouldShowSelectionToolbar = true;
} }
/// Handler for [EditorTextSelectionGestureDetector.onDoubleTapDown]. /// Handler for [EditorTextSelectionGestureDetector.onDoubleTapDown].
@ -281,7 +520,7 @@ class EditorTextSelectionGestureDetectorBuilder {
/// * [EditorTextSelectionGestureDetector.onDoubleTapDown], /// * [EditorTextSelectionGestureDetector.onDoubleTapDown],
/// which triggers this callback. /// which triggers this callback.
@protected @protected
void onDoubleTapDown(TapDownDetails details) { void onDoubleTapDown(TapDragDownDetails details) {
if (delegate.selectionEnabled) { if (delegate.selectionEnabled) {
renderEditor!.selectWord(SelectionChangedCause.tap); renderEditor!.selectWord(SelectionChangedCause.tap);
// allow the selection to get updated before trying to bring up // allow the selection to get updated before trying to bring up
@ -298,6 +537,105 @@ class EditorTextSelectionGestureDetectorBuilder {
} }
} }
// Selects the set of paragraphs in a document that intersect a given range of
// global positions.
void _selectParagraphsInRange(
{required Offset from, Offset? to, SelectionChangedCause? cause}) {
final TextBoundary paragraphBoundary =
ParagraphBoundary(editor!.textEditingValue.text);
_selectTextBoundariesInRange(
boundary: paragraphBoundary, from: from, to: to, cause: cause);
}
// Selects the set of lines in a document that intersect a given range of
// global positions.
void _selectLinesInRange(
{required Offset from, Offset? to, SelectionChangedCause? cause}) {
final TextBoundary lineBoundary = LineBoundary(renderEditor!);
_selectTextBoundariesInRange(
boundary: lineBoundary, from: from, to: to, cause: cause);
}
// Returns the location of a text boundary at `extent`. When `extent` is at
// the end of the text, returns the previous text boundary's location.
TextRange _moveToTextBoundary(
TextPosition extent, TextBoundary textBoundary) {
assert(extent.offset >= 0);
final start = textBoundary.getLeadingTextBoundaryAt(
extent.offset == editor!.textEditingValue.text.length
? extent.offset - 1
: extent.offset) ??
0;
final end = textBoundary.getTrailingTextBoundaryAt(extent.offset) ??
editor!.textEditingValue.text.length;
return TextRange(start: start, end: end);
}
// Selects the set of text boundaries in a document that intersect a given
// range of global positions.
//
// The set of text boundaries selected are not strictly bounded by the range
// of global positions.
//
// The first and last endpoints of the selection will always be at the
// beginning and end of a text boundary respectively.
void _selectTextBoundariesInRange(
{required TextBoundary boundary,
required Offset from,
Offset? to,
SelectionChangedCause? cause}) {
final fromPosition = renderEditor!.getPositionForOffset(from);
final fromRange = _moveToTextBoundary(fromPosition, boundary);
final toPosition =
to == null ? fromPosition : renderEditor!.getPositionForOffset(to);
final toRange = toPosition == fromPosition
? fromRange
: _moveToTextBoundary(toPosition, boundary);
final isFromBoundaryBeforeToBoundary = fromRange.start < toRange.end;
final newSelection = isFromBoundaryBeforeToBoundary
? TextSelection(baseOffset: fromRange.start, extentOffset: toRange.end)
: TextSelection(baseOffset: fromRange.end, extentOffset: toRange.start);
editor!.userUpdateTextEditingValue(
editor!.textEditingValue.copyWith(selection: newSelection),
cause ?? SelectionChangedCause.drag);
}
/// Handler for [TextSelectionGestureDetector.onTripleTapDown].
///
/// By default, it selects a paragraph if
/// [TextSelectionGestureDetectorBuilderDelegate.selectionEnabled] is true
/// and shows the toolbar if necessary.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onTripleTapDown], which triggers this
/// callback.
@protected
void onTripleTapDown(TapDragDownDetails details) {
if (!delegate.selectionEnabled) {
return;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
case TargetPlatform.macOS:
case TargetPlatform.windows:
_selectParagraphsInRange(
from: details.globalPosition, cause: SelectionChangedCause.tap);
case TargetPlatform.linux:
_selectLinesInRange(
from: details.globalPosition, cause: SelectionChangedCause.tap);
}
if (shouldShowSelectionToolbar) {
editor?.showToolbar();
}
}
/// Handler for [EditorTextSelectionGestureDetector.onDragSelectionStart]. /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionStart].
/// ///
/// By default, it selects a text position specified in [details]. /// By default, it selects a text position specified in [details].
@ -307,8 +645,106 @@ class EditorTextSelectionGestureDetectorBuilder {
/// * [EditorTextSelectionGestureDetector.onDragSelectionStart], /// * [EditorTextSelectionGestureDetector.onDragSelectionStart],
/// which triggers this callback. /// which triggers this callback.
@protected @protected
void onDragSelectionStart(DragStartDetails details) { void onDragSelectionStart(TapDragStartDetails details) {
renderEditor!.handleDragStart(details); if (delegate.selectionEnabled == false) return;
// underline show open on ios and android,
// when has isCollapsed, show not reposonse to tapdarg gesture
// so that will not change texteditingvalue,
// and same issue to TextField, tap selection area, will lost selection,
// if (editor?.textEditingValue.selection.isCollapsed == false) return;
final kind = details.kind;
shouldShowSelectionToolbar = kind == null ||
kind == PointerDeviceKind.touch ||
kind == PointerDeviceKind.stylus;
_dragStartSelection = renderEditor?.selection;
_dragStartScrollOffset = _scrollPosition;
_dragStartViewportOffset = renderEditor?.offset?.pixels ?? 0.0;
_dragBeganOnPreviousSelection =
_positionOnSelection(details.globalPosition, _dragStartSelection);
if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount(
details.consecutiveTapCount) >
1) {
// Do not set the selection on a consecutive tap and drag.
return;
}
if (_isShiftPressed &&
renderEditor?.selection != null &&
renderEditor?.selection.isValid == true) {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
renderEditor?.extendSelection(details.globalPosition,
cause: SelectionChangedCause.drag);
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
renderEditor?.extendSelection(details.globalPosition,
cause: SelectionChangedCause.drag);
}
} else {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
switch (details.kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
renderEditor?.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
// For iOS platforms, a touch drag does not initiate unless the
// editable has focus and the drag began on the previous selection.
assert(_dragBeganOnPreviousSelection != null);
if (renderEditor?.hasFocus == true &&
_dragBeganOnPreviousSelection!) {
renderEditor?.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
editor?.showMagnifier(details.globalPosition);
}
case null:
}
case TargetPlatform.android:
case TargetPlatform.fuchsia:
switch (details.kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
renderEditor?.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
// For Android, Fucshia, and iOS platforms, a touch drag
// does not initiate unless the editable has focus.
if (renderEditor?.hasFocus == true) {
renderEditor?.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
editor?.showMagnifier(details.globalPosition);
}
case null:
}
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
renderEditor?.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
}
}
} }
/// Handler for [EditorTextSelectionGestureDetector.onDragSelectionUpdate]. /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionUpdate].
@ -321,13 +757,206 @@ class EditorTextSelectionGestureDetectorBuilder {
/// * [EditorTextSelectionGestureDetector.onDragSelectionUpdate], /// * [EditorTextSelectionGestureDetector.onDragSelectionUpdate],
/// which triggers this callback./lib/src/material/text_field.dart /// which triggers this callback./lib/src/material/text_field.dart
@protected @protected
void onDragSelectionUpdate( void onDragSelectionUpdate(TapDragUpdateDetails updateDetails) {
//DragStartDetails startDetails, if (delegate.selectionEnabled == false) return;
DragUpdateDetails updateDetails) { // if (editor?.textEditingValue.selection.isCollapsed == false) return;
renderEditor!.extendSelection( if (!_isShiftPressed) {
updateDetails.globalPosition, // Adjust the drag start offset for possible viewport offset changes.
cause: SelectionChangedCause.drag, final editableOffset =
); Offset(0, renderEditor!.offset!.pixels - _dragStartViewportOffset);
final scrollableOffset =
Offset(0, _scrollPosition - _dragStartScrollOffset);
final dragStartGlobalPosition =
updateDetails.globalPosition - updateDetails.offsetFromOrigin;
// Select word by word.
if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount(
updateDetails.consecutiveTapCount) ==
2) {
renderEditor?.selectWordsInRange(
dragStartGlobalPosition - editableOffset - scrollableOffset,
updateDetails.globalPosition,
SelectionChangedCause.drag,
);
switch (updateDetails.kind) {
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
return editor?.updateMagnifier(updateDetails.globalPosition);
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
case null:
return;
}
}
// Select paragraph-by-paragraph.
if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount(
updateDetails.consecutiveTapCount) ==
3) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
switch (updateDetails.kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
return _selectParagraphsInRange(
from: dragStartGlobalPosition -
editableOffset -
scrollableOffset,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
case null:
// Triple tap to drag is not present on these platforms when using
// non-precise pointer devices at the moment.
break;
}
return;
case TargetPlatform.linux:
return _selectLinesInRange(
from: dragStartGlobalPosition - editableOffset - scrollableOffset,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
case TargetPlatform.windows:
case TargetPlatform.macOS:
return _selectParagraphsInRange(
from: dragStartGlobalPosition - editableOffset - scrollableOffset,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
}
}
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
// With a touch device, nothing should happen, unless there was a double tap, or
// there was a collapsed selection, and the tap/drag position is at the collapsed selection.
// In that case the caret should move with the drag position.
//
// With a mouse device, a drag should select the range from the origin of the drag
// to the current position of the drag.
switch (updateDetails.kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
renderEditor?.selectPositionAt(
from:
dragStartGlobalPosition - editableOffset - scrollableOffset,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
return;
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
assert(_dragBeganOnPreviousSelection != null);
if (renderEditor?.hasFocus == true &&
_dragStartSelection!.isCollapsed &&
_dragBeganOnPreviousSelection!) {
renderEditor?.selectPositionAt(
from: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
return editor?.updateMagnifier(updateDetails.globalPosition);
}
case null:
break;
}
return;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
// With a precise pointer device, such as a mouse, trackpad, or stylus,
// the drag will select the text spanning the origin of the drag to the end of the drag.
// With a touch device, the cursor should move with the drag.
switch (updateDetails.kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
renderEditor?.selectPositionAt(
from:
dragStartGlobalPosition - editableOffset - scrollableOffset,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
return;
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
if (renderEditor?.hasFocus == true) {
renderEditor?.selectPositionAt(
from: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
return editor?.updateMagnifier(updateDetails.globalPosition);
}
case null:
break;
}
return;
case TargetPlatform.macOS:
case TargetPlatform.linux:
case TargetPlatform.windows:
renderEditor?.selectPositionAt(
from: dragStartGlobalPosition - editableOffset - scrollableOffset,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
}
}
if (_dragStartSelection!.isCollapsed ||
(defaultTargetPlatform != TargetPlatform.iOS &&
defaultTargetPlatform != TargetPlatform.macOS)) {
return _extendSelection(
updateDetails.globalPosition, SelectionChangedCause.drag);
}
// If the drag inverts the selection, Mac and iOS revert to the initial
// selection.
final selection = renderEditor!.selection;
final nextExtent =
renderEditor!.getPositionForOffset(updateDetails.globalPosition);
final isShiftTapDragSelectionForward =
_dragStartSelection!.baseOffset < _dragStartSelection!.extentOffset;
final isInverted = isShiftTapDragSelectionForward
? nextExtent.offset < _dragStartSelection!.baseOffset
: nextExtent.offset > _dragStartSelection!.baseOffset;
if (isInverted && selection.baseOffset == _dragStartSelection!.baseOffset) {
editor?.userUpdateTextEditingValue(
editor!.textEditingValue.copyWith(
selection: TextSelection(
baseOffset: _dragStartSelection!.extentOffset,
extentOffset: nextExtent.offset,
),
),
SelectionChangedCause.drag,
);
} else if (!isInverted &&
nextExtent.offset != _dragStartSelection!.baseOffset &&
selection.baseOffset != _dragStartSelection!.baseOffset) {
editor?.userUpdateTextEditingValue(
editor!.textEditingValue.copyWith(
selection: TextSelection(
baseOffset: _dragStartSelection!.baseOffset,
extentOffset: nextExtent.offset,
),
),
SelectionChangedCause.drag,
);
} else {
_extendSelection(
updateDetails.globalPosition, SelectionChangedCause.drag);
}
} }
/// Handler for [EditorTextSelectionGestureDetector.onDragSelectionEnd]. /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionEnd].
@ -339,7 +968,8 @@ class EditorTextSelectionGestureDetectorBuilder {
/// * [EditorTextSelectionGestureDetector.onDragSelectionEnd], /// * [EditorTextSelectionGestureDetector.onDragSelectionEnd],
/// which triggers this callback. /// which triggers this callback.
@protected @protected
void onDragSelectionEnd(DragEndDetails details) { void onDragSelectionEnd(TapDragEndDetails details) {
// if (editor?.textEditingValue.selection.isCollapsed == false) return;
renderEditor!.handleDragEnd(details); renderEditor!.handleDragEnd(details);
if (isDesktop(supportWeb: true) && if (isDesktop(supportWeb: true) &&
delegate.selectionEnabled && delegate.selectionEnabled &&
@ -347,6 +977,7 @@ class EditorTextSelectionGestureDetectorBuilder {
// added to show selection copy/paste toolbar after drag to select // added to show selection copy/paste toolbar after drag to select
editor!.showToolbar(); editor!.showToolbar();
} }
editor?.hideMagnifier();
} }
/// Returns a [EditorTextSelectionGestureDetector] configured with /// Returns a [EditorTextSelectionGestureDetector] configured with
@ -361,21 +992,26 @@ class EditorTextSelectionGestureDetectorBuilder {
}) { }) {
return EditorTextSelectionGestureDetector( return EditorTextSelectionGestureDetector(
key: key, key: key,
onTapTrackStart: onTapTrackStart,
onTapTrackReset: onTapTrackReset,
onTapDown: onTapDown, onTapDown: onTapDown,
onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null, onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null, onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
onSingleTapUp: onSingleTapUp, onSingleTapUp: onSingleTapUp,
onSingleTapCancel: onSingleTapCancel, onSingleTapCancel: onSingleTapCancel,
onUserTap: onUserTap,
onSingleLongTapStart: onSingleLongTapStart, onSingleLongTapStart: onSingleLongTapStart,
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
onSingleLongTapEnd: onSingleLongTapEnd, onSingleLongTapEnd: onSingleLongTapEnd,
onDoubleTapDown: onDoubleTapDown, onDoubleTapDown: onDoubleTapDown,
onSecondarySingleTapUp: onSecondarySingleTapUp, onTripleTapDown: onTripleTapDown,
onDragSelectionStart: onDragSelectionStart, onDragSelectionStart: onDragSelectionStart,
onDragSelectionUpdate: onDragSelectionUpdate, onDragSelectionUpdate: onDragSelectionUpdate,
onDragSelectionEnd: onDragSelectionEnd, onDragSelectionEnd: onDragSelectionEnd,
onUserTapAlwaysCalled: onUserTapAlwaysCalled,
behavior: behavior, behavior: behavior,
detectWordBoundary: detectWordBoundary,
child: child, child: child,
); );
} }

@ -391,10 +391,8 @@ class _TextLineState extends State<TextLine> {
final nodeStyle = textNode.style; final nodeStyle = textNode.style;
final isLink = nodeStyle.containsKey(Attribute.link.key) && final isLink = nodeStyle.containsKey(Attribute.link.key) &&
nodeStyle.attributes[Attribute.link.key]!.value != null; nodeStyle.attributes[Attribute.link.key]!.value != null;
final style = final style =
_getInlineTextStyle(nodeStyle, defaultStyles, lineStyle, isLink); _getInlineTextStyle(nodeStyle, defaultStyles, lineStyle, isLink);
if (widget.controller.configurations.requireScriptFontFeatures == false && if (widget.controller.configurations.requireScriptFontFeatures == false &&
textNode.value.isNotEmpty) { textNode.value.isNotEmpty) {
if (nodeStyle.containsKey(Attribute.script.key)) { if (nodeStyle.containsKey(Attribute.script.key)) {
@ -406,6 +404,22 @@ class _TextLineState extends State<TextLine> {
} }
} }
if (!isLink &&
!widget.readOnly &&
!widget.line.style.attributes.containsKey('code-block') &&
!widget.line.style.attributes.containsKey('placeholder') &&
!kIsWeb) {
final service = SpellCheckerServiceProvider.instance;
final spellcheckedSpans = service.checkSpelling(textNode.value);
if (spellcheckedSpans != null && spellcheckedSpans.isNotEmpty) {
return TextSpan(
children: spellcheckedSpans,
style: style,
mouseCursor: null,
);
}
}
final recognizer = _getRecognizer(node, isLink); final recognizer = _getRecognizer(node, isLink);
return TextSpan( return TextSpan(
text: textNode.value, text: textNode.value,
@ -432,7 +446,7 @@ class _TextLineState extends State<TextLine> {
if (k == Attribute.underline.key || k == Attribute.strikeThrough.key) { if (k == Attribute.underline.key || k == Attribute.strikeThrough.key) {
var textColor = defaultStyles.color; var textColor = defaultStyles.color;
if (color?.value is String) { if (color?.value is String) {
textColor = stringToColor(color?.value, textColor); textColor = stringToColor(color?.value, textColor, defaultStyles);
} }
res = _merge(res.copyWith(decorationColor: textColor), res = _merge(res.copyWith(decorationColor: textColor),
s!.copyWith(decorationColor: textColor)); s!.copyWith(decorationColor: textColor));
@ -486,7 +500,7 @@ class _TextLineState extends State<TextLine> {
if (color != null && color.value != null) { if (color != null && color.value != null) {
var textColor = defaultStyles.color; var textColor = defaultStyles.color;
if (color.value is String) { if (color.value is String) {
textColor = stringToColor(color.value); textColor = stringToColor(color.value, null, defaultStyles);
} }
if (textColor != null) { if (textColor != null) {
res = res.merge(TextStyle(color: textColor)); res = res.merge(TextStyle(color: textColor));
@ -495,7 +509,8 @@ class _TextLineState extends State<TextLine> {
final background = nodeStyle.attributes[Attribute.background.key]; final background = nodeStyle.attributes[Attribute.background.key];
if (background != null && background.value != null) { if (background != null && background.value != null) {
final backgroundColor = stringToColor(background.value); final backgroundColor =
stringToColor(background.value, null, defaultStyles);
res = res.merge(TextStyle(backgroundColor: backgroundColor)); res = res.merge(TextStyle(backgroundColor: backgroundColor));
} }

@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -187,6 +186,8 @@ class EditorTextSelectionOverlay {
final MagnifierController _magnifierController = MagnifierController(); final MagnifierController _magnifierController = MagnifierController();
bool get magnifierIsVisible => _magnifierController.shown;
final TextMagnifierConfiguration magnifierConfiguration; final TextMagnifierConfiguration magnifierConfiguration;
final ValueNotifier<MagnifierInfo> _magnifierInfo = final ValueNotifier<MagnifierInfo> _magnifierInfo =
@ -433,7 +434,7 @@ class EditorTextSelectionOverlay {
context: context, context: context,
below: magnifierConfiguration.shouldDisplayHandlesInMagnifier below: magnifierConfiguration.shouldDisplayHandlesInMagnifier
? null ? null
: _handles![0], : _handles?.elementAtOrNull(0),
builder: (_) => builtMagnifier, builder: (_) => builtMagnifier,
); );
} }
@ -790,31 +791,39 @@ class EditorTextSelectionGestureDetector extends StatefulWidget {
/// The [child] parameter must not be null. /// The [child] parameter must not be null.
const EditorTextSelectionGestureDetector({ const EditorTextSelectionGestureDetector({
required this.child, required this.child,
super.key,
this.onTapTrackStart,
this.onTapTrackReset,
this.onTapDown, this.onTapDown,
this.onForcePressStart, this.onForcePressStart,
this.onForcePressEnd, this.onForcePressEnd,
this.onSecondaryTap,
this.onSecondaryTapDown,
this.onSingleTapUp, this.onSingleTapUp,
this.onSingleTapCancel, this.onSingleTapCancel,
this.onSecondaryTapDown, this.onUserTap,
this.onSecondarySingleTapUp,
this.onSecondarySingleTapCancel,
this.onSecondaryDoubleTapDown,
this.onSingleLongTapStart, this.onSingleLongTapStart,
this.onSingleLongTapMoveUpdate, this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd, this.onSingleLongTapEnd,
this.onDoubleTapDown, this.onDoubleTapDown,
this.onTripleTapDown,
this.onDragSelectionStart, this.onDragSelectionStart,
this.onDragSelectionUpdate, this.onDragSelectionUpdate,
this.onDragSelectionEnd, this.onDragSelectionEnd,
this.onUserTapAlwaysCalled = false,
this.behavior, this.behavior,
this.detectWordBoundary = true,
super.key,
}); });
/// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.onTapTrackStart}
final VoidCallback? onTapTrackStart;
/// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.onTapTrackReset}
final VoidCallback? onTapTrackReset;
/// Called for every tap down including every tap down that's part of a /// Called for every tap down including every tap down that's part of a
/// double click or a long press, except touches that include enough movement /// double click or a long press, except touches that include enough movement
/// to not qualify as taps (e.g. pans and flings). /// to not qualify as taps (e.g. pans and flings).
final GestureTapDownCallback? onTapDown; final GestureTapDragDownCallback? onTapDown;
/// Called when a pointer has tapped down and the force of the pointer has /// Called when a pointer has tapped down and the force of the pointer has
/// just become greater than [ForcePressGestureRecognizer.startPressure]. /// just become greater than [ForcePressGestureRecognizer.startPressure].
@ -824,28 +833,31 @@ class EditorTextSelectionGestureDetector extends StatefulWidget {
/// lifted off the screen. /// lifted off the screen.
final GestureForcePressEndCallback? onForcePressEnd; final GestureForcePressEndCallback? onForcePressEnd;
/// Called for each distinct tap except for every second tap of a double tap. /// Called for a tap event with the secondary mouse button.
final GestureTapCallback? onSecondaryTap;
/// Called for a tap down event with the secondary mouse button.
final GestureTapDownCallback? onSecondaryTapDown;
/// Called for the first tap in a series of taps, consecutive taps do not call
/// this method.
///
/// For example, if the detector was configured with [onTapDown] and /// For example, if the detector was configured with [onTapDown] and
/// [onDoubleTapDown], three quick taps would be recognized as a single tap /// [onDoubleTapDown], three quick taps would be recognized as a single tap
/// down, followed by a double tap down, followed by a single tap down. /// down, followed by a tap up, then a double tap down, followed by a single tap down.
final GestureTapUpCallback? onSingleTapUp; final GestureTapDragUpCallback? onSingleTapUp;
/// Called for each touch that becomes recognized as a gesture that is not a /// Called for each touch that becomes recognized as a gesture that is not a
/// short tap, such as a long tap or drag. It is called at the moment when /// short tap, such as a long tap or drag. It is called at the moment when
/// another gesture from the touch is recognized. /// another gesture from the touch is recognized.
final GestureTapCancelCallback? onSingleTapCancel; final GestureCancelCallback? onSingleTapCancel;
/// onTapDown for mouse right click
final GestureTapDownCallback? onSecondaryTapDown;
/// onTapUp for mouse right click
final GestureTapUpCallback? onSecondarySingleTapUp;
/// onTapCancel for mouse right click
final GestureTapCancelCallback? onSecondarySingleTapCancel;
/// onDoubleTap for mouse right click /// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is
final GestureTapDownCallback? onSecondaryDoubleTapDown; /// disabled, which is the default behavior.
///
/// When [onUserTapAlwaysCalled] is enabled, this is called for every tap,
/// including consecutive taps.
final GestureTapCallback? onUserTap;
/// Called for a single long tap that's sustained for longer than /// Called for a single long tap that's sustained for longer than
/// [kLongPressTimeout] but not necessarily lifted. Not called for a /// [kLongPressTimeout] but not necessarily lifted. Not called for a
@ -860,20 +872,25 @@ class EditorTextSelectionGestureDetector extends StatefulWidget {
/// Called after a momentary hold or a short tap that is close in space and /// Called after a momentary hold or a short tap that is close in space and
/// time (within [kDoubleTapTimeout]) to a previous short tap. /// time (within [kDoubleTapTimeout]) to a previous short tap.
final GestureTapDownCallback? onDoubleTapDown; final GestureTapDragDownCallback? onDoubleTapDown;
/// Called after a momentary hold or a short tap that is close in space and
/// time (within [kDoubleTapTimeout]) to a previous double-tap.
final GestureTapDragDownCallback? onTripleTapDown;
/// Called when a mouse starts dragging to select text. /// Called when a mouse starts dragging to select text.
final GestureDragStartCallback? onDragSelectionStart; final GestureTapDragStartCallback? onDragSelectionStart;
/// Called repeatedly as a mouse moves while dragging. /// Called repeatedly as a mouse moves while dragging.
/// final GestureTapDragUpdateCallback? onDragSelectionUpdate;
/// The frequency of calls is throttled to avoid excessive text layout
/// operations in text fields. The throttling is controlled by the constant
/// [_kDragSelectionUpdateThrottle].
final GestureDragUpdateCallback? onDragSelectionUpdate;
/// Called when a mouse that was previously dragging is released. /// Called when a mouse that was previously dragging is released.
final GestureDragEndCallback? onDragSelectionEnd; final GestureTapDragEndCallback? onDragSelectionEnd;
/// Whether [onUserTap] will be called for all taps including consecutive taps.
///
/// Defaults to false, so [onUserTap] is only called for each distinct tap.
final bool onUserTapAlwaysCalled;
/// How this gesture detector should behave during hit testing. /// How this gesture detector should behave during hit testing.
/// ///
@ -883,210 +900,145 @@ class EditorTextSelectionGestureDetector extends StatefulWidget {
/// Child below this widget. /// Child below this widget.
final Widget child; final Widget child;
final bool detectWordBoundary;
@override @override
State<StatefulWidget> createState() => State<StatefulWidget> createState() =>
_EditorTextSelectionGestureDetectorState(); _EditorTextSelectionGestureDetectorState();
static int getEffectiveConsecutiveTapCount(int rawCount) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
// From observation, these platform's reset their tap count to 0 when
// the number of consecutive taps exceeds 3. For example on Debian Linux
// with GTK, when going past a triple click, on the fourth click the
// selection is moved to the precise click position, on the fifth click
// the word at the position is selected, and on the sixth click the
// paragraph at the position is selected.
return rawCount <= 3
? rawCount
: (rawCount % 3 == 0 ? 3 : rawCount % 3);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
// From observation, these platform's either hold their tap count at 3.
// For example on macOS, when going past a triple click, the selection
// should be retained at the paragraph that was first selected on triple
// click.
return math.min(rawCount, 3);
case TargetPlatform.windows:
// From observation, this platform's consecutive tap actions alternate
// between double click and triple click actions. For example, after a
// triple click has selected a paragraph, on the next click the word at
// the clicked position will be selected, and on the next click the
// paragraph at the position is selected.
return rawCount < 2 ? rawCount : 2 + rawCount % 2;
}
}
} }
class _EditorTextSelectionGestureDetectorState class _EditorTextSelectionGestureDetectorState
extends State<EditorTextSelectionGestureDetector> { extends State<EditorTextSelectionGestureDetector> {
// Counts down for a short duration after a previous tap. Null otherwise. // Converts the details.consecutiveTapCount from a TapAndDrag*Details object,
Timer? _doubleTapTimer; // which can grow to be infinitely large, to a value between 1 and 3. The value
Offset? _lastTapOffset; // that the raw count is converted to is based on the default observed behavior
// on the native platforms.
// True if a second tap down of a double tap is detected. Used to discard //
// subsequent tap up / tap hold of the same tap. // This method should be used in all instances when details.consecutiveTapCount
bool _isDoubleTap = false; // would be used.
// _isDoubleTap for mouse right click void _handleTapTrackStart() {
bool _isSecondaryDoubleTap = false; widget.onTapTrackStart?.call();
}
@override void _handleTapTrackReset() {
void dispose() { widget.onTapTrackReset?.call();
_doubleTapTimer?.cancel();
_dragUpdateThrottleTimer?.cancel();
super.dispose();
} }
// The down handler is force-run on success of a single tap and optimistically // The down handler is force-run on success of a single tap and optimistically
// run before a long press success. // run before a long press success.
void _handleTapDown(TapDownDetails details) { void _handleTapDown(TapDragDownDetails details) {
widget.onTapDown?.call(details); widget.onTapDown?.call(details);
// This isn't detected as a double tap gesture in the gesture recognizer // This isn't detected as a double tap gesture in the gesture recognizer
// because it's 2 single taps, each of which may do different things // because it's 2 single taps, each of which may do different things depending
// depending on whether it's a single tap, the first tap of a double tap, // on whether it's a single tap, the first tap of a double tap, the second
// the second tap held down, a clean double tap etc. // tap held down, a clean double tap etc.
if (_doubleTapTimer != null && if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount(
_isWithinDoubleTapTolerance(details.globalPosition)) { details.consecutiveTapCount) ==
// If there was already a previous tap, the second down hold/tap is a 2) {
// double tap down. return widget.onDoubleTapDown?.call(details);
}
widget.onDoubleTapDown?.call(details);
if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount(
_doubleTapTimer!.cancel(); details.consecutiveTapCount) ==
_doubleTapTimeout(); 3) {
_isDoubleTap = true; return widget.onTripleTapDown?.call(details);
} }
} }
void _handleTapUp(TapUpDetails details) { void _handleTapUp(TapDragUpDetails details) {
if (!_isDoubleTap) { if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount(
details.consecutiveTapCount) ==
1) {
widget.onSingleTapUp?.call(details); widget.onSingleTapUp?.call(details);
_lastTapOffset = details.globalPosition; widget.onUserTap?.call();
_doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); } else if (widget.onUserTapAlwaysCalled) {
widget.onUserTap?.call();
} }
_isDoubleTap = false;
} }
void _handleTapCancel() { void _handleTapCancel() {
widget.onSingleTapCancel?.call(); widget.onSingleTapCancel?.call();
} }
// added secondary tap function for mouse right click to show toolbar void _handleDragStart(TapDragStartDetails details) {
void _handleSecondaryTapDown(TapDownDetails details) {
if (widget.onSecondaryTapDown != null) {
widget.onSecondaryTapDown?.call(details);
}
if (_doubleTapTimer != null &&
_isWithinDoubleTapTolerance(details.globalPosition)) {
widget.onSecondaryDoubleTapDown?.call(details);
_doubleTapTimer!.cancel();
_doubleTapTimeout();
_isDoubleTap = true;
}
}
void _handleSecondaryTapUp(TapUpDetails details) {
if (!_isSecondaryDoubleTap) {
widget.onSecondarySingleTapUp?.call(details);
_lastTapOffset = details.globalPosition;
_doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout);
}
_isSecondaryDoubleTap = false;
}
void _handleSecondaryTapCancel() {
widget.onSecondarySingleTapCancel?.call();
}
DragStartDetails? _lastDragStartDetails;
DragUpdateDetails? _lastDragUpdateDetails;
Timer? _dragUpdateThrottleTimer;
void _handleDragStart(DragStartDetails details) {
assert(_lastDragStartDetails == null);
_lastDragStartDetails = details;
widget.onDragSelectionStart?.call(details); widget.onDragSelectionStart?.call(details);
} }
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(TapDragUpdateDetails details) {
_lastDragUpdateDetails = details; widget.onDragSelectionUpdate?.call(details);
_dragUpdateThrottleTimer ??= Timer(
const Duration(milliseconds: 50),
_handleDragUpdateThrottled,
);
} }
/// Drag updates are being throttled to avoid excessive text layouts in text void _handleDragEnd(TapDragEndDetails details) {
/// fields. The frequency of invocations is controlled by the constant
/// [_kDragSelectionUpdateThrottle].
///
/// Once the drag gesture ends, any pending drag update will be fired
/// immediately. See [_handleDragEnd].
void _handleDragUpdateThrottled() {
assert(_lastDragStartDetails != null);
assert(_lastDragUpdateDetails != null);
if (widget.onDragSelectionUpdate != null) {
widget.onDragSelectionUpdate!(
//_lastDragStartDetails!,
_lastDragUpdateDetails!);
}
_dragUpdateThrottleTimer = null;
_lastDragUpdateDetails = null;
}
void _handleDragEnd(DragEndDetails details) {
assert(_lastDragStartDetails != null);
if (_dragUpdateThrottleTimer != null) {
// If there's already an update scheduled, trigger it immediately and
// cancel the timer.
_dragUpdateThrottleTimer!.cancel();
_handleDragUpdateThrottled();
}
widget.onDragSelectionEnd?.call(details); widget.onDragSelectionEnd?.call(details);
_dragUpdateThrottleTimer = null;
_lastDragStartDetails = null;
_lastDragUpdateDetails = null;
} }
void _forcePressStarted(ForcePressDetails details) { void _forcePressStarted(ForcePressDetails details) {
_doubleTapTimer?.cancel();
_doubleTapTimer = null;
widget.onForcePressStart?.call(details); widget.onForcePressStart?.call(details);
} }
void _forcePressEnded(ForcePressDetails details) { void _forcePressEnded(ForcePressDetails details) {
if (widget.onForcePressEnd != null) { widget.onForcePressEnd?.call(details);
widget.onForcePressEnd?.call(details);
}
} }
void _handleLongPressStart(LongPressStartDetails details) { void _handleLongPressStart(LongPressStartDetails details) {
if (!_isDoubleTap) { if (widget.onSingleLongTapStart != null) {
widget.onSingleLongTapStart?.call(details); widget.onSingleLongTapStart!(details);
} }
} }
void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
if (!_isDoubleTap) { if (widget.onSingleLongTapMoveUpdate != null) {
widget.onSingleLongTapMoveUpdate?.call(details); widget.onSingleLongTapMoveUpdate!(details);
} }
} }
void _handleLongPressEnd(LongPressEndDetails details) { void _handleLongPressEnd(LongPressEndDetails details) {
if (!_isDoubleTap) { if (widget.onSingleLongTapEnd != null) {
widget.onSingleLongTapEnd?.call(details); widget.onSingleLongTapEnd!(details);
} }
_isDoubleTap = false;
}
void _doubleTapTimeout() {
_doubleTapTimer = null;
_lastTapOffset = null;
}
bool _isWithinDoubleTapTolerance(Offset secondTapOffset) {
if (_lastTapOffset == null) {
return false;
}
return (secondTapOffset - _lastTapOffset!).distance <= kDoubleTapSlop;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final gestures = <Type, GestureRecognizerFactory>{}; final gestures = <Type, GestureRecognizerFactory>{};
// Use _TransparentTapGestureRecognizer so that TextSelectionGestureDetector gestures[TapGestureRecognizer] =
// can receive the same tap events that a selection handle placed visually GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
// on top of it also receives. () => TapGestureRecognizer(debugOwner: this),
gestures[_TransparentTapGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<_TransparentTapGestureRecognizer>(
() => _TransparentTapGestureRecognizer(debugOwner: this),
(instance) { (instance) {
instance instance
..onTapDown = _handleTapDown ..onSecondaryTap = widget.onSecondaryTap
..onTapUp = _handleTapUp ..onSecondaryTapDown = widget.onSecondaryTapDown;
..onTapCancel = _handleTapCancel
..onSecondaryTapDown = _handleSecondaryTapDown
..onSecondaryTapUp = _handleSecondaryTapUp
..onSecondaryTapCancel = _handleSecondaryTapCancel;
}, },
); );
@ -1110,21 +1062,51 @@ class _EditorTextSelectionGestureDetectorState
if (widget.onDragSelectionStart != null || if (widget.onDragSelectionStart != null ||
widget.onDragSelectionUpdate != null || widget.onDragSelectionUpdate != null ||
widget.onDragSelectionEnd != null) { widget.onDragSelectionEnd != null) {
gestures[HorizontalDragGestureRecognizer] = switch (defaultTargetPlatform) {
GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>( case TargetPlatform.android:
() => HorizontalDragGestureRecognizer( case TargetPlatform.fuchsia:
debugOwner: this, case TargetPlatform.iOS:
supportedDevices: <PointerDeviceKind>{PointerDeviceKind.mouse}), gestures[TapAndHorizontalDragGestureRecognizer] =
(instance) { GestureRecognizerFactoryWithHandlers<
// Text selection should start from the position of the first pointer TapAndHorizontalDragGestureRecognizer>(
// down event. () => TapAndHorizontalDragGestureRecognizer(debugOwner: this),
instance (instance) {
..dragStartBehavior = DragStartBehavior.down instance
..onStart = _handleDragStart // Text selection should start from the position of the first pointer
..onUpdate = _handleDragUpdate // down event.
..onEnd = _handleDragEnd; ..dragStartBehavior = DragStartBehavior.down
}, ..onTapTrackStart = _handleTapTrackStart
); ..onTapTrackReset = _handleTapTrackReset
..onTapDown = _handleTapDown
..onDragStart = _handleDragStart
..onDragUpdate = _handleDragUpdate
..onDragEnd = _handleDragEnd
..onTapUp = _handleTapUp
..onCancel = _handleTapCancel;
},
);
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
gestures[TapAndPanGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
() => TapAndPanGestureRecognizer(debugOwner: this),
(instance) {
instance
// Text selection should start from the position of the first pointer
// down event.
..dragStartBehavior = DragStartBehavior.down
..onTapTrackStart = _handleTapTrackStart
..onTapTrackReset = _handleTapTrackReset
..onTapDown = _handleTapDown
..onDragStart = _handleDragStart
..onDragUpdate = _handleDragUpdate
..onDragEnd = _handleDragEnd
..onTapUp = _handleTapUp
..onCancel = _handleTapCancel;
},
);
}
} }
if (widget.onForcePressStart != null || widget.onForcePressEnd != null) { if (widget.onForcePressStart != null || widget.onForcePressEnd != null) {
@ -1148,32 +1130,3 @@ class _EditorTextSelectionGestureDetectorState
); );
} }
} }
// A TapGestureRecognizer which allows other GestureRecognizers to win in the
// GestureArena. This means both _TransparentTapGestureRecognizer and other
// GestureRecognizers can handle the same event.
//
// This enables proper handling of events on both the selection handle and the
// underlying input, since there is significant overlap between the two given
// the handle's padded hit area. For example, the selection handle needs to
// handle single taps on itself, but double taps need to be handled by the
// underlying input.
class _TransparentTapGestureRecognizer extends TapGestureRecognizer {
_TransparentTapGestureRecognizer({
super.debugOwner,
});
@override
void rejectGesture(int pointer) {
// Accept new gestures that another recognizer has already won.
// Specifically, this needs to accept taps on the text selection handle on
// behalf of the text field in order to handle double tap to select. It must
// not accept other gestures like longpresses and drags that end outside of
// the text field.
if (state == GestureRecognizerState.ready) {
acceptGesture(pointer);
} else {
super.rejectGesture(pointer);
}
}
}

@ -21,6 +21,7 @@ import 'quill_localizations_hi.dart';
import 'quill_localizations_id.dart'; import 'quill_localizations_id.dart';
import 'quill_localizations_it.dart'; import 'quill_localizations_it.dart';
import 'quill_localizations_ja.dart'; import 'quill_localizations_ja.dart';
import 'quill_localizations_km.dart';
import 'quill_localizations_ko.dart'; import 'quill_localizations_ko.dart';
import 'quill_localizations_ku.dart'; import 'quill_localizations_ku.dart';
import 'quill_localizations_ms.dart'; import 'quill_localizations_ms.dart';
@ -42,6 +43,8 @@ import 'quill_localizations_ur.dart';
import 'quill_localizations_vi.dart'; import 'quill_localizations_vi.dart';
import 'quill_localizations_zh.dart'; import 'quill_localizations_zh.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of FlutterQuillLocalizations /// Callers can lookup localized strings with an instance of FlutterQuillLocalizations
/// returned by `FlutterQuillLocalizations.of(context)`. /// returned by `FlutterQuillLocalizations.of(context)`.
/// ///
@ -144,6 +147,7 @@ abstract class FlutterQuillLocalizations {
Locale('id'), Locale('id'),
Locale('it'), Locale('it'),
Locale('ja'), Locale('ja'),
Locale('km'),
Locale('ko'), Locale('ko'),
Locale('ku'), Locale('ku'),
Locale('ku', 'CKB'), Locale('ku', 'CKB'),
@ -782,6 +786,7 @@ class _FlutterQuillLocalizationsDelegate
'id', 'id',
'it', 'it',
'ja', 'ja',
'km',
'ko', 'ko',
'ku', 'ku',
'ms', 'ms',
@ -889,6 +894,8 @@ FlutterQuillLocalizations lookupFlutterQuillLocalizations(Locale locale) {
return FlutterQuillLocalizationsIt(); return FlutterQuillLocalizationsIt();
case 'ja': case 'ja':
return FlutterQuillLocalizationsJa(); return FlutterQuillLocalizationsJa();
case 'km':
return FlutterQuillLocalizationsKm();
case 'ko': case 'ko':
return FlutterQuillLocalizationsKo(); return FlutterQuillLocalizationsKo();
case 'ku': case 'ku':

@ -1,8 +1,10 @@
import 'quill_localizations.dart'; import 'quill_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Arabic (`ar`). /// The translations for Arabic (`ar`).
class FlutterQuillLocalizationsAr extends FlutterQuillLocalizations { class FlutterQuillLocalizationsAr extends FlutterQuillLocalizations {
FlutterQuillLocalizationsAr([super.locale = 'ar']); FlutterQuillLocalizationsAr([String locale = 'ar']) : super(locale);
@override @override
String get pasteLink => 'نسخ الرابط'; String get pasteLink => 'نسخ الرابط';
@ -235,10 +237,10 @@ class FlutterQuillLocalizationsAr extends FlutterQuillLocalizations {
String get errorWhileSavingImage => 'حدث خطأ أثناء حفظ الصورة'; String get errorWhileSavingImage => 'حدث خطأ أثناء حفظ الصورة';
@override @override
String get pleaseEnterTextForYourLink => "مثال: 'تعلم المزيد'"; String get pleaseEnterTextForYourLink => 'مثال: \'تعلم المزيد\'';
@override @override
String get pleaseEnterTheLinkURL => "مثال: 'https://example.com'"; String get pleaseEnterTheLinkURL => 'مثال: \'https://example.com\'';
@override @override
String get pleaseEnterAValidImageURL => 'الرجاء إدخال عنوان URL صحيح للصورة'; String get pleaseEnterAValidImageURL => 'الرجاء إدخال عنوان URL صحيح للصورة';

@ -1,8 +1,10 @@
import 'quill_localizations.dart'; import 'quill_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Bulgarian (`bg`). /// The translations for Bulgarian (`bg`).
class FlutterQuillLocalizationsBg extends FlutterQuillLocalizations { class FlutterQuillLocalizationsBg extends FlutterQuillLocalizations {
FlutterQuillLocalizationsBg([super.locale = 'bg']); FlutterQuillLocalizationsBg([String locale = 'bg']) : super(locale);
@override @override
String get pasteLink => 'Поставете връзка'; String get pasteLink => 'Поставете връзка';
@ -235,10 +237,10 @@ class FlutterQuillLocalizationsBg extends FlutterQuillLocalizations {
String get errorWhileSavingImage => 'Error while saving image'; String get errorWhileSavingImage => 'Error while saving image';
@override @override
String get pleaseEnterTextForYourLink => "Например, 'Научете повече'"; String get pleaseEnterTextForYourLink => 'Например, \'Научете повече\'';
@override @override
String get pleaseEnterTheLinkURL => "Например, 'https://example.com'"; String get pleaseEnterTheLinkURL => 'Например, \'https://example.com\'';
@override @override
String get pleaseEnterAValidImageURL => String get pleaseEnterAValidImageURL =>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save