From 100566104847bff564d1cdb8e37073370e1f6e4b Mon Sep 17 00:00:00 2001 From: Mmisiek Date: Thu, 11 May 2023 07:35:57 -0700 Subject: [PATCH 01/36] Fixed issues from last Flutter update. (#1210) --- lib/src/widgets/delegate.dart | 3 ++- lib/src/widgets/raw_editor.dart | 2 ++ lib/src/widgets/text_selection.dart | 5 +++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/src/widgets/delegate.dart b/lib/src/widgets/delegate.dart index de92d8eb..988473bd 100644 --- a/lib/src/widgets/delegate.dart +++ b/lib/src/widgets/delegate.dart @@ -312,7 +312,8 @@ class EditorTextSelectionGestureDetectorBuilder { /// which triggers this callback./lib/src/material/text_field.dart @protected void onDragSelectionUpdate( - DragStartDetails startDetails, DragUpdateDetails updateDetails) { + //DragStartDetails startDetails, + DragUpdateDetails updateDetails) { renderEditor!.extendSelection(updateDetails.globalPosition, cause: SelectionChangedCause.drag); } diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 89831d96..7d60a051 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -325,6 +325,8 @@ class RawEditorState extends EditorState TextDirection get _textDirection => Directionality.of(context); + void insertContent(KeyboardInsertedContent content) {} + /// Returns the [ContextMenuButtonItem]s representing the buttons in this /// platform's default selection menu for [RawEditor]. /// diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index ad1398e0..688505e2 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -713,7 +713,7 @@ class EditorTextSelectionGestureDetector extends StatefulWidget { /// The frequency of calls is throttled to avoid excessive text layout /// operations in text fields. The throttling is controlled by the constant /// [_kDragSelectionUpdateThrottle]. - final DragSelectionUpdateCallback? onDragSelectionUpdate; + final GestureDragUpdateCallback? onDragSelectionUpdate; /// Called when a mouse that was previously dragging is released. final GestureDragEndCallback? onDragSelectionEnd; @@ -857,7 +857,8 @@ class _EditorTextSelectionGestureDetectorState assert(_lastDragUpdateDetails != null); if (widget.onDragSelectionUpdate != null) { widget.onDragSelectionUpdate!( - _lastDragStartDetails!, _lastDragUpdateDetails!); + //_lastDragStartDetails!, + _lastDragUpdateDetails!); } _dragUpdateThrottleTimer = null; _lastDragUpdateDetails = null; From 0a3fe3f63c4636c68de1e44523eae3b9de787dcb Mon Sep 17 00:00:00 2001 From: Cheryl Date: Thu, 11 May 2023 12:51:46 -0700 Subject: [PATCH 02/36] Upgrade to 7.1.18 --- CHANGELOG.md | 5 ++++- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46b9b977..a688982f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -# [7.1.17] +# [7.1.18] +- Support flutter latest version. + +# [7.1.17+1] - Updates `device_info_plus` to version 9.0.0 to benefit from AGP 8 (see [changelog#900](https://pub.dev/packages/device_info_plus/changelog#900)). # [7.1.16] diff --git a/pubspec.yaml b/pubspec.yaml index 0a369e47..88c1e2b3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.1.17+1 +version: 7.1.18 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 3b43baf229e4db8fe72bb7a7faa82773c8cca5b0 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Thu, 11 May 2023 12:53:31 -0700 Subject: [PATCH 03/36] Fix analysis error --- lib/src/widgets/raw_editor.dart | 3 ++- lib/src/widgets/text_block.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 7d60a051..b80aa1b2 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -325,6 +325,7 @@ class RawEditorState extends EditorState TextDirection get _textDirection => Directionality.of(context); + @override void insertContent(KeyboardInsertedContent content) {} /// Returns the [ContextMenuButtonItem]s representing the buttons in this @@ -1711,7 +1712,7 @@ class RawEditorState extends EditorState } class _Editor extends MultiChildRenderObjectWidget { - _Editor({ + const _Editor({ required Key key, required List children, required this.document, diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index b61ad9cf..f30ccea0 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -596,7 +596,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } class _EditableBlock extends MultiChildRenderObjectWidget { - _EditableBlock( + const _EditableBlock( {required this.block, required this.textDirection, required this.padding, diff --git a/pubspec.yaml b/pubspec.yaml index 88c1e2b3..479b2115 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=2.17.0 <4.0.0" flutter: ">=3.0.0" dependencies: From c35170dcd45a4976237c3f2136a5c4e51049e240 Mon Sep 17 00:00:00 2001 From: Jon Salmon <26483285+Jon-Salmon@users.noreply.github.com> Date: Sat, 13 May 2023 00:38:15 +0100 Subject: [PATCH 04/36] Allow use of latest i18n dependencies (#1215) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 479b2115..4c4b023a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: pedantic: ^1.11.1 characters: ^1.2.1 diff_match_patch: ^0.4.1 - i18n_extension: ^8.0.0 + i18n_extension: ">=8.0.0 <10.0.0" device_info_plus: ^9.0.0 platform: ^3.1.0 pasteboard: ^0.2.0 From e06c3f32f66193c533dd4c0c966bb4ffe6e37f5e Mon Sep 17 00:00:00 2001 From: rodrigo-itao <66178504+rodrigo-itao@users.noreply.github.com> Date: Wed, 17 May 2023 13:42:34 -0300 Subject: [PATCH 05/36] Update pubspec.yaml (#1221) --- flutter_quill_extensions/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index f03c97c7..2c132290 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: video_player: ^2.4.2 youtube_player_flutter: ^8.1.1 gallery_saver: ^2.3.2 - math_keyboard: ^0.1.8 + math_keyboard: ^0.2.0 string_validator: ^1.0.0 universal_html: ^2.2.1 url_launcher: ^6.1.9 From 5ae6e824fa677d0d6ba5dac9035c803125bb5390 Mon Sep 17 00:00:00 2001 From: Firas Abd Alrahman Date: Wed, 17 May 2023 21:05:22 +0300 Subject: [PATCH 06/36] Fix Rtl leading alignment problem (#1222) --- lib/src/widgets/raw_editor.dart | 2 +- lib/src/widgets/text_line.dart | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index b80aa1b2..444c49dd 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -891,7 +891,7 @@ class RawEditorState extends EditorState final editableTextBlock = EditableTextBlock( block: node, controller: controller, - textDirection: _textDirection, + textDirection: getDirectionOfNode(node), scrollBottomInset: widget.scrollBottomInset, verticalSpacing: _getVerticalSpacingForBlock(node, _styles), textSelection: controller.selection, diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index f38cbebf..467fddb2 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -1082,9 +1082,16 @@ class RenderEditableTextLine extends RenderEditableBox { @override void paint(PaintingContext context, Offset offset) { if (_leading != null) { - final parentData = _leading!.parentData as BoxParentData; - final effectiveOffset = offset + parentData.offset; - context.paintChild(_leading!, effectiveOffset); + if (textDirection == TextDirection.ltr) { + final parentData = _leading!.parentData as BoxParentData; + final effectiveOffset = offset + parentData.offset; + context.paintChild(_leading!, effectiveOffset); + } else { + final parentData = _leading!.parentData as BoxParentData; + final effectiveOffset = offset + parentData.offset; + context.paintChild(_leading!, + Offset(size.width - _leading!.size.width, effectiveOffset.dy)); + } } if (_body != null) { From 5cd0785ce4ce976cc7fc283cd68664b9c4069938 Mon Sep 17 00:00:00 2001 From: Jonathan Salmon Date: Thu, 18 May 2023 02:23:17 +0100 Subject: [PATCH 07/36] flutter_quill_extensions release 0.3.2 --- flutter_quill_extensions/CHANGELOG.md | 3 +++ flutter_quill_extensions/pubspec.yaml | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index b741b21a..3bb1abe4 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.3.2 +* Updated dependencies to support intl 0.18 + ## 0.3.1 * Image embedding tweaks * Add MediaButton which is intened to superseed the ImageButton and VideoButton. Only image selection is working. diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index 2c132290..a5f88c0d 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill_extensions description: Embed extensions for flutter_quill including image, video, formula and etc. -version: 0.3.1 +version: 0.3.2 homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions @@ -19,7 +19,7 @@ dependencies: video_player: ^2.4.2 youtube_player_flutter: ^8.1.1 gallery_saver: ^2.3.2 - math_keyboard: ^0.2.0 + math_keyboard: ">=0.1.8 <0.3.0" string_validator: ^1.0.0 universal_html: ^2.2.1 url_launcher: ^6.1.9 From 140e77bedf1fe79c795cc2a6d4cb12740229f7f9 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Wed, 17 May 2023 19:00:51 -0700 Subject: [PATCH 08/36] Upgrade to 7.1.19 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a688982f..9f3c96b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [7.1.19] +- Fix Rtl leading alignment problem. + # [7.1.18] - Support flutter latest version. diff --git a/pubspec.yaml b/pubspec.yaml index 4c4b023a..1122854b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.1.18 +version: 7.1.19 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 494be80045b5a5d8dbcc15d19212337a6c5e4ca3 Mon Sep 17 00:00:00 2001 From: Jonathan Salmon Date: Thu, 18 May 2023 03:27:36 +0100 Subject: [PATCH 09/36] Updated supported dart versions in flutter_quill_extensions --- flutter_quill_extensions/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index a5f88c0d..c58b0d24 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=2.17.0 <4.0.0" flutter: ">=3.0.0" dependencies: From 99d60bc1b7da2a2bdc904c6921348959e8d7e780 Mon Sep 17 00:00:00 2001 From: Abdurrahman Adel <47070231+Abdurrahman98XX@users.noreply.github.com> Date: Sun, 21 May 2023 12:14:29 +0300 Subject: [PATCH 10/36] Update Arabic language of the toolbar.i18n.dart (#1225) --- lib/src/translations/toolbar.i18n.dart | 110 +++++++++++++------------ 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 5eb2b344..77c0bd67 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -139,67 +139,69 @@ extension Localization on String { 'Paste a link': 'نسخ الرابط', 'Ok': 'نعم', 'Select Color': 'اختار اللون', - 'Gallery': 'الصور', + 'Gallery': 'المعرض', 'Link': 'الرابط', 'Please first select some text to transform into a link.': 'يرجى اختيار نص للتحويل إلى رابط', 'Open': 'فتح', - 'Copy': 'ينسخ', + 'Copy': 'نسخ', 'Remove': 'إزالة', - 'Save': 'يحفظ', + 'Save': 'حفظ', 'Zoom': 'تكبير', - 'Saved': 'أنقذ', - 'Text': 'Text', - 'What is entered is not a link': 'What is entered is not a link', - 'Resize': 'Resize', - 'Width': 'Width', - 'Height': 'Height', - 'Size': 'Size', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'matches': 'matches', - 'showing match': 'showing match', - 'Prev': 'Prev', - 'Next': 'Next', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', + 'Saved': 'تم الحفظ', + 'Text': 'نص', + 'What is entered is not a link': 'ما تم ادخاله ليس رابط', + 'Resize': 'تحجيم', + 'Width': 'عرض', + 'Height': 'ارتفاع', + 'Size': 'حجم', + 'Small': 'صغير', + 'Large': 'كبير', + 'Huge': 'ضخم', + 'Clear': 'تنظيف', + 'Font': 'خط', + 'Search': 'بحث', + 'matches': 'تطابق', + 'showing match': 'عرض التطابق', + 'Prev': 'سابق', + 'Next': 'تالي', + 'Camera': 'كاميرا', + 'Video': 'فيديو', + 'Undo': 'تراجع', + 'Redo': 'تقدم', + 'Font family': 'عائلة الخط', + 'Font size': 'حجم الخط', + 'Bold': 'عريض', + 'Subscript': 'نص سفلي', + 'Superscript': 'نص علوي', + 'Italic': 'مائل', + 'Underline': 'تحته خط', + 'Strike through': 'داخله خط', + 'Inline code': 'كود بوسط السطر', + 'Font color': 'لون الخط', + 'Background color': 'لون الخلفية', + 'Clear format': 'تنظيف التنسيق', + 'Align left': 'محاذاة اليسار', + 'Align center': 'محاذاة الوسط', + 'Align right': 'محاذاة اليمين', + // i think it should be 'Justify with width' + // it is wrong in all properties 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', + 'Text direction': 'اتجاه النص', + 'Header style': 'ستايل العنوان', + 'Numbered list': 'قائمة مرقمة', + 'Bullet list': 'قائمة منقطة', + 'Checked list': 'قائمة للمهام', + 'Code block': 'كود كامل', + 'Quote': 'اقتباس', + 'Increase indent': 'زيادة الهامش', + 'Decrease indent': 'تنقيص الهامش', + 'Insert URL': 'ادخل عنوان رابط', + 'Visit link': 'زيارة الرابط', + 'Enter link': 'ادخل رابط', + 'Enter media': 'ادخل وسائط', + 'Edit': 'تعديل', + 'Apply': 'تطبيق', }, 'da': { 'Paste a link': 'Indsæt link', From f56d5c36dc93d3f92d301b902674ea0d6024bd88 Mon Sep 17 00:00:00 2001 From: Firas Abd Alrahman Date: Wed, 24 May 2023 03:46:56 +0300 Subject: [PATCH 11/36] pass linestyle to embeded block (#1230) --- lib/src/widgets/embeds.dart | 1 + lib/src/widgets/text_line.dart | 12 ++++-------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/src/widgets/embeds.dart b/lib/src/widgets/embeds.dart index 8d72cd5b..565ee778 100644 --- a/lib/src/widgets/embeds.dart +++ b/lib/src/widgets/embeds.dart @@ -21,6 +21,7 @@ abstract class EmbedBuilder { leaf.Embed node, bool readOnly, bool inline, + TextStyle textStyle, ); } diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 467fddb2..ed77f79e 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -148,15 +148,10 @@ class _TextLineState extends State { final embedBuilder = widget.embedBuilder(embed); if (embedBuilder.expanded) { // Creates correct node for custom embed - + final lineStyle = _getLineStyle(widget.styles); return EmbedProxy( - embedBuilder.build( - context, - widget.controller, - embed, - widget.readOnly, - false, - ), + embedBuilder.build(context, widget.controller, embed, widget.readOnly, + false, lineStyle), ); } } @@ -208,6 +203,7 @@ class _TextLineState extends State { child, widget.readOnly, true, + lineStyle, ), ); final embed = embedBuilder.buildWidgetSpan(embedWidget); From a58ca0abbbc7dbfe012a7e786efc50536610d5c9 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Tue, 23 May 2023 18:00:39 -0700 Subject: [PATCH 12/36] Upgrade to 7.1.20 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3c96b0..a6b78d0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [7.1.20] +- Pass linestyle to embedded block. + # [7.1.19] - Fix Rtl leading alignment problem. diff --git a/pubspec.yaml b/pubspec.yaml index 1122854b..e1562d97 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.1.19 +version: 7.1.20 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 6c6217343d36c13fe9c9704b293d2848d742d999 Mon Sep 17 00:00:00 2001 From: Firas Abd Alrahman Date: Wed, 24 May 2023 07:39:19 +0300 Subject: [PATCH 13/36] keep style when creating embedded element (#1232) --- lib/src/widgets/text_line.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index ed77f79e..9358777a 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -193,7 +193,8 @@ class _TextLineState extends State { } // Creates correct node for custom embed if (child.value.type == BlockEmbed.customType) { - child = Embed(CustomBlockEmbed.fromJsonString(child.value.data)); + child = Embed(CustomBlockEmbed.fromJsonString(child.value.data)) + ..applyStyle(child.style); } final embedBuilder = widget.embedBuilder(child); final embedWidget = EmbedProxy( From fb890017695f4d4e40c4ae5d43bf4093bf6664b0 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 24 May 2023 14:55:49 +0100 Subject: [PATCH 14/36] Scale leading widgets based on paragraph font size (#1226) --- CHANGELOG.md | 3 ++ example/windows/runner/Runner.rc | 10 ++--- flutter_quill_extensions/pubspec.yaml | 2 +- .../widgets/style_widgets/bullet_point.dart | 4 +- lib/src/widgets/text_block.dart | 41 +++++++++++-------- pubspec.yaml | 2 +- 6 files changed, 36 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b78d0f..ffbb02ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [7.2.0] +- Checkboxes, bullet points, and number points are now scaled based on the default paragraph font size. + # [7.1.20] - Pass linestyle to embedded block. diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc index 3ac0062f..a922e84e 100644 --- a/example/windows/runner/Runner.rc +++ b/example/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index c58b0d24..d57cd8bf 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: video_player: ^2.4.2 youtube_player_flutter: ^8.1.1 gallery_saver: ^2.3.2 - math_keyboard: ">=0.1.8 <0.3.0" + math_keyboard: ^0.2.0 string_validator: ^1.0.0 universal_html: ^2.2.1 url_launcher: ^6.1.9 diff --git a/lib/src/widgets/style_widgets/bullet_point.dart b/lib/src/widgets/style_widgets/bullet_point.dart index ee33c93a..8b5fce70 100644 --- a/lib/src/widgets/style_widgets/bullet_point.dart +++ b/lib/src/widgets/style_widgets/bullet_point.dart @@ -4,18 +4,20 @@ class QuillBulletPoint extends StatelessWidget { const QuillBulletPoint({ required this.style, required this.width, + this.padding = 0, Key? key, }) : super(key: key); final TextStyle style; final double width; + final double padding; @override Widget build(BuildContext context) { return Container( alignment: AlignmentDirectional.topEnd, width: width, - padding: const EdgeInsetsDirectional.only(end: 13), + padding: EdgeInsetsDirectional.only(end: padding), child: Text('•', style: style), ); } diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index f30ccea0..4adaac37 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -152,7 +152,7 @@ class EditableTextBlock extends StatelessWidget { onLaunchUrl: onLaunchUrl, customLinkPrefixes: customLinkPrefixes, ), - _getIndentWidth(), + _getIndentWidth(context), _getSpacingForLine(line, index, count, defaultStyles), textDirection, textSelection, @@ -170,45 +170,48 @@ class EditableTextBlock extends StatelessWidget { Widget? _buildLeading(BuildContext context, Line line, int index, Map indentLevelCounts, int count) { - final defaultStyles = QuillStyles.getStyles(context, false); + final defaultStyles = QuillStyles.getStyles(context, false)!; + final fontSize = defaultStyles.paragraph?.style.fontSize ?? 16; final attrs = line.style.attributes; + if (attrs[Attribute.list.key] == Attribute.ol) { return QuillNumberPoint( index: index, indentLevelCounts: indentLevelCounts, count: count, - style: defaultStyles!.leading!.style, + style: defaultStyles.leading!.style, attrs: attrs, - width: 32, - padding: 8, + width: fontSize * 2, + padding: fontSize / 2, ); } if (attrs[Attribute.list.key] == Attribute.ul) { return QuillBulletPoint( style: - defaultStyles!.leading!.style.copyWith(fontWeight: FontWeight.bold), - width: 32, + defaultStyles.leading!.style.copyWith(fontWeight: FontWeight.bold), + width: fontSize * 2, + padding: fontSize / 2, ); } if (attrs[Attribute.list.key] == Attribute.checked) { return CheckboxPoint( - size: 14, + size: fontSize, value: true, enabled: !readOnly, onChanged: (checked) => onCheckboxTap(line.documentOffset, checked), - uiBuilder: defaultStyles?.lists?.checkboxUIBuilder, + uiBuilder: defaultStyles.lists?.checkboxUIBuilder, ); } if (attrs[Attribute.list.key] == Attribute.unchecked) { return CheckboxPoint( - size: 14, + size: fontSize, value: false, enabled: !readOnly, onChanged: (checked) => onCheckboxTap(line.documentOffset, checked), - uiBuilder: defaultStyles?.lists?.checkboxUIBuilder, + uiBuilder: defaultStyles.lists?.checkboxUIBuilder, ); } @@ -217,35 +220,37 @@ class EditableTextBlock extends StatelessWidget { index: index, indentLevelCounts: indentLevelCounts, count: count, - style: defaultStyles!.code!.style + style: defaultStyles.code!.style .copyWith(color: defaultStyles.code!.style.color!.withOpacity(0.4)), - width: 32, + width: fontSize * 2, attrs: attrs, - padding: 16, + padding: fontSize, withDot: false, ); } return null; } - double _getIndentWidth() { + double _getIndentWidth(BuildContext context) { + final defaultStyles = QuillStyles.getStyles(context, false)!; + final fontSize = defaultStyles.paragraph?.style.fontSize ?? 16; final attrs = block.style.attributes; final indent = attrs[Attribute.indent.key]; var extraIndent = 0.0; if (indent != null && indent.value != null) { - extraIndent = 16.0 * indent.value; + extraIndent = fontSize * indent.value; } if (attrs.containsKey(Attribute.blockQuote.key)) { - return 16.0 + extraIndent; + return fontSize + extraIndent; } var baseIndent = 0.0; if (attrs.containsKey(Attribute.list.key) || attrs.containsKey(Attribute.codeBlock.key)) { - baseIndent = 32.0; + baseIndent = fontSize * 2; } return baseIndent + extraIndent; diff --git a/pubspec.yaml b/pubspec.yaml index e1562d97..d2ab913a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.1.20 +version: 7.2.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From df3afd3f37bf3e1b7124fb2b8f7292f7d8e6bdc0 Mon Sep 17 00:00:00 2001 From: MacDeveloper1 <130981115+MacDeveloper1@users.noreply.github.com> Date: Wed, 24 May 2023 15:56:19 +0200 Subject: [PATCH 15/36] Fix PR #1230 (#1233) --- example/lib/pages/home_page.dart | 1 + example/lib/universal_ui/universal_ui.dart | 2 ++ flutter_quill_extensions/CHANGELOG.md | 3 +++ flutter_quill_extensions/lib/embeds/builders.dart | 4 ++++ flutter_quill_extensions/pubspec.yaml | 4 ++-- 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 40449aeb..11baf428 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -502,6 +502,7 @@ class NotesEmbedBuilder extends EmbedBuilder { Embed node, bool readOnly, bool inline, + TextStyle textStyle, ) { final notes = NotesBlockEmbed(node.value.data).document; diff --git a/example/lib/universal_ui/universal_ui.dart b/example/lib/universal_ui/universal_ui.dart index 7b35f1f1..91344afb 100644 --- a/example/lib/universal_ui/universal_ui.dart +++ b/example/lib/universal_ui/universal_ui.dart @@ -38,6 +38,7 @@ class ImageEmbedBuilderWeb extends EmbedBuilder { Embed node, bool readOnly, bool inline, + TextStyle textStyle, ) { final imageUrl = node.value.data; if (isImageBase64(imageUrl)) { @@ -80,6 +81,7 @@ class VideoEmbedBuilderWeb extends EmbedBuilder { Embed node, bool readOnly, bool inline, + TextStyle textStyle, ) { var videoUrl = node.value.data; if (videoUrl.contains('youtube.com') || videoUrl.contains('youtu.be')) { diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index 3bb1abe4..176876c7 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.3.3 +* Fix a prototype bug which was bring by [PR #1230](https://github.com/singerdmx/flutter-quill/pull/1230#issuecomment-1560597099) + ## 0.3.2 * Updated dependencies to support intl 0.18 diff --git a/flutter_quill_extensions/lib/embeds/builders.dart b/flutter_quill_extensions/lib/embeds/builders.dart index cf04a462..b16b5845 100644 --- a/flutter_quill_extensions/lib/embeds/builders.dart +++ b/flutter_quill_extensions/lib/embeds/builders.dart @@ -28,6 +28,7 @@ class ImageEmbedBuilder extends EmbedBuilder { base.Embed node, bool readOnly, bool inline, + TextStyle textStyle, ) { assert(!kIsWeb, 'Please provide image EmbedBuilder for Web'); @@ -164,6 +165,7 @@ class ImageEmbedBuilderWeb extends EmbedBuilder { Embed node, bool readOnly, bool inline, + TextStyle textStyle, ) { final imageUrl = node.value.data; @@ -198,6 +200,7 @@ class VideoEmbedBuilder extends EmbedBuilder { base.Embed node, bool readOnly, bool inline, + TextStyle textStyle, ) { assert(!kIsWeb, 'Please provide video EmbedBuilder for Web'); @@ -226,6 +229,7 @@ class FormulaEmbedBuilder extends EmbedBuilder { base.Embed node, bool readOnly, bool inline, + TextStyle textStyle, ) { assert(!kIsWeb, 'Please provide formula EmbedBuilder for Web'); diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index d57cd8bf..a1ce333f 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill_extensions description: Embed extensions for flutter_quill including image, video, formula and etc. -version: 0.3.2 +version: 0.3.3 homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions @@ -12,7 +12,7 @@ dependencies: flutter: sdk: flutter - flutter_quill: ^7.1.17 + flutter_quill: ^7.1.20 image_picker: ^0.8.5+3 photo_view: ^0.14.0 From c1bef0ae4f357a3d4e97041ea4b1ef4b0fb8b669 Mon Sep 17 00:00:00 2001 From: Richard Marshall Date: Thu, 25 May 2023 14:56:21 -0700 Subject: [PATCH 16/36] Add support for android keyboard content insertion (#1236) --- lib/src/widgets/editor.dart | 7 +++++++ lib/src/widgets/raw_editor.dart | 13 ++++++++++++- .../raw_editor_state_text_input_client_mixin.dart | 3 +++ pubspec.yaml | 2 +- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 906b67fb..c946e0b7 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -187,6 +187,7 @@ class QuillEditor extends StatefulWidget { this.enableUnfocusOnTapOutside = true, this.customLinkPrefixes = const [], this.dialogTheme, + this.contentInsertionConfiguration, Key? key, }) : super(key: key); @@ -427,6 +428,11 @@ class QuillEditor extends StatefulWidget { /// Configures the dialog theme. final QuillDialogTheme? dialogTheme; + /// Configuration of handler for media content inserted via the system input method. + /// + /// See [https://api.flutter.dev/flutter/widgets/EditableText/contentInsertionConfiguration.html] + final ContentInsertionConfiguration? contentInsertionConfiguration; + @override QuillEditorState createState() => QuillEditorState(); } @@ -528,6 +534,7 @@ class QuillEditorState extends State customLinkPrefixes: widget.customLinkPrefixes, enableUnfocusOnTapOutside: widget.enableUnfocusOnTapOutside, dialogTheme: widget.dialogTheme, + contentInsertionConfiguration: widget.contentInsertionConfiguration, ); final editor = I18n( diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 444c49dd..fbf5528c 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -84,6 +84,7 @@ class RawEditor extends StatefulWidget { this.onImagePaste, this.customLinkPrefixes = const [], this.dialogTheme, + this.contentInsertionConfiguration, }) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'), assert(maxHeight == null || minHeight == null || maxHeight >= minHeight, @@ -270,6 +271,11 @@ class RawEditor extends StatefulWidget { /// Configures the dialog theme. final QuillDialogTheme? dialogTheme; + /// Configuration of handler for media content inserted via the system input method. + /// + /// See [https://api.flutter.dev/flutter/widgets/EditableText/contentInsertionConfiguration.html] + final ContentInsertionConfiguration? contentInsertionConfiguration; + @override State createState() => RawEditorState(); } @@ -326,7 +332,12 @@ class RawEditorState extends EditorState TextDirection get _textDirection => Directionality.of(context); @override - void insertContent(KeyboardInsertedContent content) {} + void insertContent(KeyboardInsertedContent content) { + assert(widget.contentInsertionConfiguration?.allowedMimeTypes + .contains(content.mimeType) ?? + false); + widget.contentInsertionConfiguration?.onContentInserted.call(content); + } /// Returns the [ContextMenuButtonItem]s representing the buttons in this /// platform's default selection menu for [RawEditor]. diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index e3b9ff5f..f3cec425 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -59,6 +59,9 @@ mixin RawEditorStateTextInputClientMixin on EditorState enableSuggestions: !widget.readOnly, keyboardAppearance: widget.keyboardAppearance, textCapitalization: widget.textCapitalization, + allowedMimeTypes: widget.contentInsertionConfiguration == null + ? const [] + : widget.contentInsertionConfiguration!.allowedMimeTypes, ), ); diff --git a/pubspec.yaml b/pubspec.yaml index d2ab913a..b6403a01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ repository: https://github.com/singerdmx/flutter-quill environment: sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + flutter: ">=3.10.0" dependencies: flutter: From dbcc06a9d6743cafdd387765b6a38b06a31f8d01 Mon Sep 17 00:00:00 2001 From: Firas Abd Alrahman Date: Sun, 28 May 2023 02:51:12 +0300 Subject: [PATCH 17/36] enhance color picker, enter hex color and color palette option (#1234) --- lib/src/translations/toolbar.i18n.dart | 9 ++ lib/src/widgets/toolbar/color_button.dart | 152 +++++++++++++++++++--- 2 files changed, 145 insertions(+), 16 deletions(-) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 77c0bd67..2e0cdca0 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -68,6 +68,9 @@ extension Localization on String { 'Enter media': 'Enter media', 'Edit': 'Edit', 'Apply': 'Apply', + 'Hex': 'Hex', + 'Material': 'Material', + 'Color': 'Color', }, 'en_us': { 'Paste a link': 'Paste a link', @@ -134,6 +137,9 @@ extension Localization on String { 'Enter media': 'Enter media', 'Edit': 'Edit', 'Apply': 'Apply', + 'Hex': 'Hex', + 'Material': 'Material', + 'Color': 'Color', }, 'ar': { 'Paste a link': 'نسخ الرابط', @@ -202,6 +208,9 @@ extension Localization on String { 'Enter media': 'ادخل وسائط', 'Edit': 'تعديل', 'Apply': 'تطبيق', + 'Hex': 'Hex', + 'Material': 'Material', + 'Color': 'اللون', }, 'da': { 'Paste a link': 'Indsæt link', diff --git a/lib/src/widgets/toolbar/color_button.dart b/lib/src/widgets/toolbar/color_button.dart index 60f0a591..65139629 100644 --- a/lib/src/widgets/toolbar/color_button.dart +++ b/lib/src/widgets/toolbar/color_button.dart @@ -136,29 +136,149 @@ class _ColorButtonState extends State { } void _changeColor(BuildContext context, Color color) { - var hex = color.value.toRadixString(16); - if (hex.startsWith('ff')) { - hex = hex.substring(2); - } + var hex = colorToHex(color); hex = '#$hex'; widget.controller.formatSelection( widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex)); - Navigator.of(context).pop(); } void _showColorPicker() { - showDialog( + var pickerType = 'material'; + + var selectedColor = Colors.black; + + if (_isToggledColor) { + selectedColor = widget.background + ? hexToColor(_selectionStyle.attributes['background']?.value) + : hexToColor(_selectionStyle.attributes['color']?.value); + } + + final hexController = + TextEditingController(text: colorToHex(selectedColor)); + late void Function(void Function()) colorBoxSetState; + + showDialog( context: context, - builder: (context) => AlertDialog( - title: Text('Select Color'.i18n), - backgroundColor: Theme.of(context).canvasColor, - content: SingleChildScrollView( - child: MaterialPicker( - pickerColor: const Color(0x00000000), - onColorChanged: (color) => _changeColor(context, color), - ), - ), - ), + builder: (context) => StatefulBuilder(builder: (context, dlgSetState) { + return AlertDialog( + title: Text('Select Color'.i18n), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text('OK'.i18n)), + ], + backgroundColor: Theme.of(context).canvasColor, + content: SizedBox( + height: 400, + child: Column( + children: [ + Row( + children: [ + TextButton( + onPressed: () { + dlgSetState(() { + pickerType = 'material'; + }); + }, + child: Text('Material'.i18n)), + TextButton( + onPressed: () { + dlgSetState(() { + pickerType = 'color'; + }); + }, + child: Text('Color'.i18n)), + ], + ), + Expanded( + child: Column(children: [ + if (pickerType == 'material') + MaterialPicker( + pickerColor: selectedColor, + onColorChanged: (color) { + _changeColor(context, color); + Navigator.of(context).pop(); + }, + ), + if (pickerType == 'color') + ColorPicker( + pickerColor: selectedColor, + onColorChanged: (color) { + _changeColor(context, color); + hexController.text = colorToHex(color); + selectedColor = color; + colorBoxSetState(() {}); + }, + ), + const SizedBox( + height: 10, + ), + Row( + children: [ + SizedBox( + width: 100, + height: 60, + child: TextFormField( + controller: hexController, + onChanged: (value) { + selectedColor = hexToColor(value); + _changeColor(context, selectedColor); + + colorBoxSetState(() {}); + }, + decoration: InputDecoration( + labelText: 'Hex'.i18n, + border: const OutlineInputBorder(), + ), + ), + ), + const SizedBox( + width: 10, + ), + StatefulBuilder(builder: (context, mcolorBoxSetState) { + colorBoxSetState = mcolorBoxSetState; + return Container( + width: 25, + height: 25, + decoration: BoxDecoration( + border: Border.all( + color: Colors.black45, + ), + color: selectedColor, + borderRadius: BorderRadius.circular(5), + ), + ); + }), + ], + ), + ])) + ], + ), + )); + }), ); } + + Color hexToColor(String? hexString) { + if (hexString == null) { + return Colors.black; + } + final hexRegex = RegExp(r'([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$'); + + hexString = hexString.replaceAll('#', ''); + if (!hexRegex.hasMatch(hexString)) { + return Colors.black; + } + + final buffer = StringBuffer(); + if (hexString.length == 6 || hexString.length == 7) buffer.write('ff'); + buffer.write(hexString); + return Color(int.tryParse(buffer.toString(), radix: 16) ?? 0xFF000000); + } + + String colorToHex(Color color) { + return color.value.toRadixString(16).padLeft(8, '0').toUpperCase(); + } } From f1b60d83b82453c9f4bd29df10444deb7133beed Mon Sep 17 00:00:00 2001 From: Cheryl Date: Sat, 27 May 2023 17:36:12 -0700 Subject: [PATCH 18/36] Upgrade to 7.2.1 --- CHANGELOG.md | 4 ++++ .../raw_editor/raw_editor_state_text_input_client_mixin.dart | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffbb02ed..639f46e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [7.2.1] +- Add support for android keyboard content insertion. +- Enhance color picker, enter hex color and color palette option. + # [7.2.0] - Checkboxes, bullet points, and number points are now scaled based on the default paragraph font size. diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index f3cec425..fff4c5ac 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -60,8 +60,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState keyboardAppearance: widget.keyboardAppearance, textCapitalization: widget.textCapitalization, allowedMimeTypes: widget.contentInsertionConfiguration == null - ? const [] - : widget.contentInsertionConfiguration!.allowedMimeTypes, + ? const [] + : widget.contentInsertionConfiguration!.allowedMimeTypes, ), ); diff --git a/pubspec.yaml b/pubspec.yaml index b6403a01..f105b378 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.2.0 +version: 7.2.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 94da3cfd4687fa465fd480908d9ba3c2a9f0091e Mon Sep 17 00:00:00 2001 From: Cheryl Date: Sat, 27 May 2023 17:37:04 -0700 Subject: [PATCH 19/36] Fix lint error --- lib/src/widgets/editor.dart | 3 ++- lib/src/widgets/raw_editor.dart | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index c946e0b7..aefa21dc 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -428,7 +428,8 @@ class QuillEditor extends StatefulWidget { /// Configures the dialog theme. final QuillDialogTheme? dialogTheme; - /// Configuration of handler for media content inserted via the system input method. + /// Configuration of handler for media content inserted via the system input + /// method. /// /// See [https://api.flutter.dev/flutter/widgets/EditableText/contentInsertionConfiguration.html] final ContentInsertionConfiguration? contentInsertionConfiguration; diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index fbf5528c..657c63f7 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -271,7 +271,8 @@ class RawEditor extends StatefulWidget { /// Configures the dialog theme. final QuillDialogTheme? dialogTheme; - /// Configuration of handler for media content inserted via the system input method. + /// Configuration of handler for media content inserted via the system input + /// method. /// /// See [https://api.flutter.dev/flutter/widgets/EditableText/contentInsertionConfiguration.html] final ContentInsertionConfiguration? contentInsertionConfiguration; From 4dd4212b9ae0f4c2c84f213fe0ddd802c7d502db Mon Sep 17 00:00:00 2001 From: Oxana Kostikova Date: Tue, 30 May 2023 22:19:24 +0300 Subject: [PATCH 20/36] Add icon color to custom button (#1245) --- lib/src/models/themes/quill_custom_button.dart | 4 ++++ lib/src/widgets/toolbar.dart | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/src/models/themes/quill_custom_button.dart b/lib/src/models/themes/quill_custom_button.dart index 4ea4e4f5..791b7095 100644 --- a/lib/src/models/themes/quill_custom_button.dart +++ b/lib/src/models/themes/quill_custom_button.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; class QuillCustomButton { const QuillCustomButton({ this.icon, + this.iconColor, this.onTap, this.tooltip, }); @@ -10,6 +11,9 @@ class QuillCustomButton { ///The icon widget final IconData? icon; + ///The icon color; + final Color? iconColor; + ///The function when the icon is tapped final VoidCallback? onTap; diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index f1cb3d96..ed5f6a07 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -570,7 +570,11 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { highlightElevation: 0, hoverElevation: 0, size: toolbarIconSize * kIconButtonFactor, - icon: Icon(customButton.icon, size: toolbarIconSize), + icon: Icon( + customButton.icon, + size: toolbarIconSize, + color: customButton.iconColor, + ), tooltip: customButton.tooltip, borderRadius: iconTheme?.borderRadius ?? 2, onPressed: customButton.onTap, From 6399cca1d8ce6ba931403e201016d198ec910612 Mon Sep 17 00:00:00 2001 From: Simon Uzar <82913812+xGreatSoulx@users.noreply.github.com> Date: Thu, 1 Jun 2023 16:34:44 -0300 Subject: [PATCH 21/36] Translated some texts in 'pt_br' (#1250) --- lib/src/translations/toolbar.i18n.dart | 68 +++++++++++++------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 2e0cdca0..7fc356e9 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -1034,45 +1034,45 @@ extension Localization on String { 'Clear': 'Limpar', 'Font': 'Fonte', 'Search': 'Buscar', - 'matches': 'matches', - 'showing match': 'showing match', + 'matches': 'resultado(s)', + 'showing match': 'mostrando resultado', 'Prev': 'Anterior', 'Next': 'Próximo', - 'Camera': 'Camera', + 'Camera': 'Câmera', 'Video': 'Vídeo', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', + 'Undo': 'Desfazer', + 'Redo': 'Refazer', + 'Font family': 'Fonte', + 'Font size': 'Tamanho da fonte', + 'Bold': 'Negrito', + 'Subscript': 'Subscrito', + 'Superscript': 'Sobrescrito', + 'Italic': 'Itálico', + 'Underline': 'Sublinhado', + 'Strike through': 'Tachado', 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', + 'Font color': 'Cor da fonte', + 'Background color': 'Cor do fundo', + 'Clear format': 'Limpar formatação', + 'Align left': 'Texto à esquerda', + 'Align center': 'Centralizar', + 'Align right': 'Texto à direita', + 'Justify win width': 'Justificado', + 'Text direction': 'Direção do texto', + 'Header style': 'Estilo de cabeçalho', + 'Numbered list': 'Numeração', + 'Bullet list': 'Marcadores', + 'Checked list': 'Lista de verificação', 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', + 'Quote': 'Citação', + 'Increase indent': 'Aumentar recuo', + 'Decrease indent': 'Diminuir recuo', + 'Insert URL': 'Inserir URL', + 'Visit link': 'Visitar link', + 'Enter link': 'Inserir link', + 'Enter media': 'Inserir mídia', + 'Edit': 'Editar', + 'Apply': 'Aplicar', }, 'pl': { 'Paste a link': 'Wklej link', From 205be05e9aeb8b321fa7a4ddab25136f6188930f Mon Sep 17 00:00:00 2001 From: George Tian Date: Mon, 5 Jun 2023 22:17:38 +0800 Subject: [PATCH 22/36] Fix color picker dialog overflow (#1251) --- lib/src/widgets/toolbar/color_button.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/src/widgets/toolbar/color_button.dart b/lib/src/widgets/toolbar/color_button.dart index 65139629..41a57734 100644 --- a/lib/src/widgets/toolbar/color_button.dart +++ b/lib/src/widgets/toolbar/color_button.dart @@ -170,9 +170,9 @@ class _ColorButtonState extends State { child: Text('OK'.i18n)), ], backgroundColor: Theme.of(context).canvasColor, - content: SizedBox( - height: 400, + content: SingleChildScrollView( child: Column( + mainAxisSize: MainAxisSize.min, children: [ Row( children: [ @@ -192,8 +192,7 @@ class _ColorButtonState extends State { child: Text('Color'.i18n)), ], ), - Expanded( - child: Column(children: [ + Column(children: [ if (pickerType == 'material') MaterialPicker( pickerColor: selectedColor, @@ -253,7 +252,7 @@ class _ColorButtonState extends State { }), ], ), - ])) + ]) ], ), )); From 6fa057987363d15dc78eadaf5e71dd721bb3ea56 Mon Sep 17 00:00:00 2001 From: Dilanka Yapa <104094511+dilankayapagit@users.noreply.github.com> Date: Mon, 5 Jun 2023 19:47:57 +0530 Subject: [PATCH 23/36] Update pubspec.yaml (#1252) --- flutter_quill_extensions/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index a1ce333f..f1b77e8c 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: flutter: sdk: flutter - flutter_quill: ^7.1.20 + flutter_quill: ^7.2.1 image_picker: ^0.8.5+3 photo_view: ^0.14.0 From 1124a1f26cea21a8d5839b15fc23ceb26a0ea1cd Mon Sep 17 00:00:00 2001 From: Richard Marshall Date: Tue, 6 Jun 2023 17:18:46 -0700 Subject: [PATCH 24/36] Prevent operations on stale editor state (#1256) --- lib/src/widgets/editor.dart | 3 ++ lib/src/widgets/raw_editor.dart | 44 ++++++++++++++----- ..._editor_state_text_input_client_mixin.dart | 3 +- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index aefa21dc..667d6d7e 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -46,6 +46,9 @@ abstract class EditorState extends State /// The floating cursor is animated to merge with the regular cursor. AnimationController get floatingCursorResetController; + /// Returns true if the editor has been marked as needing to be rebuilt. + bool get dirty; + bool showToolbar(); void requestKeyboard(); diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 657c63f7..8a669c0c 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -332,6 +332,10 @@ class RawEditorState extends EditorState TextDirection get _textDirection => Directionality.of(context); + @override + bool get dirty => _dirty; + bool _dirty = false; + @override void insertContent(KeyboardInsertedContent content) { assert(widget.contentInsertionConfiguration?.allowedMimeTypes @@ -855,6 +859,7 @@ class RawEditorState extends EditorState final currentSelection = controller.selection.copyWith(); final attribute = value ? Attribute.checked : Attribute.unchecked; + _markNeedsBuild(); controller ..ignoreFocusOnTextChange = true ..formatText(offset, 0, attribute) @@ -929,9 +934,11 @@ class RawEditorState extends EditorState clearIndents = false; } else { + _dirty = false; throw StateError('Unreachable.'); } } + _dirty = false; return result; } @@ -1170,6 +1177,17 @@ class RawEditorState extends EditorState _selectionOverlay?.updateForScroll(); } + /// Marks the editor as dirty and trigger a rebuild. + /// + /// When the editor is dirty methods that depend on the editor + /// state being in sync with the controller know they may be + /// operating on stale data. + void _markNeedsBuild() { + setState(() { + _dirty = true; + }); + } + void _didChangeTextEditingValue([bool ignoreFocus = false]) { if (kIsWeb) { _onChangeTextEditingValue(ignoreFocus); @@ -1184,10 +1202,9 @@ class RawEditorState extends EditorState } else { requestKeyboard(); if (mounted) { - setState(() { - // Use controller.value in build() - // Trigger build and updateChildren - }); + // Use controller.value in build() + // Mark widget as dirty and trigger build and updateChildren + _markNeedsBuild(); } } @@ -1222,10 +1239,9 @@ class RawEditorState extends EditorState _updateOrDisposeSelectionOverlayIfNeeded(); }); if (mounted) { - setState(() { - // Use controller.value in build() - // Trigger build and updateChildren - }); + // Use controller.value in build() + // Mark widget as dirty and trigger build and updateChildren + _markNeedsBuild(); } } @@ -1258,6 +1274,11 @@ class RawEditorState extends EditorState } void _handleFocusChanged() { + if (dirty) { + SchedulerBinding.instance + .addPostFrameCallback((_) => _handleFocusChanged()); + return; + } openOrCloseConnection(); _cursorCont.startOrStopCursorTimerIfNeeded(_hasFocus, controller.selection); _updateOrDisposeSelectionOverlayIfNeeded(); @@ -1272,10 +1293,9 @@ class RawEditorState extends EditorState void _onChangedClipboardStatus() { if (!mounted) return; - setState(() { - // Inform the widget that the value of clipboardStatus has changed. - // Trigger build and updateChildren - }); + // Inform the widget that the value of clipboardStatus has changed. + // Trigger build and updateChildren + _markNeedsBuild(); } Future _linkActionPicker(Node linkNode) async { diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index fff4c5ac..396a76e1 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -91,7 +91,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState void _updateCaretRectIfNeeded() { if (hasConnection) { - if (renderEditor.selection.isValid && + if (!dirty && + renderEditor.selection.isValid && renderEditor.selection.isCollapsed) { final currentTextPosition = TextPosition(offset: renderEditor.selection.baseOffset); From ebef9d7d95b941cc33451ccc099b761ab4daa667 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Tue, 6 Jun 2023 17:34:24 -0700 Subject: [PATCH 25/36] Upgrade to 7.2.2 --- CHANGELOG.md | 3 +++ lib/src/models/themes/quill_custom_button.dart | 2 +- lib/src/widgets/toolbar.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 639f46e8..40c907e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [7.2.2] +- Prevent operations on stale editor state. + # [7.2.1] - Add support for android keyboard content insertion. - Enhance color picker, enter hex color and color palette option. diff --git a/lib/src/models/themes/quill_custom_button.dart b/lib/src/models/themes/quill_custom_button.dart index 791b7095..bbb472f9 100644 --- a/lib/src/models/themes/quill_custom_button.dart +++ b/lib/src/models/themes/quill_custom_button.dart @@ -13,7 +13,7 @@ class QuillCustomButton { ///The icon color; final Color? iconColor; - + ///The function when the icon is tapped final VoidCallback? onTap; diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index ed5f6a07..fb01c4df 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -571,7 +571,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { hoverElevation: 0, size: toolbarIconSize * kIconButtonFactor, icon: Icon( - customButton.icon, + customButton.icon, size: toolbarIconSize, color: customButton.iconColor, ), diff --git a/pubspec.yaml b/pubspec.yaml index f105b378..de306d17 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.2.1 +version: 7.2.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From ba6fa9c6f7f8b8704ce7f4d7f29bcc5160c57822 Mon Sep 17 00:00:00 2001 From: Pwiz Date: Fri, 9 Jun 2023 08:58:54 +0800 Subject: [PATCH 26/36] get pixel ratio from view (#1259) --- lib/src/widgets/editor.dart | 4 ++-- lib/src/widgets/raw_editor.dart | 2 +- lib/src/widgets/text_block.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 667d6d7e..52cf9a67 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -477,8 +477,8 @@ class QuillEditorState extends State selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); cursorRadius ??= const Radius.circular(2); - cursorOffset = Offset( - iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); + cursorOffset = + Offset(iOSHorizontalOffset / View.of(context).devicePixelRatio, 0); } else { textSelectionControls = materialTextSelectionControls; paintCursorAboveText = false; diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 8a669c0c..9319bfab 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -968,7 +968,7 @@ class RawEditorState extends EditorState widget.selectionColor, widget.enableInteractiveSelection, _hasFocus, - MediaQuery.of(context).devicePixelRatio, + View.of(context).devicePixelRatio, _cursorCont); return editableTextLine; } diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index 4adaac37..3906fb88 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -159,7 +159,7 @@ class EditableTextBlock extends StatelessWidget { color, enableInteractiveSelection, hasFocus, - MediaQuery.of(context).devicePixelRatio, + View.of(context).devicePixelRatio, cursorCont); final nodeTextDirection = getDirectionOfNode(line); children.add(Directionality( From d547ec5951af7d6771203b6886f80682e55ce2d0 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Thu, 8 Jun 2023 18:39:54 -0700 Subject: [PATCH 27/36] Upgrade to 7.2.3 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40c907e3..84fab2bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [7.2.3] +- Get pixel ratio from view. + # [7.2.2] - Prevent operations on stale editor state. diff --git a/pubspec.yaml b/pubspec.yaml index de306d17..bba49aa1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.2.2 +version: 7.2.3 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From b883f727d80cb09b3ebbe61683e6ee473505f339 Mon Sep 17 00:00:00 2001 From: Richard Marshall Date: Mon, 12 Jun 2023 17:35:51 -0700 Subject: [PATCH 28/36] First pass of tests (#1261) * Initial set of tests for QuillController * Basic text input test for QuillEditor widget * Add test for Android keyboard image insertion * Add tests to validate fixes for #1189 * Export test utilites --- .gitignore | 1 + README.md | 16 ++ lib/flutter_quill_test.dart | 3 + lib/src/test/widget_tester_extension.dart | 60 +++++ test/bug_fix_test.dart | 60 +++++ test/widgets/controller_test.dart | 290 ++++++++++++++++++++++ test/widgets/editor_test.dart | 82 ++++++ 7 files changed, 512 insertions(+) create mode 100644 lib/flutter_quill_test.dart create mode 100644 lib/src/test/widget_tester_extension.dart create mode 100644 test/bug_fix_test.dart create mode 100644 test/widgets/controller_test.dart create mode 100644 test/widgets/editor_test.dart diff --git a/.gitignore b/.gitignore index 08c3c879..6b87759f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ .pub-cache/ .pub/ build/ +coverage/ # Android related **/android/**/gradle-wrapper.jar diff --git a/README.md b/README.md index d31e9a58..9fe48b01 100644 --- a/README.md +++ b/README.md @@ -391,6 +391,22 @@ tables, and mentions. Conversion can be performed in vanilla Dart (i.e., server- It is a complete Dart part of the popular and mature [quill-delta-to-html](https://www.npmjs.com/package/quill-delta-to-html) Typescript/Javascript package. +## Testing + +To aid in testing applications using the editor an extension to the flutter `WidgetTester` is provided which includes methods to simplify interacting with the editor in test cases. + +Import the test utilities in your test file: + +```dart +import 'package:flutter_quill/flutter_quill_test.dart'; +``` + +and then enter text using `quillEnterText`: + +```dart +await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); +``` + ## Sponsors diff --git a/lib/flutter_quill_test.dart b/lib/flutter_quill_test.dart new file mode 100644 index 00000000..988e4e82 --- /dev/null +++ b/lib/flutter_quill_test.dart @@ -0,0 +1,3 @@ +library flutter_quill_test; + +export 'src/test/widget_tester_extension.dart'; diff --git a/lib/src/test/widget_tester_extension.dart b/lib/src/test/widget_tester_extension.dart new file mode 100644 index 00000000..21bb75ab --- /dev/null +++ b/lib/src/test/widget_tester_extension.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../widgets/editor.dart'; +import '../widgets/raw_editor.dart'; + +/// Extends +extension QuillEnterText on WidgetTester { + /// Give the QuillEditor widget specified by [finder] the focus. + Future quillGiveFocus(Finder finder) { + return TestAsyncUtils.guard(() async { + final editor = state( + find.descendant( + of: finder, + matching: + find.byType(QuillEditor, skipOffstage: finder.skipOffstage), + matchRoot: true), + ); + editor.widget.focusNode.requestFocus(); + await pump(); + expect(editor.widget.focusNode.hasFocus, isTrue); + }); + } + + /// Give the QuillEditor widget specified by [finder] the focus and update its + /// editing value with [text], as if it had been provided by the onscreen + /// keyboard. + /// + /// The widget specified by [finder] must be a [QuillEditor] or have a + /// [QuillEditor] descendant. For example `find.byType(QuillEditor)`. + Future quillEnterText(Finder finder, String text) async { + return TestAsyncUtils.guard(() async { + await quillGiveFocus(finder); + await quillUpdateEditingValue(finder, text); + await idle(); + }); + } + + /// Update the text editing value of the QuillEditor widget specified by + /// [finder] with [text], as if it had been provided by the onscreen keyboard. + /// + /// The widget specified by [finder] must already have focus and be a + /// [QuillEditor] or have a [QuillEditor] descendant. For example + /// `find.byType(QuillEditor)`. + Future quillUpdateEditingValue(Finder finder, String text) async { + return TestAsyncUtils.guard(() async { + final editor = state( + find.descendant( + of: finder, + matching: find.byType(RawEditor, skipOffstage: finder.skipOffstage), + matchRoot: true), + ); + testTextInput.updateEditingValue(TextEditingValue( + text: text, + selection: TextSelection.collapsed( + offset: editor.textEditingValue.text.length))); + await idle(); + }); + } +} diff --git a/test/bug_fix_test.dart b/test/bug_fix_test.dart new file mode 100644 index 00000000..ecbcad2b --- /dev/null +++ b/test/bug_fix_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill/flutter_quill_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Bug fix', () { + group('1189 - The provided text position is not in the current node', () { + late QuillController controller; + late QuillEditor editor; + + setUp(() { + controller = QuillController.basic(); + editor = QuillEditor.basic(controller: controller, readOnly: false); + }); + + tearDown(() { + controller.dispose(); + }); + + testWidgets('Refocus editor after controller clears document', + (tester) async { + await tester.pumpWidget(MaterialApp(home: Column(children: [editor]))); + await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); + + editor.focusNode.unfocus(); + await tester.pump(); + controller.clear(); + editor.focusNode.requestFocus(); + await tester.pump(); + expect(tester.takeException(), isNull); + }); + + testWidgets('Refocus editor after removing block attribute', + (tester) async { + await tester.pumpWidget(MaterialApp(home: Column(children: [editor]))); + await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); + + controller.formatSelection(Attribute.ul); + editor.focusNode.unfocus(); + await tester.pump(); + controller.formatSelection(const ListAttribute(null)); + editor.focusNode.requestFocus(); + await tester.pump(); + expect(tester.takeException(), isNull); + }); + + testWidgets('Tap checkbox in unfocused editor', (tester) async { + await tester.pumpWidget(MaterialApp(home: Column(children: [editor]))); + await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); + + controller.formatSelection(Attribute.unchecked); + editor.focusNode.unfocus(); + await tester.pump(); + await tester.tap(find.byType(CheckboxPoint)); + expect(tester.takeException(), isNull); + }); + }); + }); +} diff --git a/test/widgets/controller_test.dart b/test/widgets/controller_test.dart new file mode 100644 index 00000000..047dfcae --- /dev/null +++ b/test/widgets/controller_test.dart @@ -0,0 +1,290 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const testDocumentContents = 'data'; + late QuillController controller; + + setUp(() { + controller = QuillController.basic() + ..compose(Delta()..insert(testDocumentContents), + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL); + }); + + group('controller', () { + test('set document', () { + const replacementContents = 'replacement\n'; + final newDocument = + Document.fromDelta(Delta()..insert(replacementContents)); + var listenerCalled = false; + controller + ..addListener(() { + listenerCalled = true; + }) + ..document = newDocument; + expect(listenerCalled, isTrue); + expect(controller.document.toPlainText(), replacementContents); + }); + + test('getSelectionStyle', () { + controller + ..formatText(0, 5, Attribute.h1) + ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4), + ChangeSource.LOCAL); + + expect(controller.getSelectionStyle().values, [Attribute.h1]); + }); + + test('indentSelection with single line document', () { + var listenerCalled = false; + // With selection range + controller + ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4), + ChangeSource.LOCAL) + ..addListener(() { + listenerCalled = true; + }) + ..indentSelection(true); + expect(listenerCalled, isTrue); + expect(controller.getSelectionStyle().values, [Attribute.indentL1]); + controller.indentSelection(true); + expect(controller.getSelectionStyle().values, [Attribute.indentL2]); + controller.indentSelection(false); + expect(controller.getSelectionStyle().values, [Attribute.indentL1]); + controller.indentSelection(false); + expect(controller.getSelectionStyle().values, []); + + // With collapsed selection + controller + ..updateSelection( + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) + ..indentSelection(true); + expect(controller.getSelectionStyle().values, [Attribute.indentL1]); + controller + ..updateSelection( + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) + ..indentSelection(true); + expect(controller.getSelectionStyle().values, [Attribute.indentL2]); + controller.indentSelection(false); + expect(controller.getSelectionStyle().values, [Attribute.indentL1]); + controller.indentSelection(false); + expect(controller.getSelectionStyle().values, []); + }); + + test('indentSelection with multiline document', () { + controller + ..compose(Delta()..insert('line1\nline2\nline3\n'), + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) + // Indent first line + ..updateSelection( + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) + ..indentSelection(true); + expect(controller.getSelectionStyle().values, [Attribute.indentL1]); + + // Indent first two lines + controller + ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 11), + ChangeSource.LOCAL) + ..indentSelection(true); + + // Should have both L1 and L2 indent attributes in selection. + expect(controller.getAllSelectionStyles(), + contains(Style().put(Attribute.indentL1).put(Attribute.indentL2))); + + // Remaining lines should have no attributes. + controller.updateSelection( + TextSelection( + baseOffset: 12, + extentOffset: controller.document.toPlainText().length - 1), + ChangeSource.LOCAL); + expect(controller.getAllSelectionStyles(), everyElement(Style())); + }); + + test('getAllIndividualSelectionStyles', () { + controller.formatText(0, 2, Attribute.bold); + final result = controller.getAllIndividualSelectionStyles(); + expect(result.length, 1); + expect(result[0].offset, 0); + expect(result[0].value, Style().put(Attribute.bold)); + }); + + test('getPlainText', () { + controller.updateSelection( + const TextSelection(baseOffset: 0, extentOffset: 4), + ChangeSource.LOCAL); + + expect(controller.getPlainText(), testDocumentContents); + }); + + test('getAllSelectionStyles', () { + controller.formatText(0, 2, Attribute.bold); + expect(controller.getAllSelectionStyles(), + contains(Style().put(Attribute.bold))); + }); + + test('undo', () { + var listenerCalled = false; + controller.updateSelection( + const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL); + + expect(controller.document.toDelta(), Delta()..insert('data\n')); + controller + ..addListener(() { + listenerCalled = true; + }) + ..undo(); + expect(listenerCalled, isTrue); + expect(controller.document.toDelta(), Delta()..insert('\n')); + }); + + test('redo', () { + var listenerCalled = false; + controller.updateSelection( + const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL); + + expect(controller.document.toDelta(), Delta()..insert('data\n')); + controller.undo(); + expect(controller.document.toDelta(), Delta()..insert('\n')); + controller + ..addListener(() { + listenerCalled = true; + }) + ..redo(); + expect(listenerCalled, isTrue); + expect(controller.document.toDelta(), Delta()..insert('data\n')); + }); + test('clear', () { + var listenerCalled = false; + controller + ..addListener(() { + listenerCalled = true; + }) + ..clear(); + + expect(listenerCalled, isTrue); + expect(controller.document.toDelta(), Delta()..insert('\n')); + }); + + test('replaceText', () { + var listenerCalled = false; + controller + ..addListener(() { + listenerCalled = true; + }) + ..replaceText(1, 2, '11', const TextSelection.collapsed(offset: 0)); + + expect(listenerCalled, isTrue); + expect(controller.document.toDelta(), Delta()..insert('d11a\n')); + }); + + test('formatTextStyle', () { + var listenerCalled = false; + final style = Style().put(Attribute.bold).put(Attribute.italic); + controller + ..addListener(() { + listenerCalled = true; + }) + ..formatTextStyle(0, 2, style); + expect(listenerCalled, isTrue); + expect(controller.document.collectAllStyles(0, 2), contains(style)); + expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); + }); + + test('formatText', () { + var listenerCalled = false; + controller + ..addListener(() { + listenerCalled = true; + }) + ..formatText(0, 2, Attribute.bold); + expect(listenerCalled, isTrue); + expect(controller.document.collectAllStyles(0, 2), + contains(Style().put(Attribute.bold))); + expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); + }); + + test('formatSelection', () { + var listenerCalled = false; + controller + ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 2), + ChangeSource.LOCAL) + ..addListener(() { + listenerCalled = true; + }) + ..formatSelection(Attribute.bold); + expect(listenerCalled, isTrue); + expect(controller.document.collectAllStyles(0, 2), + contains(Style().put(Attribute.bold))); + expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); + }); + + test('moveCursorToStart', () { + var listenerCalled = false; + controller + ..updateSelection( + const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL) + ..addListener(() { + listenerCalled = true; + }); + expect(controller.selection, const TextSelection.collapsed(offset: 4)); + + controller.moveCursorToStart(); + expect(listenerCalled, isTrue); + expect(controller.selection, const TextSelection.collapsed(offset: 0)); + }); + + test('moveCursorToPosition', () { + var listenerCalled = false; + controller.addListener(() { + listenerCalled = true; + }); + expect(controller.selection, const TextSelection.collapsed(offset: 0)); + + controller.moveCursorToPosition(2); + expect(listenerCalled, isTrue); + expect(controller.selection, const TextSelection.collapsed(offset: 2)); + }); + + test('moveCursorToEnd', () { + var listenerCalled = false; + controller.addListener(() { + listenerCalled = true; + }); + expect(controller.selection, const TextSelection.collapsed(offset: 0)); + + controller.moveCursorToEnd(); + expect(listenerCalled, isTrue); + expect(controller.selection, + TextSelection.collapsed(offset: controller.document.length - 1)); + }); + + test('updateSelection', () { + var listenerCalled = false; + const selection = TextSelection.collapsed(offset: 0); + controller + ..addListener(() { + listenerCalled = true; + }) + ..updateSelection(selection, ChangeSource.LOCAL); + + expect(listenerCalled, isTrue); + expect(controller.selection, selection); + }); + + test('compose', () { + var listenerCalled = false; + final originalContents = controller.document.toPlainText(); + controller + ..addListener(() { + listenerCalled = true; + }) + ..compose(Delta()..insert('test '), + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL); + + expect(listenerCalled, isTrue); + expect(controller.document.toDelta(), + Delta()..insert('test $originalContents')); + }); + }); +} diff --git a/test/widgets/editor_test.dart b/test/widgets/editor_test.dart new file mode 100644 index 00000000..3fd425fc --- /dev/null +++ b/test/widgets/editor_test.dart @@ -0,0 +1,82 @@ +import 'dart:convert' show jsonDecode; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill/flutter_quill_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late QuillController controller; + + setUp(() { + controller = QuillController.basic(); + }); + + tearDown(() { + controller.dispose(); + }); + + group('QuillEditor', () { + testWidgets('Keyboard entered text is stored in document', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: QuillEditor.basic(controller: controller, readOnly: false), + ), + ); + await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); + + expect(controller.document.toPlainText(), 'test\n'); + }); + + testWidgets('insertContent is handled correctly', (tester) async { + String? latestUri; + await tester.pumpWidget( + MaterialApp( + home: QuillEditor( + controller: controller, + focusNode: FocusNode(), + scrollController: ScrollController(), + scrollable: true, + padding: const EdgeInsets.all(0), + autoFocus: true, + readOnly: false, + expands: true, + contentInsertionConfiguration: ContentInsertionConfiguration( + onContentInserted: (content) { + latestUri = content.uri; + }, + allowedMimeTypes: const ['image/gif'], + ), + ), + ), + ); + await tester.tap(find.byType(QuillEditor)); + await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); + await tester.idle(); + + const uri = + 'content://com.google.android.inputmethod.latin.fileprovider/test.gif'; + final messageBytes = + const JSONMessageCodec().encodeMessage({ + 'args': [ + -1, + 'TextInputAction.commitContent', + jsonDecode( + '{"mimeType": "image/gif", "data": [0,1,0,1,0,1,0,0,0], "uri": "$uri"}'), + ], + 'method': 'TextInputClient.performAction', + }); + + Object? error; + try { + await tester.binding.defaultBinaryMessenger + .handlePlatformMessage('flutter/textinput', messageBytes, (_) {}); + } catch (e) { + error = e; + } + expect(error, isNull); + expect(latestUri, equals(uri)); + }); + }); +} From b738cad4abc0e64932a92634346e92883bbc6bfa Mon Sep 17 00:00:00 2001 From: liam-duan <93059008+liam-duan@users.noreply.github.com> Date: Mon, 12 Jun 2023 22:36:51 -0600 Subject: [PATCH 29/36] Fixed keepStyleOnNewLine (#1262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The “keepStyleOnNewLine” parameter in the Controller class is no longer functional. --- lib/src/widgets/controller.dart | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 48bf136d..aedd7015 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -255,14 +255,6 @@ class QuillController extends ChangeNotifier { } } - if (_keepStyleOnNewLine) { - final style = getSelectionStyle(); - final notInlineStyle = style.attributes.values.where((s) => !s.isInline); - toggledStyle = style.removeAll(notInlineStyle.toSet()); - } else { - toggledStyle = Style(); - } - if (textSelection != null) { if (delta == null || delta.isEmpty) { _updateSelection(textSelection, ChangeSource.LOCAL); @@ -400,7 +392,13 @@ class QuillController extends ChangeNotifier { _selection = selection.copyWith( baseOffset: math.min(selection.baseOffset, end), extentOffset: math.min(selection.extentOffset, end)); - toggledStyle = Style(); + if (_keepStyleOnNewLine) { + final style = getSelectionStyle(); + final notInlineStyle = style.attributes.values.where((s) => !s.isInline); + toggledStyle = style.removeAll(notInlineStyle.toSet()); + } else { + toggledStyle = Style(); + } onSelectionChanged?.call(textSelection); } From 0392a86c8935e4ba5c14e554a8bd830062e04c87 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Mon, 12 Jun 2023 21:43:35 -0700 Subject: [PATCH 30/36] Upgrade to 7.2.4 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84fab2bc..58f1e223 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [7.2.4] +- Fixed keepStyleOnNewLine. + # [7.2.3] - Get pixel ratio from view. diff --git a/pubspec.yaml b/pubspec.yaml index bba49aa1..4ca8ee66 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.2.3 +version: 7.2.4 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 9d877478ad4ec6cc53880bb85de0525b6abdc900 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Mon, 12 Jun 2023 21:47:08 -0700 Subject: [PATCH 31/36] Revert "First pass of tests (#1261)" This reverts commit b883f727d80cb09b3ebbe61683e6ee473505f339. --- .gitignore | 1 - README.md | 16 -- lib/flutter_quill_test.dart | 3 - lib/src/test/widget_tester_extension.dart | 60 ----- test/bug_fix_test.dart | 60 ----- test/widgets/controller_test.dart | 290 ---------------------- test/widgets/editor_test.dart | 82 ------ 7 files changed, 512 deletions(-) delete mode 100644 lib/flutter_quill_test.dart delete mode 100644 lib/src/test/widget_tester_extension.dart delete mode 100644 test/bug_fix_test.dart delete mode 100644 test/widgets/controller_test.dart delete mode 100644 test/widgets/editor_test.dart diff --git a/.gitignore b/.gitignore index 6b87759f..08c3c879 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ .pub-cache/ .pub/ build/ -coverage/ # Android related **/android/**/gradle-wrapper.jar diff --git a/README.md b/README.md index 9fe48b01..d31e9a58 100644 --- a/README.md +++ b/README.md @@ -391,22 +391,6 @@ tables, and mentions. Conversion can be performed in vanilla Dart (i.e., server- It is a complete Dart part of the popular and mature [quill-delta-to-html](https://www.npmjs.com/package/quill-delta-to-html) Typescript/Javascript package. -## Testing - -To aid in testing applications using the editor an extension to the flutter `WidgetTester` is provided which includes methods to simplify interacting with the editor in test cases. - -Import the test utilities in your test file: - -```dart -import 'package:flutter_quill/flutter_quill_test.dart'; -``` - -and then enter text using `quillEnterText`: - -```dart -await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); -``` - ## Sponsors diff --git a/lib/flutter_quill_test.dart b/lib/flutter_quill_test.dart deleted file mode 100644 index 988e4e82..00000000 --- a/lib/flutter_quill_test.dart +++ /dev/null @@ -1,3 +0,0 @@ -library flutter_quill_test; - -export 'src/test/widget_tester_extension.dart'; diff --git a/lib/src/test/widget_tester_extension.dart b/lib/src/test/widget_tester_extension.dart deleted file mode 100644 index 21bb75ab..00000000 --- a/lib/src/test/widget_tester_extension.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../widgets/editor.dart'; -import '../widgets/raw_editor.dart'; - -/// Extends -extension QuillEnterText on WidgetTester { - /// Give the QuillEditor widget specified by [finder] the focus. - Future quillGiveFocus(Finder finder) { - return TestAsyncUtils.guard(() async { - final editor = state( - find.descendant( - of: finder, - matching: - find.byType(QuillEditor, skipOffstage: finder.skipOffstage), - matchRoot: true), - ); - editor.widget.focusNode.requestFocus(); - await pump(); - expect(editor.widget.focusNode.hasFocus, isTrue); - }); - } - - /// Give the QuillEditor widget specified by [finder] the focus and update its - /// editing value with [text], as if it had been provided by the onscreen - /// keyboard. - /// - /// The widget specified by [finder] must be a [QuillEditor] or have a - /// [QuillEditor] descendant. For example `find.byType(QuillEditor)`. - Future quillEnterText(Finder finder, String text) async { - return TestAsyncUtils.guard(() async { - await quillGiveFocus(finder); - await quillUpdateEditingValue(finder, text); - await idle(); - }); - } - - /// Update the text editing value of the QuillEditor widget specified by - /// [finder] with [text], as if it had been provided by the onscreen keyboard. - /// - /// The widget specified by [finder] must already have focus and be a - /// [QuillEditor] or have a [QuillEditor] descendant. For example - /// `find.byType(QuillEditor)`. - Future quillUpdateEditingValue(Finder finder, String text) async { - return TestAsyncUtils.guard(() async { - final editor = state( - find.descendant( - of: finder, - matching: find.byType(RawEditor, skipOffstage: finder.skipOffstage), - matchRoot: true), - ); - testTextInput.updateEditingValue(TextEditingValue( - text: text, - selection: TextSelection.collapsed( - offset: editor.textEditingValue.text.length))); - await idle(); - }); - } -} diff --git a/test/bug_fix_test.dart b/test/bug_fix_test.dart deleted file mode 100644 index ecbcad2b..00000000 --- a/test/bug_fix_test.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_quill/flutter_quill_test.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('Bug fix', () { - group('1189 - The provided text position is not in the current node', () { - late QuillController controller; - late QuillEditor editor; - - setUp(() { - controller = QuillController.basic(); - editor = QuillEditor.basic(controller: controller, readOnly: false); - }); - - tearDown(() { - controller.dispose(); - }); - - testWidgets('Refocus editor after controller clears document', - (tester) async { - await tester.pumpWidget(MaterialApp(home: Column(children: [editor]))); - await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); - - editor.focusNode.unfocus(); - await tester.pump(); - controller.clear(); - editor.focusNode.requestFocus(); - await tester.pump(); - expect(tester.takeException(), isNull); - }); - - testWidgets('Refocus editor after removing block attribute', - (tester) async { - await tester.pumpWidget(MaterialApp(home: Column(children: [editor]))); - await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); - - controller.formatSelection(Attribute.ul); - editor.focusNode.unfocus(); - await tester.pump(); - controller.formatSelection(const ListAttribute(null)); - editor.focusNode.requestFocus(); - await tester.pump(); - expect(tester.takeException(), isNull); - }); - - testWidgets('Tap checkbox in unfocused editor', (tester) async { - await tester.pumpWidget(MaterialApp(home: Column(children: [editor]))); - await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); - - controller.formatSelection(Attribute.unchecked); - editor.focusNode.unfocus(); - await tester.pump(); - await tester.tap(find.byType(CheckboxPoint)); - expect(tester.takeException(), isNull); - }); - }); - }); -} diff --git a/test/widgets/controller_test.dart b/test/widgets/controller_test.dart deleted file mode 100644 index 047dfcae..00000000 --- a/test/widgets/controller_test.dart +++ /dev/null @@ -1,290 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - const testDocumentContents = 'data'; - late QuillController controller; - - setUp(() { - controller = QuillController.basic() - ..compose(Delta()..insert(testDocumentContents), - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL); - }); - - group('controller', () { - test('set document', () { - const replacementContents = 'replacement\n'; - final newDocument = - Document.fromDelta(Delta()..insert(replacementContents)); - var listenerCalled = false; - controller - ..addListener(() { - listenerCalled = true; - }) - ..document = newDocument; - expect(listenerCalled, isTrue); - expect(controller.document.toPlainText(), replacementContents); - }); - - test('getSelectionStyle', () { - controller - ..formatText(0, 5, Attribute.h1) - ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4), - ChangeSource.LOCAL); - - expect(controller.getSelectionStyle().values, [Attribute.h1]); - }); - - test('indentSelection with single line document', () { - var listenerCalled = false; - // With selection range - controller - ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4), - ChangeSource.LOCAL) - ..addListener(() { - listenerCalled = true; - }) - ..indentSelection(true); - expect(listenerCalled, isTrue); - expect(controller.getSelectionStyle().values, [Attribute.indentL1]); - controller.indentSelection(true); - expect(controller.getSelectionStyle().values, [Attribute.indentL2]); - controller.indentSelection(false); - expect(controller.getSelectionStyle().values, [Attribute.indentL1]); - controller.indentSelection(false); - expect(controller.getSelectionStyle().values, []); - - // With collapsed selection - controller - ..updateSelection( - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) - ..indentSelection(true); - expect(controller.getSelectionStyle().values, [Attribute.indentL1]); - controller - ..updateSelection( - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) - ..indentSelection(true); - expect(controller.getSelectionStyle().values, [Attribute.indentL2]); - controller.indentSelection(false); - expect(controller.getSelectionStyle().values, [Attribute.indentL1]); - controller.indentSelection(false); - expect(controller.getSelectionStyle().values, []); - }); - - test('indentSelection with multiline document', () { - controller - ..compose(Delta()..insert('line1\nline2\nline3\n'), - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) - // Indent first line - ..updateSelection( - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) - ..indentSelection(true); - expect(controller.getSelectionStyle().values, [Attribute.indentL1]); - - // Indent first two lines - controller - ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 11), - ChangeSource.LOCAL) - ..indentSelection(true); - - // Should have both L1 and L2 indent attributes in selection. - expect(controller.getAllSelectionStyles(), - contains(Style().put(Attribute.indentL1).put(Attribute.indentL2))); - - // Remaining lines should have no attributes. - controller.updateSelection( - TextSelection( - baseOffset: 12, - extentOffset: controller.document.toPlainText().length - 1), - ChangeSource.LOCAL); - expect(controller.getAllSelectionStyles(), everyElement(Style())); - }); - - test('getAllIndividualSelectionStyles', () { - controller.formatText(0, 2, Attribute.bold); - final result = controller.getAllIndividualSelectionStyles(); - expect(result.length, 1); - expect(result[0].offset, 0); - expect(result[0].value, Style().put(Attribute.bold)); - }); - - test('getPlainText', () { - controller.updateSelection( - const TextSelection(baseOffset: 0, extentOffset: 4), - ChangeSource.LOCAL); - - expect(controller.getPlainText(), testDocumentContents); - }); - - test('getAllSelectionStyles', () { - controller.formatText(0, 2, Attribute.bold); - expect(controller.getAllSelectionStyles(), - contains(Style().put(Attribute.bold))); - }); - - test('undo', () { - var listenerCalled = false; - controller.updateSelection( - const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL); - - expect(controller.document.toDelta(), Delta()..insert('data\n')); - controller - ..addListener(() { - listenerCalled = true; - }) - ..undo(); - expect(listenerCalled, isTrue); - expect(controller.document.toDelta(), Delta()..insert('\n')); - }); - - test('redo', () { - var listenerCalled = false; - controller.updateSelection( - const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL); - - expect(controller.document.toDelta(), Delta()..insert('data\n')); - controller.undo(); - expect(controller.document.toDelta(), Delta()..insert('\n')); - controller - ..addListener(() { - listenerCalled = true; - }) - ..redo(); - expect(listenerCalled, isTrue); - expect(controller.document.toDelta(), Delta()..insert('data\n')); - }); - test('clear', () { - var listenerCalled = false; - controller - ..addListener(() { - listenerCalled = true; - }) - ..clear(); - - expect(listenerCalled, isTrue); - expect(controller.document.toDelta(), Delta()..insert('\n')); - }); - - test('replaceText', () { - var listenerCalled = false; - controller - ..addListener(() { - listenerCalled = true; - }) - ..replaceText(1, 2, '11', const TextSelection.collapsed(offset: 0)); - - expect(listenerCalled, isTrue); - expect(controller.document.toDelta(), Delta()..insert('d11a\n')); - }); - - test('formatTextStyle', () { - var listenerCalled = false; - final style = Style().put(Attribute.bold).put(Attribute.italic); - controller - ..addListener(() { - listenerCalled = true; - }) - ..formatTextStyle(0, 2, style); - expect(listenerCalled, isTrue); - expect(controller.document.collectAllStyles(0, 2), contains(style)); - expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); - }); - - test('formatText', () { - var listenerCalled = false; - controller - ..addListener(() { - listenerCalled = true; - }) - ..formatText(0, 2, Attribute.bold); - expect(listenerCalled, isTrue); - expect(controller.document.collectAllStyles(0, 2), - contains(Style().put(Attribute.bold))); - expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); - }); - - test('formatSelection', () { - var listenerCalled = false; - controller - ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 2), - ChangeSource.LOCAL) - ..addListener(() { - listenerCalled = true; - }) - ..formatSelection(Attribute.bold); - expect(listenerCalled, isTrue); - expect(controller.document.collectAllStyles(0, 2), - contains(Style().put(Attribute.bold))); - expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); - }); - - test('moveCursorToStart', () { - var listenerCalled = false; - controller - ..updateSelection( - const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL) - ..addListener(() { - listenerCalled = true; - }); - expect(controller.selection, const TextSelection.collapsed(offset: 4)); - - controller.moveCursorToStart(); - expect(listenerCalled, isTrue); - expect(controller.selection, const TextSelection.collapsed(offset: 0)); - }); - - test('moveCursorToPosition', () { - var listenerCalled = false; - controller.addListener(() { - listenerCalled = true; - }); - expect(controller.selection, const TextSelection.collapsed(offset: 0)); - - controller.moveCursorToPosition(2); - expect(listenerCalled, isTrue); - expect(controller.selection, const TextSelection.collapsed(offset: 2)); - }); - - test('moveCursorToEnd', () { - var listenerCalled = false; - controller.addListener(() { - listenerCalled = true; - }); - expect(controller.selection, const TextSelection.collapsed(offset: 0)); - - controller.moveCursorToEnd(); - expect(listenerCalled, isTrue); - expect(controller.selection, - TextSelection.collapsed(offset: controller.document.length - 1)); - }); - - test('updateSelection', () { - var listenerCalled = false; - const selection = TextSelection.collapsed(offset: 0); - controller - ..addListener(() { - listenerCalled = true; - }) - ..updateSelection(selection, ChangeSource.LOCAL); - - expect(listenerCalled, isTrue); - expect(controller.selection, selection); - }); - - test('compose', () { - var listenerCalled = false; - final originalContents = controller.document.toPlainText(); - controller - ..addListener(() { - listenerCalled = true; - }) - ..compose(Delta()..insert('test '), - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL); - - expect(listenerCalled, isTrue); - expect(controller.document.toDelta(), - Delta()..insert('test $originalContents')); - }); - }); -} diff --git a/test/widgets/editor_test.dart b/test/widgets/editor_test.dart deleted file mode 100644 index 3fd425fc..00000000 --- a/test/widgets/editor_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:convert' show jsonDecode; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_quill/flutter_quill_test.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - late QuillController controller; - - setUp(() { - controller = QuillController.basic(); - }); - - tearDown(() { - controller.dispose(); - }); - - group('QuillEditor', () { - testWidgets('Keyboard entered text is stored in document', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: QuillEditor.basic(controller: controller, readOnly: false), - ), - ); - await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); - - expect(controller.document.toPlainText(), 'test\n'); - }); - - testWidgets('insertContent is handled correctly', (tester) async { - String? latestUri; - await tester.pumpWidget( - MaterialApp( - home: QuillEditor( - controller: controller, - focusNode: FocusNode(), - scrollController: ScrollController(), - scrollable: true, - padding: const EdgeInsets.all(0), - autoFocus: true, - readOnly: false, - expands: true, - contentInsertionConfiguration: ContentInsertionConfiguration( - onContentInserted: (content) { - latestUri = content.uri; - }, - allowedMimeTypes: const ['image/gif'], - ), - ), - ), - ); - await tester.tap(find.byType(QuillEditor)); - await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); - await tester.idle(); - - const uri = - 'content://com.google.android.inputmethod.latin.fileprovider/test.gif'; - final messageBytes = - const JSONMessageCodec().encodeMessage({ - 'args': [ - -1, - 'TextInputAction.commitContent', - jsonDecode( - '{"mimeType": "image/gif", "data": [0,1,0,1,0,1,0,0,0], "uri": "$uri"}'), - ], - 'method': 'TextInputClient.performAction', - }); - - Object? error; - try { - await tester.binding.defaultBinaryMessenger - .handlePlatformMessage('flutter/textinput', messageBytes, (_) {}); - } catch (e) { - error = e; - } - expect(error, isNull); - expect(latestUri, equals(uri)); - }); - }); -} From 0906f71b53cd6530905874cb72a3df00549e4f36 Mon Sep 17 00:00:00 2001 From: Benjamin Quinn Date: Tue, 13 Jun 2023 11:03:59 -0400 Subject: [PATCH 32/36] Always use text cursor for editor on desktop (#1264) --- example/lib/pages/home_page.dart | 128 +++++++++++++++---------------- lib/src/widgets/raw_editor.dart | 76 +++++++++--------- 2 files changed, 102 insertions(+), 102 deletions(-) diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 11baf428..9e3bcb02 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -158,76 +158,70 @@ class _HomePageState extends State { } Widget _buildWelcomeEditor(BuildContext context) { - Widget quillEditor = MouseRegion( - cursor: SystemMouseCursors.text, - child: QuillEditor( - controller: _controller!, - scrollController: ScrollController(), - scrollable: true, - focusNode: _focusNode, - autoFocus: false, - readOnly: false, - placeholder: 'Add content', - enableSelectionToolbar: isMobile(), - expands: false, - padding: EdgeInsets.zero, - onImagePaste: _onImagePaste, - onTapUp: (details, p1) { - return _onTripleClickSelection(); - }, - customStyles: DefaultStyles( - h1: DefaultTextBlockStyle( - const TextStyle( - fontSize: 32, - color: Colors.black, - height: 1.15, - fontWeight: FontWeight.w300, - ), - const VerticalSpacing(16, 0), - const VerticalSpacing(0, 0), - null), - sizeSmall: const TextStyle(fontSize: 9), - ), - embedBuilders: [ - ...FlutterQuillEmbeds.builders(), - NotesEmbedBuilder(addEditNote: _addEditNote) - ], + Widget quillEditor = QuillEditor( + controller: _controller!, + scrollController: ScrollController(), + scrollable: true, + focusNode: _focusNode, + autoFocus: false, + readOnly: false, + placeholder: 'Add content', + enableSelectionToolbar: isMobile(), + expands: false, + padding: EdgeInsets.zero, + onImagePaste: _onImagePaste, + onTapUp: (details, p1) { + return _onTripleClickSelection(); + }, + customStyles: DefaultStyles( + h1: DefaultTextBlockStyle( + const TextStyle( + fontSize: 32, + color: Colors.black, + height: 1.15, + fontWeight: FontWeight.w300, + ), + const VerticalSpacing(16, 0), + const VerticalSpacing(0, 0), + null), + sizeSmall: const TextStyle(fontSize: 9), ), + embedBuilders: [ + ...FlutterQuillEmbeds.builders(), + NotesEmbedBuilder(addEditNote: _addEditNote) + ], ); if (kIsWeb) { - quillEditor = MouseRegion( - cursor: SystemMouseCursors.text, - child: QuillEditor( - controller: _controller!, - scrollController: ScrollController(), - scrollable: true, - focusNode: _focusNode, - autoFocus: false, - readOnly: false, - placeholder: 'Add content', - expands: false, - padding: EdgeInsets.zero, - onTapUp: (details, p1) { - return _onTripleClickSelection(); - }, - customStyles: DefaultStyles( - h1: DefaultTextBlockStyle( - const TextStyle( - fontSize: 32, - color: Colors.black, - height: 1.15, - fontWeight: FontWeight.w300, - ), - const VerticalSpacing(16, 0), - const VerticalSpacing(0, 0), - null), - sizeSmall: const TextStyle(fontSize: 9), - ), - embedBuilders: [ - ...defaultEmbedBuildersWeb, - NotesEmbedBuilder(addEditNote: _addEditNote), - ]), - ); + quillEditor = QuillEditor( + controller: _controller!, + scrollController: ScrollController(), + scrollable: true, + focusNode: _focusNode, + autoFocus: false, + readOnly: false, + placeholder: 'Add content', + expands: false, + padding: EdgeInsets.zero, + onTapUp: (details, p1) { + return _onTripleClickSelection(); + }, + customStyles: DefaultStyles( + h1: DefaultTextBlockStyle( + const TextStyle( + fontSize: 32, + color: Colors.black, + height: 1.15, + fontWeight: FontWeight.w300, + ), + const VerticalSpacing(16, 0), + const VerticalSpacing(0, 0), + null), + sizeSmall: const TextStyle(fontSize: 9), + ), + embedBuilders: [ + ...defaultEmbedBuildersWeb, + NotesEmbedBuilder(addEditNote: _addEditNote), + ]); } var toolbar = QuillToolbar.basic( controller: _controller!, diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 9319bfab..48b65df0 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -461,23 +461,26 @@ class RawEditorState extends EditorState Widget child = CompositedTransformTarget( link: _toolbarLayerLink, child: Semantics( - child: _Editor( - key: _editorKey, - document: _doc, - selection: controller.selection, - hasFocus: _hasFocus, - scrollable: widget.scrollable, - cursorController: _cursorCont, - textDirection: _textDirection, - startHandleLayerLink: _startHandleLayerLink, - endHandleLayerLink: _endHandleLayerLink, - onSelectionChanged: _handleSelectionChanged, - onSelectionCompleted: _handleSelectionCompleted, - scrollBottomInset: widget.scrollBottomInset, - padding: widget.padding, - maxContentWidth: widget.maxContentWidth, - floatingCursorDisabled: widget.floatingCursorDisabled, - children: _buildChildren(_doc, context), + child: MouseRegion( + cursor: SystemMouseCursors.text, + child: _Editor( + key: _editorKey, + document: _doc, + selection: controller.selection, + hasFocus: _hasFocus, + scrollable: widget.scrollable, + cursorController: _cursorCont, + textDirection: _textDirection, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + onSelectionChanged: _handleSelectionChanged, + onSelectionCompleted: _handleSelectionCompleted, + scrollBottomInset: widget.scrollBottomInset, + padding: widget.padding, + maxContentWidth: widget.maxContentWidth, + floatingCursorDisabled: widget.floatingCursorDisabled, + children: _buildChildren(_doc, context), + ), ), ), ); @@ -499,24 +502,27 @@ class RawEditorState extends EditorState physics: widget.scrollPhysics, viewportBuilder: (_, offset) => CompositedTransformTarget( link: _toolbarLayerLink, - child: _Editor( - key: _editorKey, - offset: offset, - document: _doc, - selection: controller.selection, - hasFocus: _hasFocus, - scrollable: widget.scrollable, - textDirection: _textDirection, - startHandleLayerLink: _startHandleLayerLink, - endHandleLayerLink: _endHandleLayerLink, - onSelectionChanged: _handleSelectionChanged, - onSelectionCompleted: _handleSelectionCompleted, - scrollBottomInset: widget.scrollBottomInset, - padding: widget.padding, - maxContentWidth: widget.maxContentWidth, - cursorController: _cursorCont, - floatingCursorDisabled: widget.floatingCursorDisabled, - children: _buildChildren(_doc, context), + child: MouseRegion( + cursor: SystemMouseCursors.text, + child: _Editor( + key: _editorKey, + offset: offset, + document: _doc, + selection: controller.selection, + hasFocus: _hasFocus, + scrollable: widget.scrollable, + textDirection: _textDirection, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + onSelectionChanged: _handleSelectionChanged, + onSelectionCompleted: _handleSelectionCompleted, + scrollBottomInset: widget.scrollBottomInset, + padding: widget.padding, + maxContentWidth: widget.maxContentWidth, + cursorController: _cursorCont, + floatingCursorDisabled: widget.floatingCursorDisabled, + children: _buildChildren(_doc, context), + ), ), ), ), From 0fd790e157d9296fed2ce590294decc6992a561d Mon Sep 17 00:00:00 2001 From: Richard Marshall Date: Tue, 13 Jun 2023 11:32:22 -0700 Subject: [PATCH 33/36] First pass of tests and simple CI (#1265) --- .github/workflows/main.yml | 23 ++ .gitignore | 1 + README.md | 16 ++ lib/flutter_quill_test.dart | 3 + lib/src/test/widget_tester_extension.dart | 60 +++++ pubspec.yaml | 2 +- test/bug_fix_test.dart | 60 +++++ test/widgets/controller_test.dart | 290 ++++++++++++++++++++++ test/widgets/editor_test.dart | 82 ++++++ 9 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/main.yml create mode 100644 lib/flutter_quill_test.dart create mode 100644 lib/src/test/widget_tester_extension.dart create mode 100644 test/bug_fix_test.dart create mode 100644 test/widgets/controller_test.dart create mode 100644 test/widgets/editor_test.dart diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..c9262307 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,23 @@ +name: flutter-quill CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - run: flutter --version + - run: flutter pub get + - run: flutter pub get -C flutter_quill_extensions + - run: flutter analyze + - run: flutter test + - run: flutter pub publish --dry-run diff --git a/.gitignore b/.gitignore index 08c3c879..6b87759f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ .pub-cache/ .pub/ build/ +coverage/ # Android related **/android/**/gradle-wrapper.jar diff --git a/README.md b/README.md index d31e9a58..9fe48b01 100644 --- a/README.md +++ b/README.md @@ -391,6 +391,22 @@ tables, and mentions. Conversion can be performed in vanilla Dart (i.e., server- It is a complete Dart part of the popular and mature [quill-delta-to-html](https://www.npmjs.com/package/quill-delta-to-html) Typescript/Javascript package. +## Testing + +To aid in testing applications using the editor an extension to the flutter `WidgetTester` is provided which includes methods to simplify interacting with the editor in test cases. + +Import the test utilities in your test file: + +```dart +import 'package:flutter_quill/flutter_quill_test.dart'; +``` + +and then enter text using `quillEnterText`: + +```dart +await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); +``` + ## Sponsors diff --git a/lib/flutter_quill_test.dart b/lib/flutter_quill_test.dart new file mode 100644 index 00000000..988e4e82 --- /dev/null +++ b/lib/flutter_quill_test.dart @@ -0,0 +1,3 @@ +library flutter_quill_test; + +export 'src/test/widget_tester_extension.dart'; diff --git a/lib/src/test/widget_tester_extension.dart b/lib/src/test/widget_tester_extension.dart new file mode 100644 index 00000000..21bb75ab --- /dev/null +++ b/lib/src/test/widget_tester_extension.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../widgets/editor.dart'; +import '../widgets/raw_editor.dart'; + +/// Extends +extension QuillEnterText on WidgetTester { + /// Give the QuillEditor widget specified by [finder] the focus. + Future quillGiveFocus(Finder finder) { + return TestAsyncUtils.guard(() async { + final editor = state( + find.descendant( + of: finder, + matching: + find.byType(QuillEditor, skipOffstage: finder.skipOffstage), + matchRoot: true), + ); + editor.widget.focusNode.requestFocus(); + await pump(); + expect(editor.widget.focusNode.hasFocus, isTrue); + }); + } + + /// Give the QuillEditor widget specified by [finder] the focus and update its + /// editing value with [text], as if it had been provided by the onscreen + /// keyboard. + /// + /// The widget specified by [finder] must be a [QuillEditor] or have a + /// [QuillEditor] descendant. For example `find.byType(QuillEditor)`. + Future quillEnterText(Finder finder, String text) async { + return TestAsyncUtils.guard(() async { + await quillGiveFocus(finder); + await quillUpdateEditingValue(finder, text); + await idle(); + }); + } + + /// Update the text editing value of the QuillEditor widget specified by + /// [finder] with [text], as if it had been provided by the onscreen keyboard. + /// + /// The widget specified by [finder] must already have focus and be a + /// [QuillEditor] or have a [QuillEditor] descendant. For example + /// `find.byType(QuillEditor)`. + Future quillUpdateEditingValue(Finder finder, String text) async { + return TestAsyncUtils.guard(() async { + final editor = state( + find.descendant( + of: finder, + matching: find.byType(RawEditor, skipOffstage: finder.skipOffstage), + matchRoot: true), + ); + testTextInput.updateEditingValue(TextEditingValue( + text: text, + selection: TextSelection.collapsed( + offset: editor.textEditingValue.text.length))); + await idle(); + }); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 4ca8ee66..db62be2b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: platform: ^3.1.0 pasteboard: ^0.2.0 -dev_dependencies: + # Dependencies for testing utilities flutter_test: sdk: flutter diff --git a/test/bug_fix_test.dart b/test/bug_fix_test.dart new file mode 100644 index 00000000..ecbcad2b --- /dev/null +++ b/test/bug_fix_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill/flutter_quill_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Bug fix', () { + group('1189 - The provided text position is not in the current node', () { + late QuillController controller; + late QuillEditor editor; + + setUp(() { + controller = QuillController.basic(); + editor = QuillEditor.basic(controller: controller, readOnly: false); + }); + + tearDown(() { + controller.dispose(); + }); + + testWidgets('Refocus editor after controller clears document', + (tester) async { + await tester.pumpWidget(MaterialApp(home: Column(children: [editor]))); + await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); + + editor.focusNode.unfocus(); + await tester.pump(); + controller.clear(); + editor.focusNode.requestFocus(); + await tester.pump(); + expect(tester.takeException(), isNull); + }); + + testWidgets('Refocus editor after removing block attribute', + (tester) async { + await tester.pumpWidget(MaterialApp(home: Column(children: [editor]))); + await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); + + controller.formatSelection(Attribute.ul); + editor.focusNode.unfocus(); + await tester.pump(); + controller.formatSelection(const ListAttribute(null)); + editor.focusNode.requestFocus(); + await tester.pump(); + expect(tester.takeException(), isNull); + }); + + testWidgets('Tap checkbox in unfocused editor', (tester) async { + await tester.pumpWidget(MaterialApp(home: Column(children: [editor]))); + await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); + + controller.formatSelection(Attribute.unchecked); + editor.focusNode.unfocus(); + await tester.pump(); + await tester.tap(find.byType(CheckboxPoint)); + expect(tester.takeException(), isNull); + }); + }); + }); +} diff --git a/test/widgets/controller_test.dart b/test/widgets/controller_test.dart new file mode 100644 index 00000000..047dfcae --- /dev/null +++ b/test/widgets/controller_test.dart @@ -0,0 +1,290 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const testDocumentContents = 'data'; + late QuillController controller; + + setUp(() { + controller = QuillController.basic() + ..compose(Delta()..insert(testDocumentContents), + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL); + }); + + group('controller', () { + test('set document', () { + const replacementContents = 'replacement\n'; + final newDocument = + Document.fromDelta(Delta()..insert(replacementContents)); + var listenerCalled = false; + controller + ..addListener(() { + listenerCalled = true; + }) + ..document = newDocument; + expect(listenerCalled, isTrue); + expect(controller.document.toPlainText(), replacementContents); + }); + + test('getSelectionStyle', () { + controller + ..formatText(0, 5, Attribute.h1) + ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4), + ChangeSource.LOCAL); + + expect(controller.getSelectionStyle().values, [Attribute.h1]); + }); + + test('indentSelection with single line document', () { + var listenerCalled = false; + // With selection range + controller + ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4), + ChangeSource.LOCAL) + ..addListener(() { + listenerCalled = true; + }) + ..indentSelection(true); + expect(listenerCalled, isTrue); + expect(controller.getSelectionStyle().values, [Attribute.indentL1]); + controller.indentSelection(true); + expect(controller.getSelectionStyle().values, [Attribute.indentL2]); + controller.indentSelection(false); + expect(controller.getSelectionStyle().values, [Attribute.indentL1]); + controller.indentSelection(false); + expect(controller.getSelectionStyle().values, []); + + // With collapsed selection + controller + ..updateSelection( + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) + ..indentSelection(true); + expect(controller.getSelectionStyle().values, [Attribute.indentL1]); + controller + ..updateSelection( + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) + ..indentSelection(true); + expect(controller.getSelectionStyle().values, [Attribute.indentL2]); + controller.indentSelection(false); + expect(controller.getSelectionStyle().values, [Attribute.indentL1]); + controller.indentSelection(false); + expect(controller.getSelectionStyle().values, []); + }); + + test('indentSelection with multiline document', () { + controller + ..compose(Delta()..insert('line1\nline2\nline3\n'), + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) + // Indent first line + ..updateSelection( + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) + ..indentSelection(true); + expect(controller.getSelectionStyle().values, [Attribute.indentL1]); + + // Indent first two lines + controller + ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 11), + ChangeSource.LOCAL) + ..indentSelection(true); + + // Should have both L1 and L2 indent attributes in selection. + expect(controller.getAllSelectionStyles(), + contains(Style().put(Attribute.indentL1).put(Attribute.indentL2))); + + // Remaining lines should have no attributes. + controller.updateSelection( + TextSelection( + baseOffset: 12, + extentOffset: controller.document.toPlainText().length - 1), + ChangeSource.LOCAL); + expect(controller.getAllSelectionStyles(), everyElement(Style())); + }); + + test('getAllIndividualSelectionStyles', () { + controller.formatText(0, 2, Attribute.bold); + final result = controller.getAllIndividualSelectionStyles(); + expect(result.length, 1); + expect(result[0].offset, 0); + expect(result[0].value, Style().put(Attribute.bold)); + }); + + test('getPlainText', () { + controller.updateSelection( + const TextSelection(baseOffset: 0, extentOffset: 4), + ChangeSource.LOCAL); + + expect(controller.getPlainText(), testDocumentContents); + }); + + test('getAllSelectionStyles', () { + controller.formatText(0, 2, Attribute.bold); + expect(controller.getAllSelectionStyles(), + contains(Style().put(Attribute.bold))); + }); + + test('undo', () { + var listenerCalled = false; + controller.updateSelection( + const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL); + + expect(controller.document.toDelta(), Delta()..insert('data\n')); + controller + ..addListener(() { + listenerCalled = true; + }) + ..undo(); + expect(listenerCalled, isTrue); + expect(controller.document.toDelta(), Delta()..insert('\n')); + }); + + test('redo', () { + var listenerCalled = false; + controller.updateSelection( + const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL); + + expect(controller.document.toDelta(), Delta()..insert('data\n')); + controller.undo(); + expect(controller.document.toDelta(), Delta()..insert('\n')); + controller + ..addListener(() { + listenerCalled = true; + }) + ..redo(); + expect(listenerCalled, isTrue); + expect(controller.document.toDelta(), Delta()..insert('data\n')); + }); + test('clear', () { + var listenerCalled = false; + controller + ..addListener(() { + listenerCalled = true; + }) + ..clear(); + + expect(listenerCalled, isTrue); + expect(controller.document.toDelta(), Delta()..insert('\n')); + }); + + test('replaceText', () { + var listenerCalled = false; + controller + ..addListener(() { + listenerCalled = true; + }) + ..replaceText(1, 2, '11', const TextSelection.collapsed(offset: 0)); + + expect(listenerCalled, isTrue); + expect(controller.document.toDelta(), Delta()..insert('d11a\n')); + }); + + test('formatTextStyle', () { + var listenerCalled = false; + final style = Style().put(Attribute.bold).put(Attribute.italic); + controller + ..addListener(() { + listenerCalled = true; + }) + ..formatTextStyle(0, 2, style); + expect(listenerCalled, isTrue); + expect(controller.document.collectAllStyles(0, 2), contains(style)); + expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); + }); + + test('formatText', () { + var listenerCalled = false; + controller + ..addListener(() { + listenerCalled = true; + }) + ..formatText(0, 2, Attribute.bold); + expect(listenerCalled, isTrue); + expect(controller.document.collectAllStyles(0, 2), + contains(Style().put(Attribute.bold))); + expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); + }); + + test('formatSelection', () { + var listenerCalled = false; + controller + ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 2), + ChangeSource.LOCAL) + ..addListener(() { + listenerCalled = true; + }) + ..formatSelection(Attribute.bold); + expect(listenerCalled, isTrue); + expect(controller.document.collectAllStyles(0, 2), + contains(Style().put(Attribute.bold))); + expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); + }); + + test('moveCursorToStart', () { + var listenerCalled = false; + controller + ..updateSelection( + const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL) + ..addListener(() { + listenerCalled = true; + }); + expect(controller.selection, const TextSelection.collapsed(offset: 4)); + + controller.moveCursorToStart(); + expect(listenerCalled, isTrue); + expect(controller.selection, const TextSelection.collapsed(offset: 0)); + }); + + test('moveCursorToPosition', () { + var listenerCalled = false; + controller.addListener(() { + listenerCalled = true; + }); + expect(controller.selection, const TextSelection.collapsed(offset: 0)); + + controller.moveCursorToPosition(2); + expect(listenerCalled, isTrue); + expect(controller.selection, const TextSelection.collapsed(offset: 2)); + }); + + test('moveCursorToEnd', () { + var listenerCalled = false; + controller.addListener(() { + listenerCalled = true; + }); + expect(controller.selection, const TextSelection.collapsed(offset: 0)); + + controller.moveCursorToEnd(); + expect(listenerCalled, isTrue); + expect(controller.selection, + TextSelection.collapsed(offset: controller.document.length - 1)); + }); + + test('updateSelection', () { + var listenerCalled = false; + const selection = TextSelection.collapsed(offset: 0); + controller + ..addListener(() { + listenerCalled = true; + }) + ..updateSelection(selection, ChangeSource.LOCAL); + + expect(listenerCalled, isTrue); + expect(controller.selection, selection); + }); + + test('compose', () { + var listenerCalled = false; + final originalContents = controller.document.toPlainText(); + controller + ..addListener(() { + listenerCalled = true; + }) + ..compose(Delta()..insert('test '), + const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL); + + expect(listenerCalled, isTrue); + expect(controller.document.toDelta(), + Delta()..insert('test $originalContents')); + }); + }); +} diff --git a/test/widgets/editor_test.dart b/test/widgets/editor_test.dart new file mode 100644 index 00000000..3fd425fc --- /dev/null +++ b/test/widgets/editor_test.dart @@ -0,0 +1,82 @@ +import 'dart:convert' show jsonDecode; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill/flutter_quill_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late QuillController controller; + + setUp(() { + controller = QuillController.basic(); + }); + + tearDown(() { + controller.dispose(); + }); + + group('QuillEditor', () { + testWidgets('Keyboard entered text is stored in document', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: QuillEditor.basic(controller: controller, readOnly: false), + ), + ); + await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); + + expect(controller.document.toPlainText(), 'test\n'); + }); + + testWidgets('insertContent is handled correctly', (tester) async { + String? latestUri; + await tester.pumpWidget( + MaterialApp( + home: QuillEditor( + controller: controller, + focusNode: FocusNode(), + scrollController: ScrollController(), + scrollable: true, + padding: const EdgeInsets.all(0), + autoFocus: true, + readOnly: false, + expands: true, + contentInsertionConfiguration: ContentInsertionConfiguration( + onContentInserted: (content) { + latestUri = content.uri; + }, + allowedMimeTypes: const ['image/gif'], + ), + ), + ), + ); + await tester.tap(find.byType(QuillEditor)); + await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); + await tester.idle(); + + const uri = + 'content://com.google.android.inputmethod.latin.fileprovider/test.gif'; + final messageBytes = + const JSONMessageCodec().encodeMessage({ + 'args': [ + -1, + 'TextInputAction.commitContent', + jsonDecode( + '{"mimeType": "image/gif", "data": [0,1,0,1,0,1,0,0,0], "uri": "$uri"}'), + ], + 'method': 'TextInputClient.performAction', + }); + + Object? error; + try { + await tester.binding.defaultBinaryMessenger + .handlePlatformMessage('flutter/textinput', messageBytes, (_) {}); + } catch (e) { + error = e; + } + expect(error, isNull); + expect(latestUri, equals(uri)); + }); + }); +} From ba809c823439ffddf105cdb8f275af678c85c073 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Tue, 13 Jun 2023 12:49:25 -0700 Subject: [PATCH 34/36] Upgrade to 7.2.5 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58f1e223..f0b10bbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [7.2.5] +- Always use text cursor for editor on desktop. + # [7.2.4] - Fixed keepStyleOnNewLine. diff --git a/pubspec.yaml b/pubspec.yaml index db62be2b..432ba8ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.2.4 +version: 7.2.5 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 3fb5b7a0a23e78383a024e7871fea6c43db7cff6 Mon Sep 17 00:00:00 2001 From: Richard Marshall Date: Tue, 13 Jun 2023 16:25:28 -0700 Subject: [PATCH 35/36] Style custom toolbar buttons like builtins (#1267) --- lib/src/widgets/toolbar.dart | 28 ++++++-------- lib/src/widgets/toolbar/custom_button.dart | 43 +++++++++++++++++++++ lib/src/widgets/toolbar/history_button.dart | 2 +- lib/src/widgets/toolbar/indent_button.dart | 2 +- lib/src/widgets/toolbar/search_button.dart | 2 +- test/bug_fix_test.dart | 35 +++++++++++++++++ 6 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 lib/src/widgets/toolbar/custom_button.dart diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index fb01c4df..91521fb0 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -11,13 +11,13 @@ import 'embeds.dart'; import 'toolbar/arrow_indicated_button_list.dart'; import 'toolbar/clear_format_button.dart'; import 'toolbar/color_button.dart'; +import 'toolbar/custom_button.dart'; import 'toolbar/enum.dart'; import 'toolbar/history_button.dart'; import 'toolbar/indent_button.dart'; import 'toolbar/link_style_button.dart'; import 'toolbar/quill_font_family_button.dart'; import 'toolbar/quill_font_size_button.dart'; -import 'toolbar/quill_icon_button.dart'; import 'toolbar/search_button.dart'; import 'toolbar/select_alignment_button.dart'; import 'toolbar/select_header_style_button.dart'; @@ -26,6 +26,7 @@ import 'toolbar/toggle_style_button.dart'; export 'toolbar/clear_format_button.dart'; export 'toolbar/color_button.dart'; +export 'toolbar/custom_button.dart'; export 'toolbar/history_button.dart'; export 'toolbar/indent_button.dart'; export 'toolbar/link_style_button.dart'; @@ -566,19 +567,14 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { QuillDivider(axis, color: sectionDividerColor, space: sectionDividerSpace), for (var customButton in customButtons) - QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: toolbarIconSize * kIconButtonFactor, - icon: Icon( - customButton.icon, - size: toolbarIconSize, - color: customButton.iconColor, - ), - tooltip: customButton.tooltip, - borderRadius: iconTheme?.borderRadius ?? 2, + CustomButton( onPressed: customButton.onTap, - afterPressed: afterButtonPressed, + icon: customButton.icon, + iconColor: customButton.iconColor, + iconSize: toolbarIconSize, + iconTheme: iconTheme, + afterButtonPressed: afterButtonPressed, + tooltip: customButton.tooltip, ), ], ); @@ -650,7 +646,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { /// The divider which is used for separation of buttons in the toolbar. /// /// It can be used outside of this package, for example when user does not use -/// [QuillToolbar.basic] and compose toolbat's children on its own. +/// [QuillToolbar.basic] and compose toolbar's children on its own. class QuillDivider extends StatelessWidget { const QuillDivider( this.axis, { @@ -659,11 +655,11 @@ class QuillDivider extends StatelessWidget { this.space, }) : super(key: key); - /// Provides a horizonal divider for vertical toolbar. + /// Provides a horizontal divider for vertical toolbar. const QuillDivider.horizontal({Color? color, double? space}) : this(Axis.horizontal, color: color, space: space); - /// Provides a horizonal divider for horizontal toolbar. + /// Provides a horizontal divider for horizontal toolbar. const QuillDivider.vertical({Color? color, double? space}) : this(Axis.vertical, color: color, space: space); diff --git a/lib/src/widgets/toolbar/custom_button.dart b/lib/src/widgets/toolbar/custom_button.dart new file mode 100644 index 00000000..614c79bc --- /dev/null +++ b/lib/src/widgets/toolbar/custom_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import '../../models/themes/quill_icon_theme.dart'; +import '../toolbar.dart'; + +class CustomButton extends StatelessWidget { + const CustomButton({ + required this.onPressed, + required this.icon, + this.iconColor, + this.iconSize = kDefaultIconSize, + this.iconTheme, + this.afterButtonPressed, + this.tooltip, + Key? key, + }) : super(key: key); + + final VoidCallback? onPressed; + final IconData? icon; + final Color? iconColor; + final double iconSize; + final QuillIconTheme? iconTheme; + final VoidCallback? afterButtonPressed; + final String? tooltip; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * kIconButtonFactor, + icon: Icon(icon, size: iconSize, color: iconColor), + tooltip: tooltip, + borderRadius: iconTheme?.borderRadius ?? 2, + onPressed: onPressed, + afterPressed: afterButtonPressed, + fillColor: iconTheme?.iconUnselectedFillColor ?? theme.canvasColor, + ); + } +} diff --git a/lib/src/widgets/toolbar/history_button.dart b/lib/src/widgets/toolbar/history_button.dart index 6d3c29ad..909842c9 100644 --- a/lib/src/widgets/toolbar/history_button.dart +++ b/lib/src/widgets/toolbar/history_button.dart @@ -46,7 +46,7 @@ class _HistoryButtonState extends State { tooltip: widget.tooltip, highlightElevation: 0, hoverElevation: 0, - size: widget.iconSize * 1.77, + size: widget.iconSize * kIconButtonFactor, icon: Icon(widget.icon, size: widget.iconSize, color: _iconColor), fillColor: fillColor, borderRadius: widget.iconTheme?.borderRadius ?? 2, diff --git a/lib/src/widgets/toolbar/indent_button.dart b/lib/src/widgets/toolbar/indent_button.dart index 1ce83e99..89b57d4a 100644 --- a/lib/src/widgets/toolbar/indent_button.dart +++ b/lib/src/widgets/toolbar/indent_button.dart @@ -42,7 +42,7 @@ class _IndentButtonState extends State { tooltip: widget.tooltip, highlightElevation: 0, hoverElevation: 0, - size: widget.iconSize * 1.77, + size: widget.iconSize * kIconButtonFactor, icon: Icon(widget.icon, size: widget.iconSize, color: iconColor), fillColor: iconFillColor, borderRadius: widget.iconTheme?.borderRadius ?? 2, diff --git a/lib/src/widgets/toolbar/search_button.dart b/lib/src/widgets/toolbar/search_button.dart index 9233cf45..fc0dc79f 100644 --- a/lib/src/widgets/toolbar/search_button.dart +++ b/lib/src/widgets/toolbar/search_button.dart @@ -43,7 +43,7 @@ class SearchButton extends StatelessWidget { icon: Icon(icon, size: iconSize, color: iconColor), highlightElevation: 0, hoverElevation: 0, - size: iconSize * 1.77, + size: iconSize * kIconButtonFactor, fillColor: iconFillColor, borderRadius: iconTheme?.borderRadius ?? 2, onPressed: () => _onPressedHandler(context), diff --git a/test/bug_fix_test.dart b/test/bug_fix_test.dart index ecbcad2b..0689476e 100644 --- a/test/bug_fix_test.dart +++ b/test/bug_fix_test.dart @@ -5,6 +5,41 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('Bug fix', () { + group( + '1266 - QuillToolbar.basic() custom buttons do not have correct fill' + 'color set', () { + testWidgets('fillColor of custom buttons and builtin buttons match', + (tester) async { + const tooltip = 'custom button'; + + await tester.pumpWidget(MaterialApp( + home: QuillToolbar.basic( + showRedo: false, + controller: QuillController.basic(), + customButtons: [const QuillCustomButton(tooltip: tooltip)], + ))); + + final builtinFinder = find.descendant( + of: find.byType(HistoryButton), + matching: find.byType(QuillIconButton), + matchRoot: true); + expect(builtinFinder, findsOneWidget); + final builtinButton = + builtinFinder.evaluate().first.widget as QuillIconButton; + + final customFinder = find.descendant( + of: find.byType(QuillToolbar), + matching: find.byWidgetPredicate((widget) => + widget is QuillIconButton && widget.tooltip == tooltip), + matchRoot: true); + expect(customFinder, findsOneWidget); + final customButton = + customFinder.evaluate().first.widget as QuillIconButton; + + expect(customButton.fillColor, equals(builtinButton.fillColor)); + }); + }); + group('1189 - The provided text position is not in the current node', () { late QuillController controller; late QuillEditor editor; From a878088293c7e7fec601a9b369195958c23eb1e4 Mon Sep 17 00:00:00 2001 From: Cheryl Date: Tue, 13 Jun 2023 16:32:26 -0700 Subject: [PATCH 36/36] Upgrade to 7.2.6 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0b10bbe..6d511902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [7.2.6] +- Style custom toolbar buttons like builtins. + # [7.2.5] - Always use text cursor for editor on desktop. diff --git a/pubspec.yaml b/pubspec.yaml index 432ba8ae..f9b266dc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.2.5 +version: 7.2.6 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill