From 988c41bfa60e1370a70c582433f20255bff9816e Mon Sep 17 00:00:00 2001 From: Ahmed Hnewa <73608287+freshtechtips@users.noreply.github.com> Date: Thu, 19 Oct 2023 18:02:15 +0300 Subject: [PATCH] New changes and improvemenets (#1437) --- CHANGELOG.md | 54 +++++ README.md | 35 +++- example/android/app/build.gradle | 2 - .../android/app/src/main/AndroidManifest.xml | 8 +- 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 | 93 ++++++--- .../lib/flutter_quill_extensions.dart | 27 ++- .../lib/utils/quill_utils.dart | 2 - flutter_quill_extensions/pubspec.yaml | 6 +- 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/models/rules/format.dart | 36 +++- lib/src/models/rules/insert.dart | 121 +++++++++-- lib/src/models/rules/rule.dart | 27 ++- lib/src/translations/toolbar.i18n.dart | 101 +++++++++ lib/src/utils/platform.dart | 6 + lib/src/widgets/controller.dart | 14 +- lib/src/widgets/default_styles.dart | 9 +- lib/src/widgets/editor.dart | 13 +- lib/src/widgets/raw_editor.dart | 197 +++++++++++------- lib/src/widgets/text_block.dart | 76 ++++--- .../widgets/toolbar/link_style_button.dart | 85 +++++--- .../widgets/toolbar/link_style_button2.dart | 2 +- .../widgets/toolbar/quill_icon_button.dart | 3 +- lib/src/widgets/toolbar/search_dialog.dart | 16 +- test/widgets/controller_test.dart | 34 +-- 30 files changed, 804 insertions(+), 332 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99fef3b1..037eb159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,157 +1,210 @@ # [7.4.14] + - Custom style attrbuites for platforms other than mobile (alignment, margin, width, height) - Improve performance by reducing the number of widgets rebuilt by listening to media query for only the needed things, for example instead of using `MediaQuery.of(context).size`, now we are using `MediaQuery.sizeOf(context)` +- Bug fixes and other improvemenets +- Add MediaButton for picking the images only since the video one is not ready +- A new feature which allows customizing the text selection in quill editor which is useful for custom theme design system for custom app widget # [7.4.13] + - Fixed tab editing when in readOnly mode. # [7.4.12] + - Update the minimum version of device_info_plus to 9.1.0. # [7.4.11] + - Add sw locale. # [7.4.10] + - Update translations. # [7.4.9] + - Style recognition fixes. # [7.4.8] + - Upgrade dependencies. # [7.4.7] + - Add Vietnamese and German translations. # [7.4.6] + - Fix more null errors in Leaf.retain [#1394](https://github.com/singerdmx/flutter-quill/issues/1394) and Line.delete [#1395](https://github.com/singerdmx/flutter-quill/issues/1395). # [7.4.5] + - Fix null error in Container.insert [#1392](https://github.com/singerdmx/flutter-quill/issues/1392). # [7.4.4] + - Fix extra padding on checklists [#1131](https://github.com/singerdmx/flutter-quill/issues/1131). # [7.4.3] + - Fixed a space input error on iPad. # [7.4.2] + - Fix bug with keepStyleOnNewLine for link. # [7.4.1] + - Fix toolbar dividers condition. # [7.4.0] + - Support Flutter version 3.13.0. # [7.3.3] + - Updated Dependencies conflicting. # [7.3.2] + - Added builder for custom button in _LinkDialog. # [7.3.1] + - Added case sensitive and whole word search parameters. - Added wrap around. - Moved search dialog to the bottom in order not to override the editor and the text found. - Other minor search dialog enhancements. # [7.3.0] + - Add default attributes to basic factory. # [7.2.19] + - Feat/link regexp. # [7.2.18] + - Fix paste block text in words apply same style. # [7.2.17] + - Fix paste text mess up style. - Add support copy/cut block text. # [7.2.16] + - Allow for custom context menu. # [7.2.15] + - Add flutter_quill.delta library which only exposes Delta datatype. # [7.2.14] + - Fix errors when the editor is used in the `screenshot` package. # [7.2.13] + - Fix around image can't delete line break. # [7.2.12] + - Add support for copy/cut select image and text together. # [7.2.11] + - Add affinity for localPosition. # [7.2.10] + - LINE._getPlainText queryChild inclusive=false. # [7.2.9] + - Add toPlainText method to `EmbedBuilder`. # [7.2.8] + - Add custom button widget in toolbar. # [7.2.7] + - Fix language code of Japan. # [7.2.6] + - Style custom toolbar buttons like builtins. # [7.2.5] + - Always use text cursor for editor on desktop. # [7.2.4] + - Fixed keepStyleOnNewLine. # [7.2.3] + - Get pixel ratio from view. # [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. # [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. # [7.1.19] + - Fix Rtl leading alignment problem. # [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] + - Fixed subscript key from 'sup' to 'sub'. # [7.1.15] + - Fixed a bug introduced in 7.1.7 where each section in `QuillToolbar` was displayed on its own line. # [7.1.14] + - Add indents change for multiline selection. # [7.1.13] + - Add custom recognizer. # [7.1.12] + - Add superscript and subscript styles. # [7.1.11] + - Add inserting indents for lines of list if text is selected. # [7.1.10] + - Image embedding tweaks - Add MediaButton which is intened to superseed the ImageButton and VideoButton. Only image selection is working. - Implement image insert for web (image as base64) @@ -167,6 +220,7 @@ - Use merging shortcuts and actions correclty (if the key combination is the same) # [7.1.8] + - Dropdown tweaks - Add itemHeight, itemPadding, defaultItemColor for customization of dropdown items. - Remove alignment property as useless. diff --git a/README.md b/README.md index f01c472c..19ba97fa 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,39 @@ It is required to provide `filePickImpl` for toolbar image button, e.g. [Sample The `QuillToolbar` class lets you customize which formatting options are available. [Sample Page] provides sample code for advanced usage and configuration. +### Using Custom App Widget + +This project use some adaptive widgets like `AdaptiveTextSelectionToolbar` which require the following delegates: + +1. Default Material Localizations delegate +2. Default Cupertino Localizations delegate +3. Defualt Widgets Localizations delegate + +You don't need to include those since there are defined by default + but if you are using Custom app or you are overriding the `localizationsDelegates` in the App widget +then please make sure it's including those: + +```dart +localizationsDelegates: const [ + DefaultCupertinoLocalizations.delegate, + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, +], +``` + +And you might need more depending on your use case, for example if you are using custom localizations for your app, using custom app widget like [FluentApp](https://pub.dev/packages/fluent_ui) +which will also need + +```dart +localizationsDelegates: const [ + // Required localizations delegates ... + FluentLocalizations.delegate, + AppLocalizations.delegate, +], +``` + +in addition to the required delegates by this library + ### Font Size Within the editor toolbar, a drop-down with font-sizing capabilities is available. This can be enabled or disabled with `showFontSize`. @@ -203,7 +236,7 @@ QuillToolbar.basic( > For this to work, you need to add the appropriate permissions > to your `Info.plist` and `AndroidManifest.xml` files. > -> See https://github.com/natsuk4ze/gal#-get-started to add the needed lines. +> See to add the needed lines. ### Custom Size Image for Mobile diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index bd2115c8..9e1d2fc5 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -42,7 +42,6 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.app" minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion @@ -53,7 +52,6 @@ android { buildTypes { release { - // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug } diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 12e55a7a..aeb699d6 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,13 @@ - + + + + + + + { 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..efc05632 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, + ), + ), ), ), ); @@ -307,6 +344,8 @@ class _MediaLinkDialogState extends State { void _submitLink() => Navigator.pop(context, _linkController.text); String? _validateLink(String? value) { + // TODO: Use [AutoFormatMultipleLinksRule.oneLineRegExp] + // in the next update if ((value?.isEmpty ?? false) || !AutoFormatMultipleLinksRule.linkRegExp.hasMatch(value!)) { return widget.validationMessage ?? 'That is not a valid URL'; 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..59995c59 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -12,7 +12,10 @@ dependencies: flutter: sdk: flutter - flutter_quill: ^7.4.13 + flutter_quill: ^7.4.14 + # In case you are working on changes for both libraries, + # flutter_quill: + # path: ~/development/playground/framework_based/flutter/flutter-quill http: ^1.1.0 image_picker: ">=1.0.4" @@ -29,5 +32,4 @@ dev_dependencies: sdk: flutter pedantic: ^1.11.1 -# The following section is specific to Flutter packages. flutter: 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/models/rules/format.dart b/lib/src/models/rules/format.dart index 0424903a..a57bac7a 100644 --- a/lib/src/models/rules/format.dart +++ b/lib/src/models/rules/format.dart @@ -23,8 +23,13 @@ class ResolveLineFormatRule extends FormatRule { const ResolveLineFormatRule(); @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { if (attribute!.scope != AttributeScope.BLOCK) { return null; } @@ -108,8 +113,13 @@ class FormatLinkAtCaretPositionRule extends FormatRule { const FormatLinkAtCaretPositionRule(); @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { if (attribute!.key != Attribute.link.key || len! > 0) { return null; } @@ -142,8 +152,13 @@ class ResolveInlineFormatRule extends FormatRule { const ResolveInlineFormatRule(); @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { if (attribute!.scope != AttributeScope.INLINE) { return null; } @@ -182,8 +197,13 @@ class ResolveImageFormatRule extends FormatRule { const ResolveImageFormatRule(); @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { if (attribute == null || attribute.key != Attribute.style.key) { return null; } diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index b8297411..92fc461e 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -27,8 +27,13 @@ class PreserveLineStyleOnSplitRule extends InsertRule { const PreserveLineStyleOnSplitRule(); @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { if (data is! String || data != '\n') { return null; } @@ -72,8 +77,13 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { const PreserveBlockStyleOnInsertRule(); @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { if (data is! String || !data.contains('\n')) { // Only interested in text containing at least one newline character. return null; @@ -153,8 +163,13 @@ class AutoExitBlockRule extends InsertRule { } @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { if (data is! String || data != '\n') { return null; } @@ -217,8 +232,13 @@ class ResetLineFormatOnNewLineRule extends InsertRule { const ResetLineFormatOnNewLineRule(); @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { if (data is! String || data != '\n') { return null; } @@ -248,8 +268,13 @@ class InsertEmbedsRule extends InsertRule { const InsertEmbedsRule(); @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { if (data is String) { return null; } @@ -329,8 +354,31 @@ class AutoFormatMultipleLinksRule extends InsertRule { // http://www.example.com/?action=birds&brass=apparatus // https://example.net/ // URL generator tool (https://www.randomlists.com/urls) is used. - static const _linkPattern = r'^https?:\/\/[\w\-]+(\.[\w\-]+)*(:\d+)?(\/.*)?$'; - static final linkRegExp = RegExp(_linkPattern, caseSensitive: false); + + // TODO: You might want to rename those but everywhere even in + // flutter_quill_extensions + + static const _oneLinePattern = + r'^https?:\/\/[\w\-]+(\.[\w\-]+)*(:\d+)?(\/.*)?$'; + static const _detectLinkPattern = + r'https?:\/\/[\w\-]+(\.[\w\-]+)*(:\d+)?(\/[^\s]*)?'; + + /// It requires a valid link in one link + static final oneLineRegExp = RegExp( + _oneLinePattern, + caseSensitive: false, + ); + + /// It detect if there is a link in the text whatever if it in the middle etc + // Used to solve bug https://github.com/singerdmx/flutter-quill/issues/1432 + static final detectLinkRegExp = RegExp( + _detectLinkPattern, + caseSensitive: false, + ); + @Deprecated( + 'Please use [linkRegExp1] or [linkRegExp2]', + ) + static final linkRegExp = oneLineRegExp; @override Delta? applyRule( @@ -339,6 +387,7 @@ class AutoFormatMultipleLinksRule extends InsertRule { int? len, Object? data, Attribute? attribute, + Object? extraData, }) { // Only format when inserting text. if (data is! String) return null; @@ -373,8 +422,27 @@ class AutoFormatMultipleLinksRule extends InsertRule { // Build the segment of affected words. final affectedWords = '$leftWordPart$data$rightWordPart'; + var usedRegExp = detectLinkRegExp; + final alternativeLinkRegExp = extraData; + if (alternativeLinkRegExp != null) { + try { + if (alternativeLinkRegExp is! String) { + throw ArgumentError.value( + alternativeLinkRegExp, + 'alternativeLinkRegExp', + '`alternativeLinkRegExp` should be of type String', + ); + } + final regPattern = alternativeLinkRegExp; + usedRegExp = RegExp( + regPattern, + caseSensitive: false, + ); + } catch (_) {} + } + // Check for URL pattern. - final matches = linkRegExp.allMatches(affectedWords); + final matches = usedRegExp.allMatches(affectedWords); // If there are no matches, do not apply any format. if (matches.isEmpty) return null; @@ -428,8 +496,13 @@ class AutoFormatLinksRule extends InsertRule { const AutoFormatLinksRule(); @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { if (data is! String || data != ' ') { return null; } @@ -468,8 +541,13 @@ class PreserveInlineStylesRule extends InsertRule { const PreserveInlineStylesRule(); @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { if (data is! String || data.contains('\n')) { return null; } @@ -514,8 +592,13 @@ class CatchAllInsertRule extends InsertRule { const CatchAllInsertRule(); @override - Delta applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { return Delta() ..retain(index + (len ?? 0)) ..insert(data); diff --git a/lib/src/models/rules/rule.dart b/lib/src/models/rules/rule.dart index 96a6d413..45b8bdf8 100644 --- a/lib/src/models/rules/rule.dart +++ b/lib/src/models/rules/rule.dart @@ -10,19 +10,34 @@ enum RuleType { INSERT, DELETE, FORMAT } abstract class Rule { const Rule(); - Delta? apply(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { + Delta? apply( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { validateArgs(len, data, attribute); - return applyRule(document, index, - len: len, data: data, attribute: attribute); + return applyRule( + document, + index, + len: len, + data: data, + attribute: attribute, + ); } void validateArgs(int? len, Object? data, Attribute? attribute); /// Applies heuristic rule to an operation on a [document] and returns /// resulting [Delta]. - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}); + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }); RuleType get type; } 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..242faa55 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; @@ -227,8 +227,12 @@ class QuillController extends ChangeNotifier { } void replaceText( - int index, int len, Object? data, TextSelection? textSelection, - {bool ignoreFocus = false}) { + int index, + int len, + Object? data, + TextSelection? textSelection, { + bool ignoreFocus = false, + }) { assert(data is String || data is Embeddable); if (onReplaceText != null && !onReplaceText!(index, len, data)) { @@ -405,7 +409,7 @@ class QuillController extends ChangeNotifier { ); toggledStyle = style.removeAll(ignoredStyles.toSet()); } else { - toggledStyle = Style(); + toggledStyle = const Style(); } onSelectionChanged?.call(textSelection); } @@ -426,5 +430,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..4b1288b7 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -533,9 +533,14 @@ class RawEditorState extends EditorState ? const BoxConstraints.expand() : BoxConstraints( minHeight: widget.minHeight ?? 0.0, - maxHeight: widget.maxHeight ?? double.infinity); + 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 isDesktopMacOS = isMacOS(); return TextFieldTapRegion( enabled: widget.enableUnfocusOnTapOutside, @@ -550,125 +555,125 @@ class RawEditorState extends EditorState ): const HideSelectionToolbarIntent(), SingleActivator( LogicalKeyboardKey.keyZ, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const UndoTextIntent(SelectionChangedCause.keyboard), SingleActivator( LogicalKeyboardKey.keyY, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const RedoTextIntent(SelectionChangedCause.keyboard), // Selection formatting. SingleActivator( LogicalKeyboardKey.keyB, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const ToggleTextStyleIntent(Attribute.bold), SingleActivator( LogicalKeyboardKey.keyU, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const ToggleTextStyleIntent(Attribute.underline), SingleActivator( LogicalKeyboardKey.keyI, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const ToggleTextStyleIntent(Attribute.italic), SingleActivator( LogicalKeyboardKey.keyS, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, shift: true, ): const ToggleTextStyleIntent(Attribute.strikeThrough), SingleActivator( LogicalKeyboardKey.backquote, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const ToggleTextStyleIntent(Attribute.inlineCode), SingleActivator( LogicalKeyboardKey.tilde, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, shift: true, ): const ToggleTextStyleIntent(Attribute.codeBlock), SingleActivator( LogicalKeyboardKey.keyB, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, shift: true, ): const ToggleTextStyleIntent(Attribute.blockQuote), SingleActivator( LogicalKeyboardKey.keyK, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const ApplyLinkIntent(), // Lists SingleActivator( LogicalKeyboardKey.keyL, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, shift: true, ): const ToggleTextStyleIntent(Attribute.ul), SingleActivator( LogicalKeyboardKey.keyO, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, shift: true, ): const ToggleTextStyleIntent(Attribute.ol), SingleActivator( LogicalKeyboardKey.keyC, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, shift: true, ): const ApplyCheckListIntent(), // Indents SingleActivator( LogicalKeyboardKey.keyM, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const IndentSelectionIntent(true), SingleActivator( LogicalKeyboardKey.keyM, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, shift: true, ): const IndentSelectionIntent(false), // Headers SingleActivator( LogicalKeyboardKey.digit1, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const ApplyHeaderIntent(Attribute.h1), SingleActivator( LogicalKeyboardKey.digit2, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const ApplyHeaderIntent(Attribute.h2), SingleActivator( LogicalKeyboardKey.digit3, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const ApplyHeaderIntent(Attribute.h3), SingleActivator( LogicalKeyboardKey.digit0, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const ApplyHeaderIntent(Attribute.header), SingleActivator( LogicalKeyboardKey.keyG, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const InsertEmbedIntent(Attribute.image), SingleActivator( LogicalKeyboardKey.keyF, - control: !isMacOS, - meta: isMacOS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, ): const OpenSearchIntent(), }, { ...?widget.customShortcuts @@ -915,31 +920,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 +1121,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); } } @@ -1497,13 +1513,23 @@ class RawEditorState extends EditorState final index = textEditingValue.selection.baseOffset; final length = textEditingValue.selection.extentOffset - index; final copied = controller.copiedImageUrl!; - controller.replaceText(index, length, BlockEmbed.image(copied.url), null); + controller.replaceText( + index, + length, + BlockEmbed.image(copied.url), + null, + ); if (copied.styleString.isNotEmpty) { - controller.formatText(getEmbedNode(controller, index + 1).offset, 1, - StyleAttribute(copied.styleString)); + controller.formatText( + getEmbedNode(controller, index + 1).offset, + 1, + StyleAttribute(copied.styleString), + ); } controller.copiedImageUrl = null; - await Clipboard.setData(const ClipboardData(text: '')); + await Clipboard.setData( + const ClipboardData(text: ''), + ); return; } @@ -1516,7 +1542,13 @@ class RawEditorState extends EditorState final text = await Clipboard.getData(Clipboard.kTextPlain); if (text != null) { _replaceText( - ReplaceTextIntent(textEditingValue, text.text!, selection, cause)); + ReplaceTextIntent( + textEditingValue, + text.text!, + selection, + cause, + ), + ); bringIntoView(textEditingValue.selection.extent); @@ -1524,8 +1556,9 @@ class RawEditorState extends EditorState userUpdateTextEditingValue( TextEditingValue( text: textEditingValue.text, - selection: - TextSelection.collapsed(offset: textEditingValue.selection.end), + selection: TextSelection.collapsed( + offset: textEditingValue.selection.end, + ), ), cause, ); @@ -1533,14 +1566,15 @@ class RawEditorState extends EditorState return; } - if (widget.onImagePaste != null) { + final onImagePaste = widget.onImagePaste; + if (onImagePaste != null) { final image = await Pasteboard.image; if (image == null) { return; } - final imageUrl = await widget.onImagePaste!(image); + final imageUrl = await onImagePaste(image); if (imageUrl == null) { return; } @@ -2559,8 +2593,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..2e1d2374 100644 --- a/lib/src/widgets/toolbar/link_style_button.dart +++ b/lib/src/widgets/toolbar/link_style_button.dart @@ -184,51 +184,82 @@ class _LinkDialogState extends State<_LinkDialog> { super.initState(); _link = widget.link ?? ''; _text = widget.text ?? ''; - linkRegExp = widget.linkRegExp ?? AutoFormatMultipleLinksRule.linkRegExp; + linkRegExp = widget.linkRegExp ?? AutoFormatMultipleLinksRule.oneLineRegExp; _linkController = TextEditingController(text: _link); _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/link_style_button2.dart b/lib/src/widgets/toolbar/link_style_button2.dart index 98140312..895a08f6 100644 --- a/lib/src/widgets/toolbar/link_style_button2.dart +++ b/lib/src/widgets/toolbar/link_style_button2.dart @@ -379,7 +379,7 @@ class _LinkStyleDialogState extends State { String? _validateLink(String? value) { if ((value?.isEmpty ?? false) || - !AutoFormatMultipleLinksRule.linkRegExp.hasMatch(value!)) { + !AutoFormatMultipleLinksRule.oneLineRegExp.hasMatch(value!)) { return widget.validationMessage ?? 'That is not a valid URL'; } 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', () {