From 4470a75def663014d10daada62e254fea3dc6bda Mon Sep 17 00:00:00 2001 From: Ahmed Hnewa <73608287+freshtechtips@users.noreply.github.com> Date: Thu, 19 Oct 2023 14:00:16 +0300 Subject: [PATCH] Improvemenets and allow to change the default text selection theme --- example/android/app/build.gradle | 4 +- example/lib/pages/home_page.dart | 24 ++--- .../lib/embeds/builders.dart | 71 +++--------- .../lib/embeds/toolbar/camera_button.dart | 6 +- .../lib/embeds/toolbar/image_video_utils.dart | 54 ++++++---- .../lib/embeds/toolbar/media_button.dart | 91 +++++++++++----- .../lib/flutter_quill_extensions.dart | 27 ++++- .../lib/utils/quill_utils.dart | 2 - flutter_quill_extensions/pubspec.yaml | 4 +- lib/src/models/documents/nodes/line.dart | 2 +- lib/src/models/documents/nodes/node.dart | 4 +- lib/src/models/documents/style.dart | 8 +- lib/src/translations/toolbar.i18n.dart | 101 ++++++++++++++++++ lib/src/utils/platform.dart | 6 ++ lib/src/widgets/controller.dart | 6 +- lib/src/widgets/default_styles.dart | 9 +- lib/src/widgets/editor.dart | 13 ++- lib/src/widgets/raw_editor.dart | 74 ++++++++----- lib/src/widgets/text_block.dart | 76 +++++++------ .../widgets/toolbar/link_style_button.dart | 84 ++++++++++----- .../widgets/toolbar/quill_icon_button.dart | 3 +- lib/src/widgets/toolbar/search_dialog.dart | 16 ++- test/widgets/controller_test.dart | 34 +++--- 23 files changed, 479 insertions(+), 240 deletions(-) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index bd2115c8..89c45d69 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -42,7 +42,7 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + // I removed the todo because it's not required for this example applicationId "com.example.app" minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion @@ -53,7 +53,7 @@ android { buildTypes { release { - // TODO: Add your own signing config for the release build. + // I removed the todo because it's not required for this example // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug } diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 2367719d..851491bb 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -190,23 +190,23 @@ class _HomePageState extends State { onTapUp: (details, p1) { return _onTripleClickSelection(); }, - customStyles: DefaultStyles( + customStyles: const DefaultStyles( h1: DefaultTextBlockStyle( - const TextStyle( + TextStyle( fontSize: 32, color: Colors.black, height: 1.15, fontWeight: FontWeight.w300, ), - const VerticalSpacing(16, 0), - const VerticalSpacing(0, 0), + VerticalSpacing(16, 0), + VerticalSpacing(0, 0), null), - sizeSmall: const TextStyle(fontSize: 9), - subscript: const TextStyle( + sizeSmall: TextStyle(fontSize: 9), + subscript: TextStyle( fontFamily: 'SF-UI-Display', fontFeatures: [FontFeature.subscripts()], ), - superscript: const TextStyle( + superscript: TextStyle( fontFamily: 'SF-UI-Display', fontFeatures: [FontFeature.superscripts()], ), @@ -230,18 +230,18 @@ class _HomePageState extends State { onTapUp: (details, p1) { return _onTripleClickSelection(); }, - customStyles: DefaultStyles( + customStyles: const DefaultStyles( h1: DefaultTextBlockStyle( - const TextStyle( + TextStyle( fontSize: 32, color: Colors.black, height: 1.15, fontWeight: FontWeight.w300, ), - const VerticalSpacing(16, 0), - const VerticalSpacing(0, 0), + VerticalSpacing(16, 0), + VerticalSpacing(0, 0), null), - sizeSmall: const TextStyle(fontSize: 9), + sizeSmall: TextStyle(fontSize: 9), ), embedBuilders: [ ...defaultEmbedBuildersWeb, diff --git a/flutter_quill_extensions/lib/embeds/builders.dart b/flutter_quill_extensions/lib/embeds/builders.dart index 5d9e0236..8ea23202 100644 --- a/flutter_quill_extensions/lib/embeds/builders.dart +++ b/flutter_quill_extensions/lib/embeds/builders.dart @@ -55,10 +55,6 @@ class ImageEmbedBuilder extends EmbedBuilder { OptionalSize? imageSize; final style = node.style.attributes['style']; - // TODO: Please use the one from [Attribute.margin] - const marginKey = 'margin'; - // TODO: Please use the one from [Attribute.alignment] - const alignmentKey = 'alignment'; if (style != null) { final attrs = base.isMobile() ? base.parseKeyValuePairs(style.value.toString(), { @@ -70,8 +66,8 @@ class ImageEmbedBuilder extends EmbedBuilder { : base.parseKeyValuePairs(style.value.toString(), { Attribute.width.key, Attribute.height.key, - marginKey, - alignmentKey, + Attribute.margin, + Attribute.alignment, }); if (attrs.isNotEmpty) { final width = double.tryParse( @@ -88,10 +84,10 @@ class ImageEmbedBuilder extends EmbedBuilder { ); final alignment = base.getAlignment(base.isMobile() ? attrs[Attribute.mobileAlignment] - : attrs[alignmentKey]); + : attrs[Attribute.alignment]); final margin = (base.isMobile() ? double.tryParse(Attribute.mobileMargin) - : double.tryParse(marginKey)) ?? + : double.tryParse(Attribute.margin)) ?? 0.0; assert( @@ -198,57 +194,14 @@ class ImageEmbedBuilder extends EmbedBuilder { controller, controller.selection.start, ); - // For desktop - String _replaceStyleStringWithSize( - String s, - double width, - double height, - ) { - final result = {}; - final pairs = s.split(';'); - for (final pair in pairs) { - final _index = pair.indexOf(':'); - if (_index < 0) { - continue; - } - final _key = - pair.substring(0, _index).trim(); - result[_key] = - pair.substring(_index + 1).trim(); - } - - result[Attribute.width.key] = - width.toString(); - result[Attribute.height.key] = - height.toString(); - final sb = StringBuffer(); - for (final pair in result.entries) { - sb - ..write(pair.key) - ..write(': ') - ..write(pair.value) - ..write('; '); - } - return sb.toString(); - } - - // TODO: When update flutter_quill - // we should update flutter_quill_extensions - // to use the latest version and use - // base.replaceStyleStringWithSize() - // instead of replaceStyleString - - final attr = base.isMobile() - ? base.replaceStyleString( - getImageStyleString(controller), - w, - h, - ) - : _replaceStyleStringWithSize( - getImageStyleString(controller), - w, - h, - ); + + final attr = + base.replaceStyleStringWithSize( + getImageStyleString(controller), + width: w, + height: h, + isMobile: base.isMobile(), + ); controller ..skipRequestKeyboard = true ..formatText( diff --git a/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart index b9cb4458..d654425c 100644 --- a/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart +++ b/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart @@ -146,11 +146,13 @@ class CameraButton extends StatelessWidget { break; case MediaPickSetting.Gallery: throw ArgumentError( - 'Invalid MediaSetting for the camera button', + 'Invalid MediaSetting for the camera button.\n' + 'gallery is not related to camera button', ); case MediaPickSetting.Link: throw ArgumentError( - 'Invalid MediaSetting for the camera button', + 'Invalid MediaSetting for the camera button.\n' + 'link is not related to camera button', ); } } diff --git a/flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart b/flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart index f7739ce9..66045a51 100644 --- a/flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart +++ b/flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart @@ -35,17 +35,22 @@ class LinkDialogState extends State { super.initState(); _link = widget.link ?? ''; _controller = TextEditingController(text: _link); - // TODO: Consider replace the default Regex with this one - // Since that is not the reason I sent the changes then I will not edit it - // TODO: Consider use one of those as default or provide a - // way to custmize the check, that are not based on RegExp, - // I already implemented one so tell me if you are interested + final defaultLinkNonSecureRegExp = RegExp( + r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)', + caseSensitive: false, + ); // Not secure + // final defaultLinkRegExp = RegExp( + // r'https://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)', + // caseSensitive: false, + // ); // Secure + _linkRegExp = widget.linkRegExp ?? defaultLinkNonSecureRegExp; + } - // final defaultLinkNonSecureRegExp = RegExp(r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Not secure - // final defaultLinkRegExp = RegExp(r'https://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Secure - // _linkRegExp = widget.linkRegExp ?? defaultLinkRegExp; - _linkRegExp = widget.linkRegExp ?? AutoFormatMultipleLinksRule.linkRegExp; + @override + void dispose() { + _controller.dispose(); + super.dispose(); } @override @@ -53,23 +58,29 @@ class LinkDialogState extends State { return AlertDialog( backgroundColor: widget.dialogTheme?.dialogBackgroundColor, content: TextField( - keyboardType: TextInputType.multiline, + keyboardType: TextInputType.url, + textInputAction: TextInputAction.done, maxLines: null, style: widget.dialogTheme?.inputTextStyle, decoration: InputDecoration( labelText: 'Paste a link'.i18n, + hintText: 'Please enter a valid image url'.i18n, labelStyle: widget.dialogTheme?.labelTextStyle, floatingLabelStyle: widget.dialogTheme?.labelTextStyle, ), autofocus: true, onChanged: _linkChanged, controller: _controller, + onEditingComplete: () { + if (!_canPress()) { + return; + } + _applyLink(); + }, ), actions: [ TextButton( - onPressed: _link.isNotEmpty && _linkRegExp.hasMatch(_link) - ? _applyLink - : null, + onPressed: _canPress() ? _applyLink : null, child: Text( 'Ok'.i18n, style: widget.dialogTheme?.labelTextStyle, @@ -88,6 +99,10 @@ class LinkDialogState extends State { void _applyLink() { Navigator.pop(context, _link.trim()); } + + bool _canPress() { + return _link.isNotEmpty && _linkRegExp.hasMatch(_link); + } } class ImageVideoUtils { @@ -179,12 +194,13 @@ class ImageVideoUtils { /// For video picking logic static Future handleVideoButtonTap( - BuildContext context, - QuillController controller, - ImageSource videoSource, - OnVideoPickCallback onVideoPickCallback, - {FilePickImpl? filePickImpl, - WebVideoPickImpl? webVideoPickImpl}) async { + BuildContext context, + QuillController controller, + ImageSource videoSource, + OnVideoPickCallback onVideoPickCallback, { + FilePickImpl? filePickImpl, + WebVideoPickImpl? webVideoPickImpl, + }) async { final index = controller.selection.baseOffset; final length = controller.selection.extentOffset - index; diff --git a/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart index 1bb1673f..5b54f8a0 100644 --- a/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart +++ b/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart @@ -9,6 +9,7 @@ import 'package:flutter_quill/translations.dart'; import 'package:image_picker/image_picker.dart'; import '../embed_types.dart'; +import 'image_video_utils.dart'; /// Widget which combines [ImageButton] and [VideButton] widgets. This widget /// has more customization and uses dialog similar to one which is used @@ -16,6 +17,11 @@ import '../embed_types.dart'; class MediaButton extends StatelessWidget { const MediaButton({ required this.controller, + required this.onImagePickCallback, + required this.onVideoPickCallback, + required this.filePickImpl, + required this.webImagePickImpl, + required this.webVideoPickImpl, required this.icon, this.type = QuillMediaType.image, this.iconSize = kDefaultIconSize, @@ -73,6 +79,11 @@ class MediaButton extends StatelessWidget { final AutovalidateMode autovalidateMode; final String? validationMessage; + final OnImagePickCallback onImagePickCallback; + final FilePickImpl? filePickImpl; + final WebImagePickImpl? webImagePickImpl; + final OnVideoPickCallback onVideoPickCallback; + final WebVideoPickImpl? webVideoPickImpl; @override Widget build(BuildContext context) { @@ -94,24 +105,48 @@ class MediaButton extends StatelessWidget { } Future _onPressedHandler(BuildContext context) async { - if (onMediaPickedCallback != null) { - final mediaSource = await showDialog( - context: context, - builder: (_) => MediaSourceSelectorDialog( - dialogTheme: dialogTheme, - galleryButtonText: galleryButtonText, - linkButtonText: linkButtonText, - ), - ); - if (mediaSource != null) { - if (mediaSource == MediaPickSetting.Gallery) { - await _pickImage(); - } else { - _inputLink(context); - } - } - } else { + if (onMediaPickedCallback == null) { _inputLink(context); + return; + } + final mediaSource = await showDialog( + context: context, + builder: (_) => MediaSourceSelectorDialog( + dialogTheme: dialogTheme, + galleryButtonText: galleryButtonText, + linkButtonText: linkButtonText, + ), + ); + if (mediaSource == null) { + return; + } + switch (mediaSource) { + case MediaPickSetting.Gallery: + await _pickImage(); + break; + case MediaPickSetting.Link: + _inputLink(context); + break; + case MediaPickSetting.Camera: + await ImageVideoUtils.handleImageButtonTap( + context, + controller, + ImageSource.camera, + onImagePickCallback, + filePickImpl: filePickImpl, + webImagePickImpl: webImagePickImpl, + ); + break; + case MediaPickSetting.Video: + await ImageVideoUtils.handleVideoButtonTap( + context, + controller, + ImageSource.camera, + onVideoPickCallback, + filePickImpl: filePickImpl, + webVideoPickImpl: webVideoPickImpl, + ); + break; } } @@ -281,16 +316,18 @@ class _MediaLinkDialogState extends State { child: Padding( padding: widget.dialogTheme?.linkDialogPadding ?? const EdgeInsets.all(16), - child: isWrappable - ? Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, - children: children, - ) - : Row( - children: children, - ), + child: Form( + child: isWrappable + ? Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, + children: children, + ) + : Row( + children: children, + ), + ), ), ), ); diff --git a/flutter_quill_extensions/lib/flutter_quill_extensions.dart b/flutter_quill_extensions/lib/flutter_quill_extensions.dart index e4c77396..0fc6a54e 100644 --- a/flutter_quill_extensions/lib/flutter_quill_extensions.dart +++ b/flutter_quill_extensions/lib/flutter_quill_extensions.dart @@ -9,6 +9,7 @@ import 'embeds/embed_types.dart'; import 'embeds/toolbar/camera_button.dart'; import 'embeds/toolbar/formula_button.dart'; import 'embeds/toolbar/image_button.dart'; +import 'embeds/toolbar/media_button.dart'; import 'embeds/toolbar/video_button.dart'; export 'embeds/embed_types.dart'; @@ -233,6 +234,7 @@ class FlutterQuillEmbeds { bool showImageButton = true, bool showVideoButton = true, bool showCameraButton = true, + bool showImageMediaButton = false, bool showFormulaButton = false, String? imageButtonTooltip, String? videoButtonTooltip, @@ -242,6 +244,7 @@ class FlutterQuillEmbeds { OnVideoPickCallback? onVideoPickCallback, MediaPickSettingSelector? mediaPickSettingSelector, MediaPickSettingSelector? cameraPickSettingSelector, + MediaPickedCallback? onImageMediaPickedCallback, FilePickImpl? filePickImpl, WebImagePickImpl? webImagePickImpl, WebVideoPickImpl? webVideoPickImpl, @@ -292,6 +295,28 @@ class FlutterQuillEmbeds { cameraPickSettingSelector: cameraPickSettingSelector, iconTheme: iconTheme, ), + if (showImageMediaButton) + (controller, toolbarIconSize, iconTheme, dialogTheme) => MediaButton( + controller: controller, + dialogTheme: dialogTheme, + iconTheme: iconTheme, + iconSize: toolbarIconSize, + onMediaPickedCallback: onImageMediaPickedCallback, + onImagePickCallback: onImagePickCallback ?? + (throw ArgumentError.notNull( + 'onImagePickCallback is required when showCameraButton is' + ' true', + )), + onVideoPickCallback: onVideoPickCallback ?? + (throw ArgumentError.notNull( + 'onVideoPickCallback is required when showCameraButton is' + ' true', + )), + filePickImpl: filePickImpl, + webImagePickImpl: webImagePickImpl, + webVideoPickImpl: webVideoPickImpl, + icon: Icons.perm_media, + ), if (showFormulaButton) (controller, toolbarIconSize, iconTheme, dialogTheme) => FormulaButton( @@ -301,6 +326,6 @@ class FlutterQuillEmbeds { controller: controller, iconTheme: iconTheme, dialogTheme: dialogTheme, - ) + ), ]; } diff --git a/flutter_quill_extensions/lib/utils/quill_utils.dart b/flutter_quill_extensions/lib/utils/quill_utils.dart index 83096b28..1ee9caff 100644 --- a/flutter_quill_extensions/lib/utils/quill_utils.dart +++ b/flutter_quill_extensions/lib/utils/quill_utils.dart @@ -74,8 +74,6 @@ class QuillImageUtilities { final newImageFileExtensionWithDot = path.extension(cachedImagePath); final dateTimeAsString = DateTime.now().toIso8601String(); - // TODO: You might want to make it easier for the developer to change - // the newImageFileName, but he can rename it anyway final newImageFileName = '$startOfEachFile$dateTimeAsString$newImageFileExtensionWithDot'; final newImagePath = path.join(saveDirectory.path, newImageFileName); diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index c557c735..2ec76a9a 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -12,7 +12,9 @@ dependencies: flutter: sdk: flutter - flutter_quill: ^7.4.13 + # flutter_quill: ^7.4.14 + flutter_quill: + path: /Users/ahmedhnewa/development/playground/framework_based/flutter/flutter-quill http: ^1.1.0 image_picker: ">=1.0.4" diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index 539f3c5d..724425a4 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -348,7 +348,7 @@ class Line extends Container { /// In essence, it is INTERSECTION of each individual segment's styles Style collectStyle(int offset, int len) { final local = math.min(length - offset, len); - var result = Style(); + var result = const Style(); final excluded = {}; void _handle(Style style) { diff --git a/lib/src/models/documents/nodes/node.dart b/lib/src/models/documents/nodes/node.dart index 6a057a90..098f1241 100644 --- a/lib/src/models/documents/nodes/node.dart +++ b/lib/src/models/documents/nodes/node.dart @@ -22,7 +22,7 @@ abstract class Node extends LinkedListEntry { Container? parent; Style get style => _style; - Style _style = Style(); + Style _style = const Style(); /// Returns `true` if this node is the first node in the [parent] list. bool get isFirst => list!.first == this; @@ -78,7 +78,7 @@ abstract class Node extends LinkedListEntry { } void clearStyle() { - _style = Style(); + _style = const Style(); } @override diff --git a/lib/src/models/documents/style.dart b/lib/src/models/documents/style.dart index 58469a2a..d3e1247a 100644 --- a/lib/src/models/documents/style.dart +++ b/lib/src/models/documents/style.dart @@ -1,19 +1,21 @@ import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart' show immutable; import 'package:quiver/core.dart'; import 'attribute.dart'; /* Collection of style attributes */ +@immutable class Style { - Style() : _attributes = {}; + const Style() : _attributes = const {}; - Style.attr(this._attributes); + const Style.attr(this._attributes); final Map _attributes; static Style fromJson(Map? attributes) { if (attributes == null) { - return Style(); + return const Style(); } final result = attributes.map((key, dynamic value) { diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 6ad15ec9..d9957111 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -1,5 +1,7 @@ import 'package:i18n_extension/i18n_extension.dart'; +// TODO: The translation need to be changed and re-reviewd + extension Localization on String { static final _t = Translations.byLocale('en') + { @@ -70,6 +72,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'en_us': { 'Paste a link': 'Paste a link', @@ -138,6 +143,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'ar': { 'Paste a link': 'نسخ الرابط', @@ -210,6 +218,9 @@ extension Localization on String { 'Saved using the local storage': 'تم الحفظ باستخدام وحدة التخزين المحلية', 'Error while saving image': 'حدث خطأ أثناء حفظ الصورة', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'da': { 'Paste a link': 'Indsæt link', @@ -275,6 +286,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'de': { 'Paste a link': 'Link hinzufügen', @@ -340,6 +354,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'fr': { 'Paste a link': 'Coller un lien', @@ -405,6 +422,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'zh_cn': { 'Paste a link': '粘贴链接', @@ -470,6 +490,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'zh_hk': { 'Paste a link': '貼上連結', @@ -535,6 +558,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'ja': { 'Paste a link': 'リンクをペースト', @@ -600,6 +626,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'ko': { 'Paste a link': '링크를 붙여넣어 주세요.', @@ -665,6 +694,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'ru': { 'Paste a link': 'Вставить ссылку', @@ -730,6 +762,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'es': { 'Paste a link': 'Pega un enlace', @@ -795,6 +830,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'tr': { 'Paste a link': 'Bağlantıyı Yapıştır', @@ -860,6 +898,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'uk': { 'Paste a link': 'Вставити посилання', @@ -925,6 +966,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'pt': { 'Paste a link': 'Colar um link', @@ -991,6 +1035,9 @@ extension Localization on String { 'Saved using the local storage': 'Guardado através do armazenamento local', 'Error while saving image': 'Erro a gravar imagem', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'pt_br': { 'Paste a link': 'Colar um link', @@ -1056,6 +1103,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'pl': { 'Paste a link': 'Wklej link', @@ -1121,6 +1171,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'vi': { 'Paste a link': 'Chèn liên kết', @@ -1186,6 +1239,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'ur': { 'Paste a link': 'لنک پیسٹ کریں', @@ -1251,6 +1307,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'id': { 'Paste a link': 'Tempel tautan', @@ -1316,6 +1375,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'no': { 'Paste a link': 'Lim inn lenke', @@ -1381,6 +1443,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'fa': { 'Paste a link': 'جایگذاری لینک', @@ -1446,6 +1511,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'hi': { 'Paste a link': 'लिंक पेस्ट करें', @@ -1511,6 +1579,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'nl': { 'Paste a link': 'Plak een link', @@ -1576,6 +1647,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'sr': { 'Paste a link': 'Nalepi vezu', @@ -1641,6 +1715,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'cs': { 'Paste a link': 'Vložit odkaz', @@ -1709,6 +1786,9 @@ extension Localization on String { 'Saved using the network': 'Uloženo pomocí sítě', 'Saved using local storage': 'Uloženo lokálně', 'Error while saving image': 'Chyba při ukládání obrázku', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'he': { 'Paste a link': 'הדבק את הלינק', @@ -1774,6 +1854,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'ms': { 'Paste a link': 'Tampal Pautan', @@ -1839,6 +1922,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'it': { 'Paste a link': 'Incolla un collegamento', @@ -1904,6 +1990,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'bn': { 'Paste a link': 'লিঙ্ক পেস্ট করুন', @@ -1972,6 +2061,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'tk': { 'Paste a link': 'Baglanyşygy goýuň', @@ -2040,6 +2132,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'bg': { 'Paste a link': 'Поставете връзка', @@ -2108,6 +2203,9 @@ extension Localization on String { 'Saved using the network': 'Saved using the network', 'Saved using the local storage': 'Saved using the local storage', 'Error while saving image': 'Error while saving image', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, 'sw': { 'Paste a link': 'Bandika Kiungo', @@ -2176,6 +2274,9 @@ extension Localization on String { 'Saved using the network': 'Imehifadhiwa kwa Kutumia Mtandao', 'Saved using the local storage': 'Imehifadhiwa kwa Hifadhi ya Ndani', 'Error while saving image': 'Hitilafu Wakati wa Kuhifadhi Picha', + 'Please enter a text for your link': "e.g., 'Learn more)", + 'Please enter the link url': "e.g., 'https://example.com'", + 'Please enter a valid image url': 'Please enter a valid image url' }, }; diff --git a/lib/src/utils/platform.dart b/lib/src/utils/platform.dart index 75965bdd..9bd5debb 100644 --- a/lib/src/utils/platform.dart +++ b/lib/src/utils/platform.dart @@ -29,6 +29,12 @@ bool isAppleOS([TargetPlatform? targetPlatform]) { }.contains(targetPlatform); } +bool isMacOS([TargetPlatform? targetPlatform]) { + if (kIsWeb) return false; + targetPlatform ??= defaultTargetPlatform; + return TargetPlatform.macOS == targetPlatform; +} + Future isIOSSimulator() async { if (!isAppleOS()) { return false; diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index b4127ff5..7965d5d2 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -72,7 +72,7 @@ class QuillController extends ChangeNotifier { /// Store any styles attribute that got toggled by the tap of a button /// and that has not been applied yet. /// It gets reset after each format action within the [document]. - Style toggledStyle = Style(); + Style toggledStyle = const Style(); bool ignoreFocusOnTextChange = false; @@ -405,7 +405,7 @@ class QuillController extends ChangeNotifier { ); toggledStyle = style.removeAll(ignoredStyles.toSet()); } else { - toggledStyle = Style(); + toggledStyle = const Style(); } onSelectionChanged?.call(textSelection); } @@ -426,5 +426,5 @@ class QuillController extends ChangeNotifier { } // Notify toolbar buttons directly with attributes - Map toolbarButtonToggler = {}; + Map toolbarButtonToggler = const {}; } diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index c2d6e878..d600b71d 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -34,8 +34,9 @@ class QuillStyles extends InheritedWidget { /// Style theme applied to a block of rich text, including single-line /// paragraphs. +@immutable class DefaultTextBlockStyle { - DefaultTextBlockStyle( + const DefaultTextBlockStyle( this.style, this.verticalSpacing, this.lineSpacing, @@ -124,8 +125,9 @@ class InlineCodeStyle { Object.hash(style, header1, header2, header3, backgroundColor, radius); } +@immutable class DefaultListBlockStyle extends DefaultTextBlockStyle { - DefaultListBlockStyle( + const DefaultListBlockStyle( TextStyle style, VerticalSpacing verticalSpacing, VerticalSpacing lineSpacing, @@ -136,8 +138,9 @@ class DefaultListBlockStyle extends DefaultTextBlockStyle { final QuillCheckboxBuilder? checkboxUIBuilder; } +@immutable class DefaultStyles { - DefaultStyles({ + const DefaultStyles({ this.h1, this.h2, this.h3, diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 11fd530d..23a58eb4 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -156,6 +156,7 @@ class QuillEditor extends StatefulWidget { required this.autoFocus, required this.readOnly, required this.expands, + this.textSelectionThemeData, this.showCursor, this.paintCursorAboveText, this.placeholder, @@ -199,6 +200,7 @@ class QuillEditor extends StatefulWidget { factory QuillEditor.basic({ required QuillController controller, required bool readOnly, + TextSelectionThemeData? textSelectionThemeData, Brightness? keyboardAppearance, Iterable? embedBuilders, EdgeInsetsGeometry padding = EdgeInsets.zero, @@ -217,6 +219,7 @@ class QuillEditor extends StatefulWidget { scrollController: ScrollController(), scrollable: true, focusNode: focusNode ?? FocusNode(), + textSelectionThemeData: textSelectionThemeData, autoFocus: autoFocus, readOnly: readOnly, expands: expands, @@ -455,6 +458,13 @@ class QuillEditor extends StatefulWidget { /// editorKey.currentState?.renderEditor.getLocalRectForCaret final GlobalKey? editorKey; + /// By default we will use + /// ``` + /// TextSelectionTheme.of(context) + /// ``` + /// to change it please pass a different value + final TextSelectionThemeData? textSelectionThemeData; + @override QuillEditorState createState() => QuillEditorState(); } @@ -477,7 +487,8 @@ class QuillEditorState extends State @override Widget build(BuildContext context) { final theme = Theme.of(context); - final selectionTheme = TextSelectionTheme.of(context); + final selectionTheme = + widget.textSelectionThemeData ?? TextSelectionTheme.of(context); TextSelectionControls textSelectionControls; bool paintCursorAboveText; diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 15483c88..3003a2a4 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -535,7 +535,11 @@ class RawEditorState extends EditorState minHeight: widget.minHeight ?? 0.0, maxHeight: widget.maxHeight ?? double.infinity); - final isMacOS = Theme.of(context).platform == TargetPlatform.macOS; + // Please notice that this change will make the check fixed + // so if we ovveride the platform in material app theme data + // it will not depend on it and doesn't change here but I don't think + // we need to + final isMacOS = isAppleOS(); return TextFieldTapRegion( enabled: widget.enableUnfocusOnTapOutside, @@ -915,31 +919,36 @@ class RawEditorState extends EditorState textDirection: getDirectionOfNode(node), child: editableTextLine)); } else if (node is Block) { final editableTextBlock = EditableTextBlock( - block: node, - controller: controller, + block: node, + controller: controller, + textDirection: getDirectionOfNode(node), + scrollBottomInset: widget.scrollBottomInset, + verticalSpacing: _getVerticalSpacingForBlock(node, _styles), + textSelection: controller.selection, + color: widget.selectionColor, + styles: _styles, + enableInteractiveSelection: widget.enableInteractiveSelection, + hasFocus: _hasFocus, + contentPadding: attrs.containsKey(Attribute.codeBlock.key) + ? const EdgeInsets.all(16) + : null, + embedBuilder: widget.embedBuilder, + linkActionPicker: _linkActionPicker, + onLaunchUrl: widget.onLaunchUrl, + cursorCont: _cursorCont, + indentLevelCounts: indentLevelCounts, + clearIndents: clearIndents, + onCheckboxTap: _handleCheckboxTap, + readOnly: widget.readOnly, + customStyleBuilder: widget.customStyleBuilder, + customLinkPrefixes: widget.customLinkPrefixes, + ); + result.add( + Directionality( textDirection: getDirectionOfNode(node), - scrollBottomInset: widget.scrollBottomInset, - verticalSpacing: _getVerticalSpacingForBlock(node, _styles), - textSelection: controller.selection, - color: widget.selectionColor, - styles: _styles, - enableInteractiveSelection: widget.enableInteractiveSelection, - hasFocus: _hasFocus, - contentPadding: attrs.containsKey(Attribute.codeBlock.key) - ? const EdgeInsets.all(16) - : null, - embedBuilder: widget.embedBuilder, - linkActionPicker: _linkActionPicker, - onLaunchUrl: widget.onLaunchUrl, - cursorCont: _cursorCont, - indentLevelCounts: indentLevelCounts, - clearIndents: clearIndents, - onCheckboxTap: _handleCheckboxTap, - readOnly: widget.readOnly, - customStyleBuilder: widget.customStyleBuilder, - customLinkPrefixes: widget.customLinkPrefixes); - result.add(Directionality( - textDirection: getDirectionOfNode(node), child: editableTextBlock)); + child: editableTextBlock, + ), + ); clearIndents = false; } else { @@ -1111,9 +1120,15 @@ class RawEditorState extends EditorState _styles = _styles!.merge(widget.customStyles!); } + // TODO: this might need some attention + _requestFocusIfShould(); + } + + Future _requestFocusIfShould() async { if (!_didAutoFocus && widget.autoFocus) { - FocusScope.of(context).autofocus(widget.focusNode); _didAutoFocus = true; + await Future.delayed(Duration.zero); + FocusScope.of(context).autofocus(widget.focusNode); } } @@ -2559,8 +2574,13 @@ class _OpenSearchAction extends ContextAction { @override Future invoke(OpenSearchIntent intent, [BuildContext? context]) async { + if (context == null) { + throw ArgumentError( + 'The context should not be null to use invoke() method', + ); + } await showDialog( - context: context!, + context: context, builder: (_) => SearchDialog(controller: state.controller, text: ''), ); } diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index 64dacb4f..fd265f45 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -104,14 +104,19 @@ class EditableTextBlock extends StatelessWidget { final defaultStyles = QuillStyles.getStyles(context, false); return _EditableBlock( - block: block, - textDirection: textDirection, - padding: verticalSpacing, - scrollBottomInset: scrollBottomInset, - decoration: _getDecorationForBlock(block, defaultStyles) ?? - const BoxDecoration(), - contentPadding: contentPadding, - children: _buildChildren(context, indentLevelCounts, clearIndents)); + block: block, + textDirection: textDirection, + padding: verticalSpacing, + scrollBottomInset: scrollBottomInset, + decoration: + _getDecorationForBlock(block, defaultStyles) ?? const BoxDecoration(), + contentPadding: contentPadding, + children: _buildChildren( + context, + indentLevelCounts, + clearIndents, + ), + ); } BoxDecoration? _getDecorationForBlock( @@ -138,32 +143,37 @@ class EditableTextBlock extends StatelessWidget { for (final line in Iterable.castFrom(block.children)) { index++; final editableTextLine = EditableTextLine( - line, - _buildLeading(context, line, index, indentLevelCounts, count), - TextLine( - line: line, - textDirection: textDirection, - embedBuilder: embedBuilder, - customStyleBuilder: customStyleBuilder, - styles: styles!, - readOnly: readOnly, - controller: controller, - linkActionPicker: linkActionPicker, - onLaunchUrl: onLaunchUrl, - customLinkPrefixes: customLinkPrefixes, - ), - _getIndentWidth(context, count), - _getSpacingForLine(line, index, count, defaultStyles), - textDirection, - textSelection, - color, - enableInteractiveSelection, - hasFocus, - MediaQuery.devicePixelRatioOf(context), - cursorCont); + line, + _buildLeading(context, line, index, indentLevelCounts, count), + TextLine( + line: line, + textDirection: textDirection, + embedBuilder: embedBuilder, + customStyleBuilder: customStyleBuilder, + styles: styles!, + readOnly: readOnly, + controller: controller, + linkActionPicker: linkActionPicker, + onLaunchUrl: onLaunchUrl, + customLinkPrefixes: customLinkPrefixes, + ), + _getIndentWidth(context, count), + _getSpacingForLine(line, index, count, defaultStyles), + textDirection, + textSelection, + color, + enableInteractiveSelection, + hasFocus, + MediaQuery.devicePixelRatioOf(context), + cursorCont, + ); final nodeTextDirection = getDirectionOfNode(line); - children.add(Directionality( - textDirection: nodeTextDirection, child: editableTextLine)); + children.add( + Directionality( + textDirection: nodeTextDirection, + child: editableTextLine, + ), + ); } return children.toList(growable: false); } diff --git a/lib/src/widgets/toolbar/link_style_button.dart b/lib/src/widgets/toolbar/link_style_button.dart index b5346b42..cd9471c4 100644 --- a/lib/src/widgets/toolbar/link_style_button.dart +++ b/lib/src/widgets/toolbar/link_style_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../../models/documents/attribute.dart'; import '../../models/rules/insert.dart'; @@ -189,46 +190,77 @@ class _LinkDialogState extends State<_LinkDialog> { _textController = TextEditingController(text: _text); } + @override + void dispose() { + _linkController.dispose(); + _textController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return AlertDialog( backgroundColor: widget.dialogTheme?.dialogBackgroundColor, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 8), - TextField( - keyboardType: TextInputType.multiline, - style: widget.dialogTheme?.inputTextStyle, - decoration: InputDecoration( + content: Form( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + TextFormField( + keyboardType: TextInputType.text, + style: widget.dialogTheme?.inputTextStyle, + decoration: InputDecoration( labelText: 'Text'.i18n, + hintText: 'Please enter a text for your link'.i18n, labelStyle: widget.dialogTheme?.labelTextStyle, - floatingLabelStyle: widget.dialogTheme?.labelTextStyle), - autofocus: true, - onChanged: _textChanged, - controller: _textController, - ), - const SizedBox(height: 16), - TextField( - keyboardType: TextInputType.multiline, - style: widget.dialogTheme?.inputTextStyle, - decoration: InputDecoration( + floatingLabelStyle: widget.dialogTheme?.labelTextStyle, + ), + autofocus: true, + onChanged: _textChanged, + controller: _textController, + textInputAction: TextInputAction.next, + autofillHints: [ + AutofillHints.name, + AutofillHints.url, + ], + ), + const SizedBox(height: 16), + TextFormField( + keyboardType: TextInputType.url, + style: widget.dialogTheme?.inputTextStyle, + decoration: InputDecoration( labelText: 'Link'.i18n, + hintText: 'Please enter the link url'.i18n, labelStyle: widget.dialogTheme?.labelTextStyle, - floatingLabelStyle: widget.dialogTheme?.labelTextStyle), - autofocus: true, - onChanged: _linkChanged, - controller: _linkController, - ), - ], + floatingLabelStyle: widget.dialogTheme?.labelTextStyle, + ), + onChanged: _linkChanged, + controller: _linkController, + textInputAction: TextInputAction.done, + autofillHints: [AutofillHints.url], + autocorrect: false, + onEditingComplete: () { + if (!_canPress()) { + return; + } + _applyLink(); + }, + ), + ], + ), ), - actions: [_okButton()], + actions: [ + _okButton(), + ], ); } Widget _okButton() { if (widget.action != null) { - return widget.action!.builder(_canPress(), _applyLink); + return widget.action!.builder( + _canPress(), + _applyLink, + ); } return TextButton( diff --git a/lib/src/widgets/toolbar/quill_icon_button.dart b/lib/src/widgets/toolbar/quill_icon_button.dart index 86c5b30b..52405702 100644 --- a/lib/src/widgets/toolbar/quill_icon_button.dart +++ b/lib/src/widgets/toolbar/quill_icon_button.dart @@ -35,7 +35,8 @@ class QuillIconButton extends StatelessWidget { child: RawMaterialButton( visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(borderRadius)), + borderRadius: BorderRadius.circular(borderRadius), + ), fillColor: fillColor, elevation: 0, hoverElevation: hoverElevation, diff --git a/lib/src/widgets/toolbar/search_dialog.dart b/lib/src/widgets/toolbar/search_dialog.dart index d9792a5c..136ec195 100644 --- a/lib/src/widgets/toolbar/search_dialog.dart +++ b/lib/src/widgets/toolbar/search_dialog.dart @@ -6,9 +6,12 @@ import '../../models/themes/quill_dialog_theme.dart'; import '../controller.dart'; class SearchDialog extends StatefulWidget { - const SearchDialog( - {required this.controller, this.dialogTheme, this.text, Key? key}) - : super(key: key); + const SearchDialog({ + required this.controller, + this.dialogTheme, + this.text, + Key? key, + }) : super(key: key); final QuillController controller; final QuillDialogTheme? dialogTheme; @@ -35,6 +38,12 @@ class _SearchDialogState extends State { _controller = TextEditingController(text: _text); } + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { var matchShown = ''; @@ -100,6 +109,7 @@ class _SearchDialogState extends State { autofocus: true, onChanged: _textChanged, textInputAction: TextInputAction.done, + keyboardType: TextInputType.text, onEditingComplete: _findText, controller: _controller, ), diff --git a/test/widgets/controller_test.dart b/test/widgets/controller_test.dart index 0221d32e..e2405d4b 100644 --- a/test/widgets/controller_test.dart +++ b/test/widgets/controller_test.dart @@ -89,8 +89,12 @@ void main() { ..indentSelection(true); // Should have both L1 and L2 indent attributes in selection. - expect(controller.getAllSelectionStyles(), - contains(Style().put(Attribute.indentL1).put(Attribute.indentL2))); + expect( + controller.getAllSelectionStyles(), + contains( + const Style().put(Attribute.indentL1).put(Attribute.indentL2), + ), + ); // Remaining lines should have no attributes. controller.updateSelection( @@ -98,7 +102,7 @@ void main() { baseOffset: 12, extentOffset: controller.document.toPlainText().length - 1), ChangeSource.LOCAL); - expect(controller.getAllSelectionStyles(), everyElement(Style())); + expect(controller.getAllSelectionStyles(), everyElement(const Style())); }); test('getAllIndividualSelectionStylesAndEmbed', () { @@ -110,7 +114,7 @@ void main() { final result = controller.getAllIndividualSelectionStylesAndEmbed(); expect(result.length, 2); expect(result[0].offset, 0); - expect(result[0].value, Style().put(Attribute.bold)); + expect(result[0].value, const Style().put(Attribute.bold)); expect((result[1].value as Embeddable).type, BlockEmbed.imageType); }); @@ -125,7 +129,7 @@ void main() { test('getAllSelectionStyles', () { controller.formatText(0, 2, Attribute.bold); expect(controller.getAllSelectionStyles(), - contains(Style().put(Attribute.bold))); + contains(const Style().put(Attribute.bold))); }); test('undo', () { @@ -133,7 +137,10 @@ void main() { controller.updateSelection( const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL); - expect(controller.document.toDelta(), Delta()..insert('data\n')); + expect( + controller.document.toDelta(), + Delta()..insert('data\n'), + ); controller ..addListener(() { listenerCalled = true; @@ -185,7 +192,7 @@ void main() { test('formatTextStyle', () { var listenerCalled = false; - final style = Style().put(Attribute.bold).put(Attribute.italic); + final style = const Style().put(Attribute.bold).put(Attribute.italic); controller ..addListener(() { listenerCalled = true; @@ -193,7 +200,8 @@ void main() { ..formatTextStyle(0, 2, style); expect(listenerCalled, isTrue); expect(controller.document.collectAllStyles(0, 2), contains(style)); - expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); + expect(controller.document.collectAllStyles(2, 4), + everyElement(const Style())); }); test('formatText', () { @@ -205,8 +213,9 @@ void main() { ..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())); + contains(const Style().put(Attribute.bold))); + expect(controller.document.collectAllStyles(2, 4), + everyElement(const Style())); }); test('formatSelection', () { @@ -220,8 +229,9 @@ void main() { ..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())); + contains(const Style().put(Attribute.bold))); + expect(controller.document.collectAllStyles(2, 4), + everyElement(const Style())); }); test('moveCursorToStart', () {